Thursday, July 21, 2011

Property based template parsing

The usage of string.Format is widely known and appreciated. It provides a simple and clean way to build parametrized strings. However, this method can become somewhat obscure when there is a large string (e.g. template) to be formatted. Sometimes lot of parameters are needed and all the parameter numbers make not much sense on a page sized string. In this case named parameters or template variable names would be desired.
The following code demonstrates the string.Format usage:

string menuItemName = string.Format("{0}. {1}", item.Number, item.Name); 

// Example result: "1. Soup" or "2. Spaghetti"
No rocket science here. String.Format is ideal for these kind of operations. The following code demonstrates string.Format on a larger scale:
string template = 
    "Dear {0} {1},\n\n You have requested us to notify when the product '"
    "'{3}' would become available for ordering. We are happy to inform'"
    " you that it is in stock now, please follow the following link to make an order: \n\n"
    "\t\t{4}\n\n"
    "With kind regards,\n\n"
    "Sales Team";

string message = string.Format(
    template,
    user.Title,
    user.LastName,
    product.Name,
    orderUrl);
Can you see it's hard to read this template fluently? Your mind is trying to make sense of the numbers, interpreting their definition which is found in the parameters of the Format method. Then your mind must count and figure out which value could be replaced at the right position. The more parameters, the harder it gets. Avoid these mind draining constructs, we have better things to do.

Here is the same code with name paramters:
string template = 
    "Dear {Title} {LastName},\n\n You have requested us to notify when the product'"
    " '{ProductName} would become available for ordering. We are happy to inform'"
    " you that it is in stock now, please follow the following link to make an order: \n\n"
    "\t\t{OrderUrl}\n\n"
    "With kind regards,\n\n"
    "Sales Team";

string message = template.ParseAsTemplate(
    new
    {
        Title = user.Title,
        LastName = user.LastName,
        ProductName = product.Name,
        OrderUrl = orderUrl
    });
This read much more fluently. As the example shows, there is an anonymous variable being created to act as the template's data container. You could also throw in a predefined object (like a view). This way you can parse the template immediately without creating a separate view.

To make the code above work, you have to create an extension method that extends string and resides in the root namespace of your application. Like so:
public static string ParseAsTemplate(this string template, object content)
        {
            var parser = new StringBuilder(template);

            var usableProperties = 
                from property in content.GetType().GetProperties()
                where property.CanRead
                where property.PropertyType.IsPublic
                let value = property.GetValue(content, null)
                select new 
                {
                    templateVariable = property.Name,
                    value = value ?? string.Empty                
                };

            foreach (var property in usableProperties)
            { 
                parser.Replace("{" + property.templateVariable + "}", property.value.ToString());
            }

            return parser.ToString();
        }

Notes

Uning this method has a slight pitfall. It uses a StringBuilder to replace for performance reasons, which makes it case sensitive. An anonymous data type can protect you from breaking changes when refactoring, but keep this in mind when throwing in a DTO.

It should be noted that extension methods on strings or other basic datatypes is a bad practice. I would advice to discussing implementation with your team to avoid extension methods popping up everywhere and making the dot NET framework look cluttered.

No comments:

Post a Comment