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.
Nice.
ReplyDeleteFrom 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...
Hey Mo,
ReplyDeleteThanks 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
Okay, here are some benchmark results I got:
ReplyDeleteFormat 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
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).
ReplyDeletePlease consider this suggestion!
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?
DeleteHi, 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