Performing Data Merges in C#

While HTML is easy, what about emails and data merging? Today, I show you how to perform a simple text merge with objects.

Written by Jonathan "JD" Danylko • Last Updated: • Develop •
Two Jigsaw Pieces put together with two hands

Back before Razor pages and ASP.NET, people performed "Mail Merges" using word processors to customize documents before they were sent out to customers.

Of course, with legacy ASP.NET systems, we had to provide a different way of generating the documents.

While generating HTML documents is easy, what if you wanted to send a customized email to a customer? I'm sure you have email templates available, but do you have it personalized? Can you load a record and easily replace text with that record's data in your template?

I touched on generating dynamic PDFs a while ago along with dynamic zip bundles, but this particular method provides a way to generate a simple find-and-replace with tokens embedded in a template.

Today, I focus on how to perform basic mail merges with simple objects.

We Need A Manager

With our merging process, we need a manager class to confirm everything performs exactly the way we want.

This "merge manager" should also have a common pattern of what is an object property looks like in the template. One of the common patterns is to enclose your variable with double braces.

{{Customer.FirstName}}

You can use double dashes (--Customer.FirstName--) or whatever suits your needs. Either way, our MergeManager will allow for custom prefixes and suffixes for your templates.

To start, here is our basic class.

public class MergeManager
{
    public string Prefix { get; set; }
    public string Suffix { get; set; }

    public MergeManager()     {         Prefix = @"{{";         Suffix = @"}}";     } }

As you can see, there's not much to it. We can add the different pattern format as methods.

private string GetGenericPattern(string searchFor)
{
    return $"{Prefix}{searchFor}{Suffix}";
}

private
 string GetPattern(string className, string propertyName) {     return $"{Prefix}{className}.{propertyName}{Suffix}"; }

This will give us a consistent pattern for whatever we want to use in our templates. It also centralizes the patterns in one location.

Time to Reflect

To get our values at runtime, we need to use reflection. Not only do we need to use reflection, we also have to dig into the class hierarchy.

Let's say we have a class that inherits from another class. If we place the base class in our pattern, it may not find the descendant class, leaving the pattern still in the finished template.

For example, if we have a Person class and we create a Manager class inheriting from the Person class, when we look for a {{Person.FirstName}} or {{Manager.FirstName}}, it should use the first name based on the class name. We'd need the Person and Manager in our class list to cover all of our bases.

We need to pass the object in question to return a list of classes.

protected IEnumerable<string> GetClassHierarchy(object bci)
{
    var classes = new List<string>();

    // Add default classname     var be = bci.GetType();     classes.Add(be.Name);
    while (be != null)     {         if (be.BaseType != null && be.BaseType.Name.ToLower() != "object")         {             var objectClass = be.BaseType.Name;             classes.Add(objectClass);         }
        be = be.BaseType;     }
    return classes; }

For our purpose, we'll be passing in an instance of a Customer object. If we pass that into our GetClassHierarchy(), we'll receive a list of strings containing one item: "Customer". This is considered the class hierarchy of an object (we really don't need the Object class unless people REALLY want to see {{Object.Name}}).

Next, we also need a list of the properties and values so we know what to replace. Our GetPropertyListAndValues() method will accomplish this for us.

public ICollection<KeyValuePair<string, string>> GetPropertyListAndValues(object sender)
{
    ICollection<KeyValuePair<string, string>> db = new Dictionary<string, string>();

    // Get the type of the object.     var type = sender.GetType();     var props = type.GetProperties();
    foreach (var mi in props)     {         var pi = GetPropertyInfoFromProperty(sender, mi);
        // If the property returns null         // OR we can't write to the property         // OR it's a collection, just go back.         if (pi == null             || !pi.CanWrite             || sender.IsCollection(pi.PropertyType))             continue;
        var propInfo = pi.GetValue(sender, null);         var value = GetPropertyValue(propInfo);         if (!string.IsNullOrEmpty(value))         {             db.Add(new KeyValuePair<string, string>(mi.Name, value));         }     }
    return db; }
private
 string GetPropertyValue(object propInfo) {     string val;     try     {         val = (string)Convert.ChangeType(propInfo, typeof(string));     }     catch     {         val = string.Empty;     }
    return val; }
private
 PropertyInfo GetPropertyInfoFromProperty(object sender, PropertyInfo mi) {     PropertyInfo pi;     try     {         pi = sender.GetType().GetProperty(mi.Name);     }     catch     {         pi = null;     }
    return pi; }

As you can see, I'm using small methods to accomplish small feats. Now that we have our helper methods, we're now able to work on the Merge function.

Also, I forgot to include an extension method to determine if an object is a collection.

public static class ListExtensions
{
    public static bool IsCollection(this object obj, Type propertyType)
    {
        var collectionType = typeof(ICollection<>);
        return propertyType.IsGenericType
               && collectionType.IsAssignableFrom(propertyType.GetGenericTypeDefinition())
               || propertyType.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == collectionType);
    }
}

This finishes our reflection methods to build our Merge method.

Finishing the Merge

Finally, we're ready to work on our merge method.

Our merge method will get a list of properties/values and a class hierarchy. Once we have those two items, we can perform our merge.

public string Merge(object objToMerge, string template)
{
    return Merge(objToMerge, template, String.Empty);
}

public
 string Merge(object objToMerge, string template, string key) {     // We need two things: A list of property/values and a class hierarchy.     var list = GetPropertyListAndValues(objToMerge);
    // Load the classes into a list.     var classList = GetClassHierarchy(objToMerge).ToList();
    // And add the key.     if (!string.IsNullOrEmpty(key) && !classList.Exists(e => e == key))         classList.Add(key);
    return list.Aggregate(         template,         (current, kvp) => ReplaceTemplatesWithData(current, classList, kvp)     ); }
protected
 string ReplaceTemplatesWithData(string template, IEnumerable<string> classList, KeyValuePair<string, string> kvp) {     foreach (var item in classList)     {         var pattern = GetPattern(item, kvp.Key);         template = ReplaceTemplateWith(template, pattern, kvp.Value);     }
    return template; }
protected
 string ReplaceTemplateWith(string template, string pattern, string value) {     var reg = new Regex(pattern, RegexOptions.Multiline | RegexOptions.IgnoreCase);     return reg.Replace(template, value); }

At the end of the merge, we perform an aggregate to loop over the list and return a template. If you are unclear on Aggregate, I'll refer you to Advanced Basics: Bringing it in with Aggregate.

In the ReplaceTemplateWithData() method, we create our pattern with our class list and properties. Once we find a match, we replace it with the value in the object.

Our Final MergeManager

Besides the IsCollection extension method, here is the entire code for our MergeManager.

public class MergeManager
{
    public string Prefix { get; set; }
    public string Suffix { get; set; }

    public MergeManager()     {         Prefix = @"{{";         Suffix = @"}}";     }
    public string Merge(object objToMerge, string template)     {         return Merge(objToMerge, template, String.Empty);     }
    public string Merge(object objToMerge, string template, string key)     {         // We need two things: A list of property/values and a class hierarchy.         var list = GetPropertyListAndValues(objToMerge);
        // Load the classes into a list.         var classList = GetClassHierarchy(objToMerge).ToList();
        // And add the key.         if (!string.IsNullOrEmpty(key) && !classList.Exists(e => e == key))             classList.Add(key);
        return list.Aggregate(             template,             (current, kvp) => ReplaceTemplatesWithData(current, classList, kvp)         );     }
    protected string ReplaceTemplatesWithData(string template, IEnumerable<string> classList, KeyValuePair<string, string> kvp)     {         foreach (var item in classList)         {             var pattern = GetPattern(item, kvp.Key);             template = ReplaceTemplateWith(template, pattern, kvp.Value);         }
        return template;     }
    protected string ReplaceTemplateWith(string template, string pattern, string value)     {         var reg = new Regex(pattern, RegexOptions.Multiline | RegexOptions.IgnoreCase);         return reg.Replace(template, value);     }
    public string StringMerge(string template, string searchFor, string replaceWith)     {         var pattern = GetGenericPattern(searchFor);         return ReplaceTemplateWith(template, pattern, replaceWith);     }
    protected IEnumerable<string> GetClassHierarchy(object bci)     {         var classes = new List<string>();
        // Add default classname         var be = bci.GetType();         classes.Add(be.Name);
        while (be != null)         {             if (be.BaseType != null && be.BaseType.Name.ToLower() != "object")             {                 var objectClass = be.BaseType.Name;                 classes.Add(objectClass);             }
            be = be.BaseType;         }
        return classes;     }
    public ICollection<KeyValuePair<string, string>> GetPropertyListAndValues(object sender)     {         ICollection<KeyValuePair<string, string>> db = new Dictionary<string, string>();
        // Get the type of the object.         var type = sender.GetType();         var props = type.GetProperties();
        foreach (var mi in props)         {             var pi = GetPropertyInfoFromProperty(sender, mi);
            // If the property returns null             // OR we can't write to the property             // OR it's a collection, just go back.             if (pi == null                 || !pi.CanWrite                 || sender.IsCollection(pi.PropertyType))                 continue;
            var propInfo = pi.GetValue(sender, null);             var value = GetPropertyValue(propInfo);             if (!string.IsNullOrEmpty(value))             {                 db.Add(new KeyValuePair<string, string>(mi.Name, value));             }         }
        return db;     }
    private string GetPropertyValue(object propInfo)     {         string val;         try         {             val = (string)Convert.ChangeType(propInfo, typeof(string));         }         catch         {             val = string.Empty;         }
        return val;     }
    private PropertyInfo GetPropertyInfoFromProperty(object sender, PropertyInfo mi)     {         PropertyInfo pi;         try         {             pi = sender.GetType().GetProperty(mi.Name);         }         catch         {             pi = null;         }
        return pi;     }
    private string GetGenericPattern(string searchFor)     {         return $"{Prefix}{searchFor}{Suffix}";     }
    private string GetPattern(string className, string propertyName)     {         return $"{Prefix}{className}.{propertyName}{Suffix}";     } }

Now the hard part...

How does it work?

Here's a unit test of how to use it.

[TestMethod]
public void ReturnAMergedTemplateWithObject()
{
    // Arrange
    var expected = "<html><head></head><body><h1>Hi Jonathan!</h1> <h2>Thanks for signing up to our newsletter!</h2></body></html>";
    var template = "<html><head></head><body><h1>Hi {{Customer.FirstName}}!</h1> <h2>Thanks for signing up to our newsletter!</h2></body></html>";     var subscriber = new Customer     {         FirstName = "Jonathan",         LastName = "Danylko",         SignedUp = new DateTime(2019, 09, 23)     };     var manager = new MergeManager();
    // Act     var actual = manager.Merge(subscriber, template);
    // Assert     Assert.IsTrue(actual.Equals(expected)); }

You can place any class.propertyName in the pattern. If it doesn't have a value, it will be ignored. You can add additional patterns to your library or simply use object patterns (like "{{Address.City}}" ).

Conclusion

Today, I explained how to perform a simple merge with a template and an object.

This concept is especially useful when you want to send customized emails or documents to your customers. You can also expand on this by adding more functionality using block functionality and custom object patterns.

How do you personalize your emails? A third-party does that for you? How do you merge data into text? Post your comments below and let's discuss.

ASP.NET 8 Best Practices on Amazon

ASP.NET 8 Best Practices by Jonathan Danylko


Reviewed as a "comprehensive guide" and a "roadmap to excellence" with over 120 Best Practices for ASP.NET Core 8, Jonathan's first book by Packt Publishing explores proven techniques for every phase of the SDLC.

Learn industry-standard concepts to improve your coding, debugging, and deployment of ASP.NET Core websites.

Order now on Amazon.com button

Picture of Jonathan "JD" Danylko

Jonathan "JD" Danylko is an author, web architect, and entrepreneur who's been programming for over 30 years. He's developed websites for small, medium, and Fortune 500 companies since 1996.

He currently works at Insight Enterprises as an Architect.

When asked what he likes to do in his spare time, he replies, "I like to write and I like to code. I also like to write about code."

comments powered by Disqus