July 29, 2013

Meta Programming with Razor

Have some programs that require developer time? Want to cut down the developer time needed? Razor engine may help. While it has been used mainly in ASP.net MVC, I have been using it to help fix issues I have had with jobs that needed to be developed quickly. Let me show some examples of how it can help.

Warning! First things first, you have to know what to use with the Razor Engine. If it doesn’t make sense to make something dynamic, don’t shoe horn it into the design because it’s the cool thing to do.

Now, say we have a program that is creating reports for various clients. You know each client has their own silly requirements, because, you know, the customer is always right. Now the first thing we want to do is create an object that is central to the operation of this application. Say an object that contains the client, and some reporting data.

 public class Report { public Client Customer { get; set; } public ICollection item { get; set; } public string FileNameFormat { get; set; } public string ReportFormat { get; set; } } public class Client { public string Name {get;set;} public string Address {get;set;} public string ExternalClientId{get;set;} public string Id {get;set;} } public class LineItem { public string Name{get;set;} public string Description{get;set;} public decimal Price {get;set;} public DateTime DateOfItem {get;set;} } </pre> 

So, this is the central object model the application is representing. Now you might have multiple Reports that you need to create for multiple clients. Let’s create a couple of customers and what the ordered.

 //get this from the database, or whereever. public static ICollection GetCustomers() { List results = new List() { new Report(){ Customer=new Client(){ Name="ABC Computers", Address="123 Easy Street", ExternalClientId=Guid.NewGuid().ToString(), Id="1" }, item=new List(){ new LineItem(){ Name="Modems", Description="Thing to get on the interwebs", Price=100.0m, Count=3, DateOfItem=DateTime.Now.AddDays(-5) }, new LineItem(){ Name="Monitor", Description="Thing to show you the interwebs", Price=200.0m, Count=5, DateOfItem=DateTime.Now.AddDays(-3) } } }, new Report(){ Customer=new Client(){ Name="Derp Computers", Address="246 Derp Street", ExternalClientId=Guid.NewGuid().ToString(), Id="3" }, item=new List(){ new LineItem(){ Name="Hard drive", Description="Thing to save stuff from the interwebs", Price=80.0m, Count=6, DateOfItem=DateTime.Now.AddDays(-2) }, new LineItem(){ Name="CPU", Description="Thing to process the interwebs", Price=150.0m, Count=5, DateOfItem=DateTime.Now.AddDays(-1) } } }, new Report(){ Customer=new Client(){ Name="Rofl Computers", Address="369 Easy Street", ExternalClientId=Guid.NewGuid().ToString(), Id="2" }, item=new List(){ new LineItem(){ Name="Monitor", Description="Thing to show you the interwebs", Price=200.0m, Count=10, DateOfItem=DateTime.Now.AddDays(-10) } } }, }; return results; } </pre> 

Now that we got some sample data, let’s take a look at the two property on Report: FileNameFormat and ReportFormat. These fields are going to use Razor to help create the format we need. So, let’s assign them:

 //ABC Computers like their file name formatted like this: results[0].FileNameFormat = "@(Model.Customer.ExternalClientId+DateTime.Now.ToString(\"MMddyyyy\") +\".csv\")"; //ABC Computers likes their files generated like this: results[0].ReportFormat = "@{ var format =\"\"; foreach (var item in Model.item) { format += item.DateOfItem + \",\" + item.Name + \",\" + item.Description + \",\" + item.Price + \",\" + item.Count.ToString()+Environment.NewLine; } }@(format)"; //Derp Computers like their file name formatted like this: results[0].FileNameFormat = "@(Model.Customer.ExternalClientId+DateTime.Now.ToString(\"MMddyyyy\") +\".txt\")"; //Derp Computers likes their files generated like this: results[0].ReportFormat = "@{ var format =\"\"; foreach (var item in Model.item) { format += item.DateOfItem + \"|\" + item.Name + \"|\" + item.Price + \"|\" + item.Count.ToString()+Environment.NewLine; } }@(format)"; //Rofl Computers like their file name formatted like this: results[0].FileNameFormat = "@(Model.Customer.ExternalClientId +\".csv\")"; //Rofl Computers likes their files generated like this: results[0].ReportFormat = "@{ var format =\"\"; foreach (var item in Model.item) { format += item.DateOfItem + \",\" + item.Name + \",\" + item.Price + \",\" + item.Count.ToString()+Environment.NewLine; } }@(format)"; 

This may look like crazyiness, but it makes sense, it just looks like barf when we format it into a string. If you take these items and read them in with new line formats, they can look like regular old Razor syntax you are accustom to seeing. Let’s take ABC Computer’s Report format to show how this can look:

 @{ var format =\"\"; foreach (var item in Model.item) { format += item.DateOfItem + \",\" + item.Name + \",\" + item.Description + \",\" + item.Price + \",\" + item.Count.ToString()+Environment.NewLine; } } @(format) 

What it does is it builds up a csv string, then returns it in the variable format. You can check out more about Razor here. Now all this is great, but how does this all fit? One more thing, and that’s the razor generation code:

 static void Main(string[] args) { var templateService = new TemplateService(new FluentTemplateServiceConfiguration(c => c.WithEncoding(RazorEngine.Encoding.Raw))); RazorEngine.Razor.SetTemplateService(templateService); var results = GetCustomers(); foreach (var customer in results) { string fileName = RazorEngine.Razor.Parse(customer.FileNameFormat, customer); string fileContents = RazorEngine.Razor.Parse(customer.ReportFormat, customer); File.WriteAllText(fileName, fileContents); } } /\* The output of the files are : ABC Computers FileName - 1da68569-f847-4381-95e1-50e93da8c2ce07292013.csv ABC Computers File: 7/24/2013 3:55:00 PM,Modems,Thing to get on the interwebs,100.0,3 7/26/2013 3:55:00 PM,Monitor,Thing to show you the interwebs,200.0,5 Derp Computer FileName - 1d201f40-935e-47ba-8117-68ca347f250c07292013.txt Derp Computers File: 7/27/2013 3:55:00 PM|Hard drive|80.0|6 7/28/2013 3:55:00 PM|CPU|150.0|5 Rofl Computers FileName - 5260b320-38e7-406e-b178-99f383eda2f1.csv Rofl Computers File: 7/19/2013 3:55:00 PM,Monitor,200.0,10 \*/ 

The code above shows some Razor configuration stuff. Basically we want Raw encoding because Html encoding will encode strings for Html by default. It is a good note to bring up 2 issues with the library:

1) It will store templates into memory, because it is compiling at runtime. Be sure generate the template, and save it. (there are functions to store templates for repeat use without memory leaks.)
2) This will run as the user running the code, so you may not want a user injecting malicious code into the templates.

Outside of those 2 things, I have developed an application to allow us to write customer specific code, but super quickly. One of the things we are working on is creating a UI around razor, so that we allow normies to write this, and eliminate need for development to solve new problems. If you got suggestions drop a line in the comments field.