A long time ago, I thought about how to generate emails from a template and I reached out to a few developer friends to kick around several ideas.  We talked about using excel merges, XSLT, regex, and writing a custom processing engine.  When they mentioned an engine I asked if anyone knew about spark.  Spark was a new View Engine that could be plugged into MVC to replace the default Razor View Engine; my theory was if you can swap out the entire view engine maybe you could run it outside of the context of a MVC application in a console application.  There have been a few articles about do that over time, but I never looked into it.

Fast forward to 2017, and the topic of generating emails from a template came up again and here we are.

You can find the repository for this project here - Razor In a console application.

A couple of notes-

  • the first commit is just the base files & project.  I added a readme.md, license.md, .Nuget (In case I wanted a package from my feed), .gitignore, .gitattributes, some solution folders, the Slow cheetah Nuget and a new console application project.  Finally I set the console application to use the 4.6.1 framework.  I start this way to simplify my first code review; no, I do not have anyone lined up to code review this POC, but it is habit.

On code reviews-
If you start your project and base files without changes in your first commit, it becomes easier to see what has changed in the later code reviews.

It also helps to see the thought process as the code changes in the commits.  

  • The next commit adds a data layer.  Because this is a POC I’m creating the data provider in the static constructor of the Program class.  Don’t do this; use DI, IOC, a factory, or something… I want to keep the POC simple and minimal so I am skipping a few things.
  • I am keeping the POC as minimal and simple as I can to avoid excess noise.  As a result somethings will be done in a way that they really should not be handled (like the data provider or tests)
  • We need some sort of data to “merge” with our “template”, it makes sense to use a Person and I’ll hide the implementation of the data provider behind an interface.

     public class Person
     {
         public int Id { get; set; }
         public string FirstName { get; set; }
         public string LastName { get; set; }
         public string Title { get; set; }
         public string Salutation { get; set; }

     }

    
public interface IDataProvider
     {
         IEnumerable<Person> GetEveryone();
         Person GetPerson(int id);
     }

  • I added a simple loop to write the names out to the console. code to create a directory where we will write files that contain the generated text, and a “paused exit” to the end of the console app.
  • I also added a GenerateText class in a separate “core” project where I will put all of the razor bits.  In short, I am isolating the parts that are related to razor in one dll and all of the house keeping and data elsewhere.
  • The only public method on the generatetext class is “public void GenerateTextForPeople(IEnumerable<Person> people, string directory)” Yes, it takes the people and the directory where we want the files.
  • I am not branching this repository.
  • There is no error handling; it is a POC.
  • Feel free to pull the source from the repository and look at the data and console projects, but I will not be addressing them in this post past this point.

With all of that said let’s make razor do something.

All of the following steps we done in a the “Core” dll-

  1. Instead of building a large amount of infrastructure code to run the Razor View Engine outside of an MVC app I’ve opted to use the “RazorEngine” Nuget package which has already done the heavy lifting for me.  Add the package to the project. 
  2. Add a templates folder to the project.
  3. Add a cshtml file to the templates folder and set the copy to output directory to “always copy” and the build action to “content”  I added a “welcome.cshtml” file and hardcoded it when I called for the template.…
    Note- In Visual Studio I added a “HTML” file and renamed it as cshtml file because the cshtml file type is not an option for my library project. 
  4. Because we are going to track out own IRazorEngineService I added the IDisposable interface to GenerateText so we can properly dispose of our IRazorEngineService. There is a static instance we could use, but I want to change the default configuration a bit.
  5. In the constructor we setup a custom configuration and create the instance of the “service”
  6. Next add a little code to call the service with the template key to run and compile the template.  This is important, if the cache doesn't already have the compiled template for the model it will throw an exception that there is no template with that key.  I was getting errors calling the “Run” method because the templates had not been compiled yet. 
  7. In order to get the Engine to read our cshtml file set the TemplateManager to the ResolvePathTemplateManager.  It will take an array of strings that point to where you are storing the cshtml templates. 
  8. We also change the caching provider to stop a message about cleaning up certain files.  (I will talk about that a little further down)

using RazorEngine.Configuration;
using RazorEngine.Templating;
using RazorInAConsoleApp.Data;
using System;
using System.Collections.Generic;
using System.IO;

namespace RazorInAConsoleApp.Core
{
     public class GenerateText : IDisposable
     {
         private IRazorEngineService _razor = null;
         private readonly string _templateDirectory = Path.Combine(Environment.CurrentDirectory, "Templates");
         public GenerateText()
         {
             var config = new TemplateServiceConfiguration();
             // .. configure your instance
#if DEBUG
             config.Debug = true;

#else
             config.Debug = false;
#endif
             config.DisableTempFileLocking = !config.Debug;
             config.CachingProvider = new DefaultCachingProvider(t => { });
             config.TemplateManager = new ResolvePathTemplateManager(new[] { _templateDirectory });
             _razor = RazorEngineService.Create(config);
         }
        
         public void GenerateTextForPeople(IEnumerable<Person> people, string directory)
         {
             foreach (Person person in people)
             {
                 string result = _razor.RunCompile("welcome.cshtml", modelType: typeof(Person), model: person);
                 using (StreamWriter writer = File.CreateText(Path.Combine(directory, $"{person.Id}.html")))
                 {
                     writer.Write(result);
                     writer.Flush();
                 }
             }
         }

        #region IDisposable Support
         private bool disposedValue = false; // To detect redundant calls

        protected virtual void Dispose(bool disposing)
         {
             if (!disposedValue)
             {
                 if (disposing)
                 {
                     // TODO: dispose managed state (managed objects).
                     if (_razor != null)
                     {
                         _razor.Dispose();
                     }
                 }

                // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
                 // TODO: set large fields to null.

                disposedValue = true;
             }
         }

        // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
         // ~GenerateText() {
         //   // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
         //   Dispose(false);
         // }

        // This code added to correctly implement the disposable pattern.
         public void Dispose()
         {
             // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
             Dispose(true);
             // TODO: uncomment the following line if the finalizer is overridden above.
             // GC.SuppressFinalize(this);
         }
         #endregion

    }
}

And now

Running this code generates a html file for each person.  Which was all we were after.

Some notes about the code-

  • I am toggling config.Debug based on if DEBUG is defined and setting config.DisableTempFileLocking = !config.Debug;.  In the current RazorEngine they cannot both be true at the same time. 
  • Unless you spin up a separate app domain there is a known issue where temp files to not get cleaned up properly because they are locked, so we disable the temp file locking.  You need to trust the templates to do this.  In cases where there is not a UI to edit the templates that shouldn't be an issue.  If you add a UI to edit templates you will want to revisit that issue.  If config.DisableTempFileLocking is false you will need to go to  c:\Users\<user>\AppData\Local\Temp directory and delete the temp files it will create.
  • I am just writing the output to a file, but you can do anything with output…email it…send to a wcf/rest service dump it to a database.
  • I’d likely change the constructor to a static constructor and look into using “yield” in a loop to get the generated razor text…but that is outside of this POC.
  • You can config the service at the start of the application and stick the instance in RazorEngine.Engine.Razor and use that instead of a private field.
  • I have made some changes that are not reflected in this post to the code in repository; they are not “functional” changes but “helpful”.  Like allowing the message that tells you where to the delete the temp files from if config.DisableTempFileLocking is false.
  • There is an IsolatedRazorEngineService that you can use to switch to a sandboxed version which could help reduce the risks (and may fix the file locking issue) but there are different constraints around what it can and cant do based on the configuration.  This is also outside of the POCs scope.

Roslyn

I did try to use the RazorEngine.Roslyn package but I ran into a couple of issue and found that it was still listed as a RC.  I removed it because I wanted to work with “released” code and not be on the bleeding edge.

Parting thoughts

The RazorEngine package made it easy to take advantage of the Razor View Engine.  I think it is a cleaner solution to use “template” text over RegEx and easier than creating a custom engine.


Happy coding