Wednesday, July 20, 2011

.Net ObjectFormatter - Using Tokens in a Format String

If you've already read this article and you don't feel like scrolling through my sample formats, you can jump directly to ObjectFormatter on github to get the source.

At some point in almost every business application, it seems you eventually run into the ubiquitous email notifications requirement. Suddenly, in the middle of what was once a pleasant and enjoyable project come the dozens of email templates with «guillemets» marking the myriad fields which will need replacing with data values.

You concoct some handy way of storing these templates in a database, on the file system, or in resource files. You compose your many String.Format() statements with the dozens of variables required to format the email templates and you move on to the greener pastures of application development.

Now, you've got a dozen email templates like this one:
Dear {0},

{1} has created a {2} task for your approval. This task must be reviewed between {3} and {4} to be considered for final approval.

This is an automagically generated email sent from an unmonitored email address. Please do not reply to this message. No, seriously . . . stop that. Nobody is going to read what you are typing right now. Don't you dare touch that send button. Stop right now. I hate you. I wish I could hate you to death.

Thank you,
The Task Approval Team

No big deal, everything is going swimmingly, and the application goes into beta. Then, it turns out, the stakeholders don't want an email template that looks like that. That was more of a draft really. Besides, you should've already known what they wanted in the template to begin with. After all, it's like you have ESPN or something.

It's important to add information about the user for whom this action is taking place, so this is your new template:
Dear {0},

{1} has created a {2} task for your approval regarding {5}({6}). This task must be reviewed between {3} and {4} to be considered for final approval.

If you have questions, please contact your approvals management supervisor {7}.

This is an automagically generated email sent from an unmonitored email address. Please do not reply to this message. No, seriously . . . stop that. Nobody is going to read what you are typing right now. Don't you dare touch that send button. Stop right now. I hate you. I wish I could hate you to death.

Thank you,
The Task Approval Team

So far so good. You've updated the template, updated your String.Format() parameters, passed QA and gone into production. But, now that users are actually hitting the system, it turns out that you need a few more changes. Specifically, you need to add contact information for the supervisor, remove the originator of the task, and by the way, what kind of sense does it make to put a low end limit on a deadline? Here's your new template:
Dear {0},

A {2} task for {5}({6}) is awaiting your approval. This task must be reviewed by {4} to be considered for final approval.

If you have questions, please contact your approvals management supervisor {7} at {1}.

This is an automagically generated email sent from an unmonitored email address. Please do not reply to this message. No, seriously . . . stop that. Nobody is going to read what you are typing right now. Don't you dare touch that send button. Stop right now. I hate you. I wish I could hate you to death.

Thank you,
The Task Approval Team

Now you have an email template format with various numbers all over the place, a String.Format() call with more parameters than there are tokens, and you have to go through the QA - deployment cycle again.

I've gone through this process on almost every application throughout my career as a software engineer. Hence the ObjectFormatter. Now, my email template looks like this:
Dear {Employee.FullName},

A {Task.Description} task for {TargetUser.FullName}({TargetUser.UserId}) is awaiting your approval. This task must be reviewed by {DueDate} to be considered for final approval.

If you have questions, please contact your approvals management supervisor {Supervisor.FullName} at {Supervisor.PhoneNumber}.

This is an automagically generated email sent from an unmonitored email address. Please do not reply to this message. No, seriously . . . stop that. Nobody is going to read what you are typing right now. Don't you dare touch that send button. Stop right now. I hate you. I wish I could hate you to death.

Thank you,
The Task Approval Team

I find that the ObjectFormatter makes my templating much easier to maintain and much more flexible. It also usually makes my calling code a lot cleaner. Here's an example of the approaches you could take to populate the sample templates:
// plain string formatting
String.Format(template, Employee.FullName, Supervisor.PhoneNumber, Task.Description, String.Empty, DueDate, TargetUser.FullName, TargetUser.UserId);

// if you have a dto already built
ObjectFormatter.Format(template, myDto);

// if you don't have a dto built
ObjectFormatter.Format(template, new { Employee, Supervisor, Task, DueDate, TargetUser });

I've found that most of the time they ask for template changes, they want me to add some value that is already a property on an object that's already in my object graph because of the current email template. That way, when they come tell me they want he target user's name formatted differently, I don't even need to recompile (well, sometimes I do . . . I mean, I can't predict everything). I can implement a lot of changes by using objects I already know I'm passing into the ObjectFormatter.Format() method. Here's the new template with the changes and I didn't have to change a line of code to make it work:
Dear {Employee.FullName},

A {Task.Description} task for {TargetUser.LastName}, {TargetUser.FirstName}({TargetUser.UserId}) is awaiting your approval. This task must be reviewed by {DueDate} to be considered for final approval.

If you have questions, please contact your approvals management supervisor {Supervisor.FullName} at {Supervisor.PhoneNumber}.

This is an automagically generated email sent from an unmonitored email address. Please do not reply to this message. No, seriously . . . stop that. Nobody is going to read what you are typing right now. Don't you dare touch that send button. Stop right now. I hate you. I wish I could hate you to death.

Thank you,
The Task Approval Team

If you'd like to check out the source or use the ObjectFormatter in your own projects, look for ObjectFormatter on github. If you make any cool changes, please let me know and I'll try to figure out how to merge them into the repository.

6 comments:

  1. Nice.

    From the examples in here this looks exactly like what I would want. Did you run this through any of the performance and plex tests that Haack and others subjected the other template engines to? Just curious where it stands from those standpoints - i.e. does it perform and is it stable...

    ReplyDelete
  2. Hey Mo,

    Thanks for the comment. I designed this to work almost exactly like the String.Format implementation so most of the testing I did was against string.format. I want to try Haack's tests, but I only found them today so I haven't had a chance.

    I also need to compare mine vs. Haacks and Hanselman's (and some of the others), but again . . . I just found them today. I did send out a few requests to see if I could get someone interested in contributing to an object formatting project on github and I'm hoping something positive comes of that.

    I'm going to do some benchmarking and post the results here.

    Patrick

    ReplyDelete
  3. Okay, here are some benchmark results I got:

    Format string: "{zero}{one}{two}{three}"
    100000 trials
    String.Format: 0.0742335
    ObjectFormatter.TokenFormat: 1.6142232
    StringInject.Inject: 6.9265263
    JamesNewtonKing.FormatWith: TL/DR
    PhilHaack.HaackFormat: 1.6881204
    JamesNewtonKingRegexFixed.FormatWithFixed: 2.218155
    Hanselman.ToString: 3.8891964
    HenriFormatter.HenriFormat: 1.0954822
    JonFormatter.JonFormat: 1.0179094


    Format string: "{zero}{zero}{zero}{zero}"
    100000 trials
    String.Format: 0.0656242
    ObjectFormatter.TokenFormat: 0.6653019
    StringInject.Inject: 6.6445071
    JamesNewtonKing.FormatWith: TL/DR
    PhilHaack.HaackFormat: 1.6774147
    JamesNewtonKingRegexFixed.FormatWithFixed: 2.1415488
    Hanselman.ToString: 3.8104205
    HenriFormatter.HenriFormat: 1.0488674
    JonFormatter.JonFormat: 0.9755809


    Format string: "{nested.zero}{nested.one}{nested.two}{nested.three}"
    100000 trials
    String.Format: 0.0716393
    ObjectFormatter.TokenFormat: 2.4510655
    StringInject.Inject: 6.7114474
    JamesNewtonKing.FormatWith: TL/DR
    PhilHaack.HaackFormat: 2.3328026
    JamesNewtonKingRegexFixed.FormatWithFixed: 3.0785967
    Hanselman.ToString: 1.583602
    HenriFormatter.HenriFormat: 1.7560837
    JonFormatter.JonFormat: 1.6660605


    Format string: "{nested.zero}{nested.zero}{nested.zero}{nested.zero}"
    100000 trials
    String.Format: 0.0677603
    ObjectFormatter.TokenFormat: 0.9572768
    StringInject.Inject: 6.6980271
    JamesNewtonKing.FormatWith: TL/DR
    PhilHaack.HaackFormat: 2.2206722
    JamesNewtonKingRegexFixed.FormatWithFixed: 3.0811685
    Hanselman.ToString: 1.6067289
    HenriFormatter.HenriFormat: 1.7338945
    JonFormatter.JonFormat: 1.6328507

    I think that there's something to be learned from the Databinder implementation. The ObjectFormatter gets a big benefit when you're reusing values often because of the caching. Databinder supports caching properties so that may be worth looking into. One problem is that Databinder doesn't support indexers with multiple parameters (i.e., myObject[1, 2]).

    That's an uncommon problem, but a problem all the same. I think that the ObjectFormatter does have some pretty good points for optimization and I'll keep working on it.

    Thanks again for the comment,
    Patrick

    ReplyDelete
  4. Found your project today. I'm looking into it to use it in some of our projects. What would be really great is if you created a NuGet package for it. It's easily created (create a .nuspec file (http://docs.nuget.org/docs/reference/nuspec-reference) and pack it with 'nuget pack foo.nuspec' and upload it).
    Please consider this suggestion!

    ReplyDelete
    Replies
    1. Rob, thanks for the comment. I'll put that on my to do list. It may be a little while before I get to it (I'm having a baby in a week or two), but if it's worth adding to nuget, I'll do it. Is it alright if I contact you for more details?

      Delete
  5. Hi, maybe I'm blind, but I just can't find the licensing information for this code. This would help tremendously, but only if it can be used..

    ReplyDelete