How to integrate the MVC Razor Engine, using Roslyn, in a .NET Core 2 Console Application



Jim Mc̮̑̑̑͒G
  ·  
15 February 2019  
  ·  
 26 min read

MVC Razor templates only work with .NET ASP MVC websites - this article shows you a way to get them working with .NET Core Console projects without a dependency on a web project.



Background

It’s rare to find real-world applications that do not have a requirement to send out an email of some description.

In many cases, sending an email from the context of a website is absolutely fine. Perhaps you wish to send a customer confirmation email when the user submits a form for example.

However, there are many scenarios where an email is not generated by a website directly – for example, scheduled jobs that process batches of records overnight.

The case for using an “email template” of some description is fairly obvious. However, many organisations attempt to reinvent the wheel, frequently with solutions that aren’t particularly flexible. I’ve seen numerous examples that use only basic word-substitution techniques.

MVC Razor templates are ideal, as they are widely understood by developers and provide decent programmatic integration. Embedded logic, such as iteration and conditional branching, is a compelling feature to have at your disposal.



The problem

If you’ve been researching for a way to integrate Razor templating, you’ll have discovered that most examples on the web, achieve this by using a direct dependency on MVC - they effectively assume that you are only using a website project.

However, things start to unravel as soon as you attempt to create console projects - which you will be if you intend to have processes, such as an Azure WebJob, running on a schedule.

Aside from the dependency on an MVC project, there are other subtle issues that arise when using the MVC framework. ASP.NET MVC is convention based – for example, views by default are expected to be located in the View folder. The impact of this, however, is that if you were to include email templates as part of your website project, you would be required to keep those files alongside the website Views - and not tidied away in a separate location.

In this article, I’ll show you how to use Roslyn, the .NET compiler, to leverage the Razor Engine in other project types, and define your project in a way that better suits you.



A quick recap of how Razor works

Razor templates have always been dynamically compiled. ASP.NET MVC websites do a terrific job of masking the developer away from most of the complexities, but behind the scenes, it’s useful to know that .cshtml files are ultimately converted into executable code. This happens dynamically and you don’t need to compile your views as you do with the rest of your code.

You can prove this quickly: go to any running MVC website and edit the template; you’ll see the change take effect as soon as you save the file.

When you don’t have access to an MVC project, you don’t have access to all this built-in behaviour, so we need to recreate the process of rendering a template manually.



Prerequisites and technologies used

This project was primarily created using .NET Core 2 on a Windows 10 system with Visual Studio 2017 Community edition.

  • You’ll need to have already installed the .NET Core 2.x SDK, as appropriate to your platform. You can get that from the Microsoft download site.

  • Visual Studio 2017, Community Edition, or Visual Studio Code. Either can be downloaded from the Visual Studio Download page.



Prior knowledge of:

  • Generics, so that we can use any variety of different ViewModels in our templating engine.

  • MVC websites and the concepts of combining MVC Views and ViewModels.

  • Razor syntax.

We’ll be using Roslyn, the .NET Compiler, to dynamically create an assembly at runtime. It’s reasonable to say that compilers are a complicated subject and well beyond the scope of this article – but we will talk about how we’re going to interact with Roslyn as we go along. I don’t believe that you will need to have a deep understanding of Roslyn in order to just get a working result.

We’ll also be using Microsoft’s RazorProjectEngine library (and related items), which are part of the Microsoft.AspNetCore.Razor namespace.



Anything else we need to know before we get started?

Throughout the article, you’ll hear me referring to “email templates” - I do this simply because this code is likely to be used to address this usage scenario. However, there is nothing inherently specific to the use of emails in this demo. Therefore, if you have other usage scenarios, they should work just fine too.

Similarly, we won’t be covering the topic of actually creating an email object and sending it - we’re simply getting you to the point of creating the “body text” that you could then use in your email.

UPDATE Sept 2019. A GitHub Issue has been raised, identifying incompatibility issues if you use more recent versions of Microsoft Nuget packages. If you are running into problems, make sure you are using the versions that are defined in the project file.

If you get stuck or can’t follow along with the instructions, don’t worry, the complete code for this project is available on GitHub: https://github.com/SiliconOrchid/RoslynRazorTemplating


UPDATE Nov 2019. The project has been updated, so the source code found in the GitHub master branch will no longer tie directly to this editorial. Please take care to refer to the “V1” branch for the code originally presented along with this article.

If you are looking to use this code in your own projects, then you should consider using the code in the master branch and refer to both this article and the newer article (TBC)[http://blogs.siliconorchid.com/], which returns to cover the project revisions, for the complete picture!



Overview of what we’re about to do

We’re going to be creating a template base model that will contain basic methods for creating our output. The output is ultimately just a big long string of HTML.

  • We’ll create a very simple sample ViewModel to show you how to fit things together.

  • We’ll create a helper class that performs the bulk of the work in this project. This will be a library class that:

    • Loads the source Razor template file from disk

    • Compiles the template dynamically into memory using Roslyn

    • Returns an assembly object that is an executable version of the template

  • Create an additional helper class used to merge the compiled version of the template with data in a ViewModel and return rendered HTML.

  • Create a console project that orchestrates the process; it’ll set up the models, invoke the helper methods and, ultimately, display the markup to the console.



Create and configure the solution

Create default projects

  • In Visual Studio, create a new Blank Solution and name it RoslynRazorTemplating. Blank solutions can be found within the “Other Project Types” node of the “Add New Project” dialogue.

  • Next, we’ll create two separate projects within the solution. In the “Add New Project” dialogue, filter the list to show “.NET Core” project types. When you add the new projects, you can leave any options in their default state.

  • Create a Class Library (.NET Core) project and name it RazorEngineTemplating. This library project will eventually contain most of the code needed to make this demo work. In this article, we’ll be referring to this project as “the library project”.

  • Create a Console App (.NET Core ) project and name it EmailTemplateDemo. This will be a very basic console project that will be responsible for setting up the demo, calling our library class methods and ultimately displaying HTML output to the console window. In this article, we’ll be referring to this project as “the host project”.

Add project dependencies

  • For the host project EmailTemplateDemo, create a project dependency to the library project RazorEngineTemplating. This project does not require any other dependencies.

  • For the library project RazorEngineTemplating, create Nuget dependencies to the following packages:

    • Microsoft.AspNetCore.All

    • Microsoft.AspNetCore.Razor.Runtime

    • Microsoft.AspNetCore.Razor.Language

    • Create a folder for the email templates

    • At the root of the EmailTemplateDemo project, create a new folder: EmailTemplates.

The context-specific email template

Now that we’ve set up an empty shell of a solution, let’s begin actual coding by working on one of the more straightforward parts the demo, an email template.

Both parts of the template are expected to live in the project (assembly) that is calling the engine - not the RazorEngine project directly. So in the case of this demo, the email templates live in the EmailTemplateDemo project.

As with regular ASP.NET MVC, when we talk about a “template” in this context, we’re referring to a composite of a View and a ViewModel.

The Email Template View

The View should contain HTML markup punctuated with Razor syntax. Any template file needs to be: Located in the host project, in a folder titled EmailTemplates.

By hard-coded convention, the demo will expect you to put the files in a root folder of the host project called EmailTemplates. This is something that you can easily redefine if you prefer. A better idea might be to make this something that is driven by project app settings, but we’ll keep it simple for now.

Included in the build output when you compile the project.

The .cshtml file needs to be available at runtime (as opposed to something such as a c# code file which is a development-time resource. Because of this, we need to make sure it gets deployed along with the compiled code when we build the project.

When we edited the EmailTemplateDemo.csproj file in a previous step, we added a directive which tells the build system to copy our demonstration template file SampleEmailTemplate.cshtml, so we don’t need to do anything now.

Defining that a file should be copied in the build is easy to do, but also easy to forget. When you come to work on your own projects, if you’re having problems, make sure to check that you’ve performed this step. You can either add CopyToOutputDirectory entries manually into the .csproj file, or, if you’re using Visual Studio, right-click the .cshtml file, select Properties and choose the Copy always option under the Copy to Output Directory menu item.

When correctly added, if you inspect the EmailTemplateDemo.csproj you should see an entry that looks like this:

<ItemGroup>
  <None Update="EmailTemplates\SampleEmailTemplate.cshtml">
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </None>
</ItemGroup>

Copying template files to the build is not a concept unique to this project; this is another one of the things that the ASP.NET Core MVC framework does for you out of the box through its use of conventions. The only difference in this project is that we just need to manually make sure that it gets copied in the build.

Next, let’s add in the code for this View:

In the newly created folder EmailTemplateDemo, create a new file SampleEmailTemplate.cshtml. Strictly speaking the file does not need the file extensions .cshtml - it could be anything that you want including a simple .txt file - but it makes the purpose much clearer if we follow convention. In that empty file, add in the following code:

@inherits RazorEngineTemplating.RazorEngineBaseTemplate<EmailTemplateDemo.EmailTemplates.SampleEmailTemplateModel>
<!doctype html>
<html>
  <head>
      <title>@(Model.EmailTagline)</title>
  </head>

  <body>
      <div>
          <h1>@(Model.EmailTagline)</h1>
      </div>
                
      <div>
          @foreach (var item in Model.ListCollectionItems)
          {
              <div>
                  <p>Description: @item.CollectionItemDescription</p>
              </div>
          }
      </div>
  </body>
</html>

In the above code, we’re telling the View which Model it should be using. The important thing to note here is that, contrary to what you may be more used to, we’re not telling the View to directly use the ViewModel. Instead, we’re adding a reference to a base-model (which we’ll talk about shortly). The ViewModel is still part of this picture though, as it is specified as a generic argument of the base model.

Seasoned MVC developers may have noticed that we are using the @inherits keyword and not @model. They are very similar, but handle generic expressions slightly different. The inherit keyword is actually a throwback to an early version of the MVC framework, before the model directive was introduced in MVC3. In our code, we need to be quite verbose in how we specify both the base-model and the view-specific ViewModel, which is why we use inherits. Another explanation can be found on this helpful StackOverflow article.

There are two straightforward, but key, elements to note in the template above:

  • An example of a super-simple string item, with the use of Model.EmailTagLine
  • An example of iteration, with the use of @foreach (var item in Model.ListCollectionItems)

With regard to the markup used, I have been deliberately terse for the demo - obviously, in your own email template you could get as creative with layout and styling as you need.



The Email Template ViewModel

Create the file EmailTemplateDemo\EmailTemplates\SampleEmailTemplateModel.cs and copy in the following code into the stubs provided in the skeleton project:

using System.Collections.Generic;

namespace EmailTemplateDemo.EmailTemplates
{
  public class SampleEmailTemplateModel
  {
      public string EmailTagline { get; set; }
      public List<SampleEmailTemplateModelCollectionItem> ListCollectionItems { get; set; }


      public SampleEmailTemplateModel()
      {
          ListCollectionItems = new List<SampleEmailTemplateModelCollectionItem>();
      }
  }

  public class SampleEmailTemplateModelCollectionItem
  {
      public string CollectionItemDescription { get; set; }
  }
}

As with the counterpart .cshtml file, this ViewModel is just a regular MVC ViewModel; it is essentially just a container for any strongly-typed data that you will be merging into the template.

I have included the class SampleEmailTemplateCollectionItem, which is there purely to demonstrate iteration in the view using a strongly-typed object.



The Generic Base Template Model

In a previous step, we added a base-model to the View, using the @inherits directive. Let’s look at that base-model more closely:

  • The RazorEngineBaseTemplate<TTemplateViewModel> model is located within the RazorEngineTemplating project and serves a couple of purposes.
  • It provides a generic attribute, TTemplateViewModel, which will contain our View-specific ViewModel.
  • It provides a private StringBuilder object which we will use to build up a document-sized string of HTML.
  • It contains methods that are used by the underlying Razor engine to build up lines of an HTML document.
  • It contains a method used to return the completed string from the StringBuilder.
  • During the dynamic compilation process, this base model will be extended with functionality available in the Microsoft library class RazorPage, part of the Microsoft.AspNetCore.Mvc.Razor Namespace.
  • For this code to compile, there are just a handful of attributes and methods that we need to implement:

    • The attribute Model aligns with the attribute of the same name in the class RazorPage

    • Write() and WriteLiteral(); These are RazorPageBase methods (where RazorPageBase is the base class of RazorPage<TModel>) used to emit encoded and unencoded HTML, respectively, to the output.

    • ExecuteAsync(); This method is called to trigger the actual rendering of the template. In our case, we don’t need to add any behaviour. We do however need to include the method in our model, returning an Async Task type in order for our code to compile. The minimum code needed to return this response is a very basic await Task.Yield();.

Create a new file in the root of the RazorEngineTemplating project called RazorEngineBaseTemplate.cs and copy in the following code:

using System.Text;
using System.Threading.Tasks;

namespace RazorEngineTemplating
{
  public abstract class RazorEngineBaseTemplate<TTemplateViewModel>
  {
      private readonly StringBuilder stringBuilder = new StringBuilder();

      public TTemplateViewModel Model;

      public void WriteLiteral(string literal)
      {
          stringBuilder.Append(literal);
      }

      public void Write(object obj)
      {
          stringBuilder.Append(obj.ToString());
      }

      public string GetMarkup()
      {
          return stringBuilder.ToString();
      }

      public async virtual Task ExecuteAsync()
      {
          await Task.Yield();
      }
  }
}



image showing the Simpsons Santas little helper character

The RazorEngineDynamicCompilerHelper Class

We can now look at the class that configures and parses the runtime compilation. It is arguably the most complex part of the solution.

We’ll be adding each part of the code to the skeleton project, talking about each in turn.

This class uses generic types <T>, as identified by TTemplateViewModel. This represents your template-specific ViewModel.

  • Create the new file RazorEngineTemplating\RazorEngineDynamicCompilerHelper.cs and copy in the following basic code to get us started:
using System;
using System.IO;
using System.Reflection;

using Microsoft.AspNetCore.Razor.Hosting;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace RazorEngineTemplating
{
  public static class RazorEngineDynamicCompilerHelper<TTemplateViewModel>
  {
  }
}

Constants Next, near to the top of the class, copy in the following private constant:

public static class RazorEngineDynamicCompilerHelper<TTemplateViewModel>
{
  private const string razorTemplateBaseFolder = "EmailTemplates";

The razorTemplateBaseFolder constant is used to define the name of the folder, in the host project, where we expect to find the .cshtml template file.

In this demo, the definition of razorTemplateBaseFolder has been hard-coded for simplicity. If you use this code in your real-life solutions, I would consider making this configurable - e.g. combining with appSettings configuration.



The CompileTemplate Method

Whilst continuing to work with the file RazorEngineTemplating\RazorEngineDynamicCompilerHelper.cs, add in the following method:

public static Assembly CompileTemplate(string razorTemplateFileName, string dynamicAssemblyNamespace, string dynamicDllName)
{
  PortableExecutableReference referenceToCallingAssembly = MetadataReference.CreateFromFile(Assembly.GetCallingAssembly().Location);
  RazorProjectEngine razorProjectEngine;
  RazorProjectFileSystem razorProjectFileSystem = InitialiseTemplateProject(dynamicAssemblyNamespace, out razorProjectEngine);
  RazorProjectItem razorProjectItem = GetRazorProjectItem(razorProjectFileSystem, razorTemplateFileName);
  SyntaxTree cSharpSyntaxTree = GenerateSyntaxTree(razorProjectItem, razorProjectEngine);
  CSharpCompilation cSharpCompilation = CompileDynamicAssembly(dynamicDllName, cSharpSyntaxTree, referenceToCallingAssembly);

  return StreamAssemblyInMemory(cSharpCompilation);
}

There is not a great deal to talk about regarding the CompileTemplate method; it is the only publicly exposed method in this class and is intended to be called by a host project. This method simply calls a number of private methods that configure the dynamic compilation and ultimately return a dynamically compiled Assembly object.



The InitialiseTemplateProject Method

This method begins the process of hooking into Microsoft’s Razor engine.

Although objects with names such as ProjectFileSystem appear to sound like we’re going to be reading our template file from disk, this is not what is happening at this stage.

Instead, we are setting up an object in memory that will contain everything the engine needs to know about the RazorProject assembly that we are creating.

Although the code is a bit complex looking, there’s not really much that we, as consumers of this API, need to get too involved in. All we’re doing here is saying “let’s create an empty file system and create a default empty project”.

We need to tell the project-builder the namespace we would like our new compilation to use. Because this value is being passed through from the public method CompileTemplate, the actual values for this will be defined in the host project, which we’ll come back to later.

private static RazorProjectFileSystem InitialiseTemplateProject(string dynamicAssemblyNamespace, out RazorProjectEngine razorProjectEngine)
{
  RazorProjectFileSystem razorProjectFileSystem = RazorProjectFileSystem.Create(".");

  razorProjectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, razorProjectFileSystem, (builder) =>
  {
      InheritsDirective.Register(builder);
      builder.SetNamespace(dynamicAssemblyNamespace);
  });
   return razorProjectFileSystem;
}



The GetRazorProjectItem Method

In the GetRazorProjectItem method we build on the previous step, taking an empty RazorProjectFileSystem and adding our .cshtml template file to it.

As a recap, we don’t define our template as a file in this project - that happens in the host project - but we do need the code we’re looking at now to be able to locate that file at runtime. There are two things that come into play here:

  • We pass the filename of the .cshtml template as an argument into this class - this is defined in the method call of the host project, the Program.cs of EmailTemplateDemo

  • The filename provided isn’t a full path, it is literally just a filename. We expect the .cshtml artifact to be located in a predetermined folder of our built output. That location is defined in the constant razorTemplateBaseFolder.

private static RazorProjectItem GetRazorProjectItem(RazorProjectFileSystem razorProjectFileSystem, string razorTemplateFileName)
{
  return razorProjectFileSystem.GetItem($"{_razorTemplateBaseFolder}/{razorTemplateFileName}");
}



image showing trees

The GenerateSyntaxTree Method

As with previous steps, as a consumer of the underlying APIs, the GenerateSyntaxTree method is orchestration code and there isn’t a great deal for us to be getting involved with.

To quickly give an overview of what is happening, this step revolves around taking code defined as Razor syntax, parsing it then processing it into a raw C# equivalent. If you were to inspect this intermediary C# code, you’d see things such as calls to methods for writing strings.

The compiler doesn’t directly read your raw-text C# code, but rather a distilled version of it. A Syntax Tree is a version of your code which the compiler understands.

The SyntaxTree object is returned as output from this method and this will be used in the actual compilation which happens in a subsequent step.

private static  SyntaxTree GenerateSyntaxTree(RazorProjectItem razorProjectItem, RazorProjectEngine razorProjectEngine)
{
  RazorCodeDocument razorCodeDocument = razorProjectEngine.Process(razorProjectItem);
  RazorCSharpDocument razorCSharpDocument = razorCodeDocument.GetCSharpDocument();
  return CSharpSyntaxTree.ParseText(razorCSharpDocument.GeneratedCode);
}



The CompileDynamicAssembly Method

In this method, we define a dynamic project that is to be compiled.

There’s quite a bit more going on in this part of the code. If you find yourself debugging or maintaining a version of this code, this is the place you’ll probably find yourself returning to change or modify.

The code in this method: * creates a new compilation object, * gives the compilation object a name, * supplies our previously created template as a SyntaxTree object, * specifies that the output is going to be a .dll, and * defines a list of references to other libraries that we want to be in scope for the build.

You’ll see an array of MetaDataReferences. These tell the compiler where to look for all the libraries we’re going to need.

Within these MetaDataReferences, rather than hard-code the paths to the .dll files, we can locate them dynamically using the static method Assembly.Location.

Amongst the references, you’ll see three lines that pull in essential libraries (Core, Runtime and .Net Standard Libraries, respectively). You can also see that a reference to the RazorEngine library is included. Additionally, you’ll find a reference to the assembly where the code for the base template can be found.

Whilst still working with RazorEngineTemplating\RazorEngineDynamicCompilerHelper.cs, let’s add the following code:

private static CSharpCompilation CompileDynamicAssembly(string dynamicDllName, SyntaxTree cSharpSyntaxTree, PortableExecutableReference referenceToCallingAssembly)
{
  return CSharpCompilation.Create(dynamicDllName, new[] { cSharpSyntaxTree },
      new[]
      {
          MetadataReference.CreateFromFile(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location),"System.Private.CoreLib.dll")),
          MetadataReference.CreateFromFile(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location),"System.Runtime.dll")),
          MetadataReference.CreateFromFile(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location),"netstandard.dll")),

          MetadataReference.CreateFromFile(typeof(RazorCompiledItemAttribute).Assembly.Location),
          MetadataReference.CreateFromFile(typeof(RazorEngineBaseTemplate<TTemplateViewModel>).Assembly.Location),
          referenceToCallingAssembly,
        
          MetadataReference.CreateFromFile(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location),"System.Collections.dll")),
          },
      new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
}

There are two things in the above code that I would draw your attention to:

Explanation regarding the use of ‘referenceToCallingAssembly’

We need a reference to the host project. This is so that we can include the assembly, in which the code for the ViewModel exists, in the list of assemblies to be included in the compilation. You may have noticed that, nestled amongst the other library references, is an item called referenceToCallingAssembly. This is a reference to the host project, that is passed as an object argument from the main public method CompileTemplate to the private method CompileDynamicAssembly.

To get our reference to the host project EmailTemplateDemo, we want to use Assembly.GetExecutingAssembly().Location to determine the actual location of that assembly. However, we can’t determine this from within the context of the private method CompileDynamicAssembly. This is because the GetExecutingAssembly would see the call as originating from the “parent” CompileTemplate method and simply tell us that the location is the same as the current code. Instead, a workaround is that we obtain this reference from within the “parent” method CompileTemplate (which is the method that we directly call from the host project) and pass this information as an argument.

Potential issues with library use in your template.

There is a brittleness in this code that could potentially cause problems for you. If you attempt to include additional libraries in your .cshtml template, beyond those already available in this demo, you will need to add a reference to that library as an additional line in the collection build references.

For example, our demo template showcases how to use iteration of a collection. Without the inclusion of a reference to System.Collections.dll, we would see an error The type 'List<>' is defined in an assembly that is not referenced. You must add a reference to assembly.

The compilation generally appears to fail with meaningful errors though, the exception messages will tell you which references are missing. The key point to take away, is that you must include any missing references in the collection of libraries defined in CompileDynamicAssembly

If you’ve identified which library you need to add, you can adapt the following line to your own purposes (i.e. replace the string “System.Collections.dll”):

MetadataReference.CreateFromFile(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location),"System.Collections.dll"))

If you don’t know which library you need, by name, an alternative is to use the following example, where you can specify a type instead:

MetadataReference.CreateFromFile(typeof(<YOUR_TYPE>).Assembly.Location),



image showing a stream

The StreamAssemblyInMemory Method

Compilation can be a relatively slow process, so we need a way to deal with the (again, relatively) slowly-received stream of data. You can think of this process as being a bit like reading a file from a disk byte by byte.

In this method, we’ll read the output from the compiler into a memory stream. Once fully received, we’ll turn this into an assembly object ready for actual execution.

Using the StreamAssemblyInMemory method, we take the CSharpCompilation object that we created in the previous step and emit it into a memory stream via var result = cSharpCompilation.Emit(memoryStream);.

We then have a section of code that checks that the compilation has been returned without issue. If there is a problem, we throw a new Exception, passing on the error message raised by the compiler. Assuming the code is now ready to go, we simply seek the beginning of the memory array and load the array, converting into an Assembly object.

We’re still working with RazorEngineTemplating\RazorEngineDynamicCompilerHelper.cs, so add the following method:

private Assembly StreamAssemblyInMemory(CSharpCompilation cSharpCompilation)
{
  Assembly dynamicAssembly;

  using (var memoryStream = new MemoryStream())
  {
      var result = cSharpCompilation.Emit(memoryStream);
      if (!result.Success)
      {
          string errorMessage = (string.Join(Environment.NewLine, result.Diagnostics));
          throw new Exception(errorMessage);
      }
      else
      {
          memoryStream.Seek(0, SeekOrigin.Begin);
          dynamicAssembly = Assembly.Load(memoryStream.ToArray());
      }
  }

  return dynamicAssembly;
}



RazorEngineTemplateHelper Class

With the template having been dynamically compiled and made ready for use in previous steps, we now need to merge it with the ViewModel data and render HTML markup.

Create a new file in the RazorEngineTemplating project called RazorEngineTemplateHelper.cs and add the following code:

using System;
using System.Reflection;

namespace RazorEngineTemplating
{
  public static class RazorEngineTemplateHelper<TTemplateViewModel>
  {
      public static string MergeViewModelReturnMarkup(TTemplateViewModel templateViewModel, Assembly templateAssembly, string dynamicAssemblyNamespace)
      {
          RazorEngineBaseTemplate<TTemplateViewModel> razorEngineBaseTemplate =
            (RazorEngineBaseTemplate<TTemplateViewModel>)Activator.CreateInstance(templateAssembly.GetType($"{dynamicAssemblyNamespace}.Template"));

          razorEngineBaseTemplate.Model = templateViewModel;
          razorEngineBaseTemplate.ExecuteAsync().Wait();
          return razorEngineBaseTemplate.GetMarkup();
      }
  }
}

The MergeViewModelReturnMarkup method accepts three arguments:

  • A ViewModel containing an object populated with data to be merged into the template. This will match the type declared in the generic argument that was used when calling this class.

  • An Assembly object containing the compiled version of the template

  • A string containing the namespace that was assigned to the assembly when we compiled it.

The key processes happening in the method are:

  • We create an instance of the RazorEngineBaseTemplate object that is extended with Microsoft RazorEngine functionality.

  • We then assign our ViewModel payload, which was originally passed as an argument on the main public method of this class, originating from the host project, to the instance.

  • The ExecuteAsync() method is called to trigger the actual rendering.

  • We call the GetMarkup() method to return the string output. This is ultimately returned to the host project where the string is used as appropriate.

The first of those bullet points are worth exploring in greater depth as the syntax used to create the object is a little difficult to follow. Let’s look specifically at the following line of code:

RazorEngineBaseTemplate<TTemplateViewModel> razorEngineBaseTemplate = (RazorEngineBaseTemplate<TTemplateViewModel>)Activator.CreateInstance(templateAssembly.GetType($"{dynamicAssemblyNamespace}.Template"));

We use Activator.CreateInstance to create a new object, specifying that we now use the Assembly that we previously compiled to do this job. It is this mechanism that ultimately extends the base model with all of the RazorEngine functionality.

We also specify the namespace used by that assembly - this will be the same namespace that we used during the compilation definition. In this demo, we pass the value in from the host project so it will match up.

We specify the .Template attribute. ‘Template’ is something that is defined in the libraries by Microsoft.

Hang on, why two helper classes?

As we were looking at this code you may have asked yourself “why not simply have the RazorEngineDynamicCompilerHelper class (that we looked at previously) return the rendered HTML and therefore just have a single helper class?”

For the purpose of this demo, having the MergeViewModelReturnMarkup method as part of the RazorEngineDynamicCompilerHelper class could have been an option, because in this demo we’re only rendering a single page of HTML from a single instance of a populated ViewModel.

However, by separating the code into separate classes, we’re acknowledging the SOLID design principle of “Single Responsibility” by making it clearer that:

  • RazorEngineDynamicCompilerHelper is responsible for generating a compiled template,
  • RazorEngineTemplateHelper is responsible for generating HTML markup

This distinction becomes fully relevant when we want to generate multiple different HTML documents from the same compiled template.

We can achieve this by using RazorEngineTemplateHelper to merge different instances of ViewModels, each populated with different datasets, whilst reusing a single instance of the compiled template.



The host project

Next, we’re going to begin work on a different part of the project. In the EmailTemplateDemo project (that you created as one of the first steps), open up the file Program.cs and add in the following code to the existing Main method:

static void Main(string[] args)
{
  var templateModel = new SampleEmailTemplateModel {
      EmailTagline = "Markup Generated With the Razor Engine !"
  };

  templateModel.ListCollectionItems.Add(new SampleEmailTemplateModelCollectionItem { CollectionItemDescription = "This is the first item (we can show a collection of data)" });
  templateModel.ListCollectionItems.Add(new SampleEmailTemplateModelCollectionItem { CollectionItemDescription = "This is the second item (so our demo is demonstrating iteration)" });

  string dynamicAssemblyNamespace = "EmailTemplateDynamicAssembly";
  string dynamicDllName = "EmailTemplateDynamicAssemblyDllName";

  Assembly templateAssembly = RazorEngineDynamicCompilerHelper<SampleEmailTemplateModel>.CompileTemplate("SampleEmailTemplate.cshtml", dynamicAssemblyNamespace, dynamicDllName);
  Console.Write(RazorEngineTemplateHelper<SampleEmailTemplateModel>.RenderTemplate(templateModel, templateAssembly, dynamicAssemblyNamespace));

  Console.ReadKey();
}

The console project is deliberately simple and Program.cs has just a few lines of code:

  • Populate a test ViewModel object.
  • Define string values for the template assembly namespace and filename.
  • Call the helper method RazorEngineDynamicCompilerHelper<T>.CompileTemplate
  • Call the helper method RazorEngineTemplateHelper<T>.RenderTemplate.
  • The output will simply be written to the console for the purpose of this demo.



image showing family testing toys

Build and test the project

The intended point of execution in this demo is the EmailTemplateDemo project. To test your code, using a command prompt:

  • Change Directory to the EmailTemplateDemo folder : cd [your source path]\RoslynRazorTemplating\src\EmailTemplateDemo

  • Simply run the console project : dotnet run

If the code is building as expected, you should see the following output in the console when you run the host project:

image showing successful console output

Be mindful that the compilation of your code, followed by the dynamic Roslyn compilation, will take a few moments … so allow a good few seconds to pass before you see any output.

Where to take this project next?

Dynamic compilation is a powerful tool to have at your disposal, but careless integration could cause you severe performance headaches. The act of bringing Roslyn into the runtime and the actual compilation step itself is computationally expensive, so you want to avoid repeating this work as much as possible.

What does this mean in practice? At a minimum, you don’t want to be dynamically compiling a new template for each individual email you are sending. I mean this seriously - don’t do this!

Remember, that a single compiled template can be reused over and over again to render many different pages of dynamic content, just like a regular MVC webpage. If you’re not planning to retain your compiled template, perhaps aim to work with batches where possible. E.g. Compile a single template object and keep making repeated calls to the RenderTemplateToString() method, supplying differently populated ViewModels on each iteration.

In a more complicated system, you would persist the dynamic compilation, either by saving to disk semi-permanently or caching for longer periods.

If you are writing the .dll to disk, you will need to take care over issues such as file permissions and naming, and also take care of file locks if reading the assembly back from disk.

If you are looking to use caching, storing a serialised version of the CSharpCompilation into an in-memory cache might work for you.

If you know that your templates are going to remain relatively unchanged, you may even want to explore the possibility of taking the process of template-creation completely out of your runtime code and prepare the .dlls separately.

References, resources and further reading

A challenge at the time this post was originally written, in the Summer 2018, is that there is not a much in the way of digested information available. Here are some links that could be of use to you for further reading and research:





Archives

2021 (1)
2020 (26)
2019 (27)