Razor Template Rendering

Use the Razor template engine from ASP.Net Core to render some data in a template pulled from a database. I also wanted to use only the Microsoft Razor libraries to do it and any other 3rd party libraries.

My requirements seemed simple, use the Razor template engine from ASP.Net Core to render some data in a template pulled from a database. I also wanted to use only the Microsoft Razor libraries to do it and any other 3rd party libraries.

.NET 5.0

This blog post details how to do it with .NET Core 2.2. The example code at the repository has been updated for .NET 5.0. However, the majority of this post still stands up for .NET 5.0. The key differences is creating a class that implements IWebHostEnvironment and using a FrameworkReference instead of referencing the individual NuGet packages. I cannot take credit for the updates, someone else did that and submitted a pull request which I gladly accepted.

You can see the code changes here:

Update to work with net5.0 & and ASP.NET Core 5.0 ยท veccsolutions/RenderRazorConsole@ccdc2b8
Companion code to https://www.frakkingsweet.com/razor-template-rendering/ - veccsolutions/RenderRazorConsole

Requirements

I need to support as much of the underlying Razor view features as possible, especially the dependency injection.

To keep this example simple, I am going to go over 3 templates sourced from static strings. I will leave it up to you as to how you want to get the template data. The 3 templates are as follows:

  • A template that dumps out the current local date and time. It has does not have a model or any injection. This proves that a simple tiny view works.
  • A template that takes in a model that contains a property of an array of strings. It then loops through the array and dumps out the contents. This proves the ability to use a model in the template.
  • A much more complex example that uses a model, dependency injection, include a template without a model (the first template) and include a template that takes a model (the second template).

You can find the project we are going over in detail at:

veccsolutions/RenderRazorConsole
Companion code to https://www.frakkingsweet.com/razor-template-rendering/ - veccsolutions/RenderRazorConsole

Implementation

First add the required NuGet references for Razor.

Microsoft.AspNetCore.Hosting
Microsoft.AspNetCore.Mvc.Razor

We will also need to bring in the following to setup the dependency injection and logging.

Microsoft.Extensions.DependencyInjection
Microsoft.Extensions.Logging
Microsoft.Extensions.Logging.Console

Now that we've brought in the 5 packages needed for using Razor, we can build our ServiceProvider. I build class for that single purpose to keep the example clean:

using System;
using System.Diagnostics;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ObjectPool;
using RenderRazorConsole;

namespace Microsoft.Extensions.DependencyInjection
{
    public static class ServiceProviderBuilder
    {
        public static IServiceProvider BuildServiceProvider()
        {
            var services = new ServiceCollection();

            services.AddLogging((builder) =>
            {
                builder.AddConsole(options =>
                {
                    options.IncludeScopes = true;
                });
                builder.SetMinimumLevel(LogLevel.Trace);
            });

            services.AddRazor();

            services.AddSingleton<RazorRunner>();
            services.AddSingleton<TestInjection>();

            var result = services.BuildServiceProvider();
            return result;
        }

        public static IServiceCollection AddRazor(this IServiceCollection services)
        {
            var hostingEnvironment = new HostingEnvironment
            {
                ApplicationName = Assembly.GetEntryAssembly()?.GetName().Name
            };

            services.AddSingleton<IHostingEnvironment>(hostingEnvironment);
            services.AddSingleton<DiagnosticSource>((IServiceProvider serviceProvider) => new DiagnosticListener("DummySource"));
            services.AddTransient<ObjectPoolProvider, DefaultObjectPoolProvider>();

            services.AddMvcCore()
                    .AddRazorViewEngine(options =>
                    {
                        options.AllowRecompilingViewsOnFileChange = false;
                        options.FileProviders.Add(new VirtualFileProvider());
                    });

            return services;
        }
    }
}

A few things in here:

  • The HostingEnvironment.ApplicationName is set to the name of the EntryAssembly. The ApplicationName may not need to be the name of the assembly but does at least need to be set. Going through the framework code, this is what it gets set to by default so it's what I used. I found that if it is not set, you will run into a bunch of missing type compiler errors when it tries to build the view. Those type errors including those that are in the base CLR. The reason being, when the engine gets registered by calling AddMvcCore it checks the application name when registering the underlying ApplicationParts, if it is set it will bring in the assemblies that your application references, otherwise it won't.
  • I put this class in the Microsoft.Extensions.DependencyInjection namespace. This is a habit of mine that I got into, so I can keep all of my using statements clean and simple. Most classes that I create that build a ServiceCollection or ServiceProvider is in that namespace.
  • The DiagnosticSource and ObjectPoolProvider usually get registered in an extension method that is created by the ASP.Net Core web host builder and AddMvc extension method, so we needed to register them here since we are not using those 2 things parts of the underlying framework.

Now we need to allow the application to see what libraries are referenced during run time. In your .csproj file, add the following to the first PropertyGroup element.

<PreserveCompilationContext>true</PreserveCompilationContext>

Without it, the referenced library list is empty. With the list being empty the engine doesn't know what libraries to reference when compiling the views at run time.

Now we need to create the classes that are referenced in the ServiceProviderBuilder we just created. An explanation of those classes:

  • RazorRunner, a helper class containing the code that will be reused when rendering a view
  • TestInjection, a simple class that proves that injection works in the views
  • VirtualFileProvider, an implementation of IFileProvider that returns requested view content, this allows us to pull data from outside sources.

We'll start working on our VirtualFileProvider class. We need to build 2 supporting classes to implement that interface. Those are:

  • VirtualDirectoryContents, a simple implementation of the IDirectoryInfo. It returns the list of files in our fake directory, this could be a list from the database, or the actual file system, or whatever. In this case, it's a fake in memory store.
  • VirtualFileInfo, this class is an implementation of the IFileInfo interface.

For the VirtualDirectoryContents here's what I'm using:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.FileProviders;

namespace RenderRazorConsole
{
    public class VirtualDirectoryContents : IDirectoryContents
    {
        public bool Exists => true;

        public IEnumerator<IFileInfo> GetEnumerator()
        {
            yield return TestFile.Value;
            yield return ModelFile.Value;
            yield return InjectionFile.Value;
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public static Lazy<IFileInfo> TestFile { get; } =
            new Lazy<IFileInfo>(() => new VirtualFileInfo("custom:\\testapp\\test.cshtml",
                                                          "test.cshtml",
                                                          DateTimeOffset.Now,
                                                          false,
                                                          (info) => Encoding.Default.GetBytes("@(System.DateTime.Now)")));

        public static Lazy<IFileInfo> ModelFile { get; } =
            new Lazy<IFileInfo>(() => new VirtualFileInfo("custom:\\testapp\\model.cshtml",
                                                          "model.cshtml",
                                                          DateTimeOffset.Now,
                                                          false,
                                                          (info) => Encoding.Default.GetBytes(@"@model RenderRazorConsole.TestModel
@foreach (var item in Model.Values)
{
<TEXT>@item
</TEXT>
}
")));

        public static Lazy<IFileInfo> InjectionFile { get; } =
            new Lazy<IFileInfo>(() => new VirtualFileInfo("custom:\\testapp\\injection.cshtml",
                                                          "injection.cshtml",
                                                          DateTimeOffset.Now,
                                                          false,
                                                          (info) => Encoding.Default.GetBytes(@"@using RenderRazorConsole
@model TestModel
@inject TestInjection _testInjection;

Foreach:
@foreach (var item in Model.Values)
{
<TEXT>@item
</TEXT>
}

Injected:
@(_testInjection.Value)

Partial:
@Html.Partial(""test.cshtml"")

Partial With Model
@Html.Partial(""model.cshtml"", Model)
")));
    }
}

It's a simple class. The biggest part of this class is the actual template file contents for proving that everything works, TestFile, ModelFile, InjectionFile.

For the VirtualFileInfo we'll use this class:

using System;
using System.IO;
using Microsoft.Extensions.FileProviders;

namespace RenderRazorConsole
{
    public class VirtualFileInfo : IFileInfo
    {
        public Lazy<byte[]> Contents { get; }

        public bool Exists => Contents.Value != null;

        public long Length => Contents.Value.Length;

        public string PhysicalPath { get; }

        public string Name { get; }

        public DateTimeOffset LastModified { get; }

        public bool IsDirectory { get; }

        public Stream CreateReadStream() => new MemoryStream(Contents.Value);

        public VirtualFileInfo(string physicalPath, string name, DateTimeOffset lastModified, bool isDirectory, Func<IFileInfo, byte[]> getContents)
        {
            Contents = new Lazy<byte[]>(() => getContents(this));
            PhysicalPath = physicalPath;
            Name = name;
            LastModified = lastModified;
            IsDirectory = isDirectory;
        }
    }
}

In this class, the CreateReadStream method and Contents property is probably the most interesting, they create a new MemoryStream from the Contents property. This property is not part of the IFileInfo interface. We are using a Lazy<byte[]> type so we only allocate the byte array when it's actually used. The returned stream from CreateReadStream gets disposed of in the underlying framework so be sure not to dispose or close it before returning.

In the VirtualFileProvider we will determine which VirtualFileInfo to return.

using System;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;

namespace RenderRazorConsole
{
    public class VirtualFileProvider : IFileProvider
    {
        public IDirectoryContents GetDirectoryContents(string subpath) =>
            new VirtualDirectoryContents();

        public IFileInfo GetFileInfo(string subpath)
        {
            switch (subpath.ToLower())
            {

                case "/custom:/testapp/test.cshtml":
                    return VirtualDirectoryContents.TestFile.Value;
                case "/custom:/testapp/model.cshtml":
                    return VirtualDirectoryContents.ModelFile.Value;
                case "/custom:/testapp/injected.cshtml":
                    return VirtualDirectoryContents.InjectionFile.Value;
                default:
                    return new NotFoundFileInfo(subpath);
            }
        }

        public IChangeToken Watch(string filter)
        {
            throw new NotImplementedException();
        }
    }
}

The biggest gotcha here is in the switch statement. Notice how the paths start with a / and the / in the path names. The underlying framework normalizes the path to include the leading / and uses / instead of \. Also, since I'm not going support watching the files, we will throw a NotImplementedException in the Watch method to make sure it doesn't get called. This feature is turned off in our ServiceProviderBuilder in the options when we call the AddRazorViewEngine extension method.

Now for the last class, the one that calls the Razor engine. The RazorRunner.

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;

namespace RenderRazorConsole
{
    public class RazorRunner
    {
        private readonly IRazorViewEngine _razorViewEngine;
        private readonly IServiceProvider _serviceProvider;

        public RazorRunner(IRazorViewEngine razorViewEngine, IServiceProvider serviceProvider)
        {
            this._razorViewEngine = razorViewEngine;
            this._serviceProvider = serviceProvider;
        }

        public async Task<string> Render(string viewPath, object model = null)
        {
            var httpContext = new DefaultHttpContext() { RequestServices = _serviceProvider };

            var routeData = new RouteData();
            var actionDescriptor = new ActionDescriptor();
            var modelStateDictionary = new ModelStateDictionary();
            var modelMetadataProvider = new EmptyModelMetadataProvider();
            var tempDataProvider = new VirtualTempDataProvider();
            var htmlHelperOptions = new HtmlHelperOptions();

            var actionContext = new ActionContext(httpContext, routeData, actionDescriptor, modelStateDictionary);
            var viewDataDictionary = new ViewDataDictionary(modelMetadataProvider, modelStateDictionary);
            var tempDataDictionary = new TempDataDictionary(httpContext, tempDataProvider);

            viewDataDictionary.Model = model;

            using (var stringWriter = new StringWriter())
            {
                var view = _razorViewEngine.GetView(string.Empty, viewPath, true);
                var viewContext = new ViewContext(actionContext, view.View, viewDataDictionary, tempDataDictionary, stringWriter, htmlHelperOptions);

                await view.View.RenderAsync(viewContext);

                var result = stringWriter.ToString();
                return result;
            }
        }
    }
}

This class has 1 method. It builds the ViewContext which is passed into the RenderAsync which is the method the Razor engine exposes to render the view. If you want to support additional features in the view you can set the properties on those objects that are created.

The VirtualTempDataProvider class is below. In a web application it is used to store data between page requests, since we don't have page requests, we don't do anything but return a new Dictionary to make sure we don't get any unexpected null reference exceptions.

using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ViewFeatures;

namespace RenderRazorConsole
{
    public class VirtualTempDataProvider : ITempDataProvider
    {
        public IDictionary<string, object> LoadTempData(HttpContext context) => new Dictionary<string, object>();

        public void SaveTempData(HttpContext context, IDictionary<string, object> values)
        {
        }
    }
}

The classes for my example views, TestModel and TestInjection are below, they are simple, small classes for demo purposes.

namespace RenderRazorConsole
{
    public class TestModel
    {
        public string[] Values { get; set; }
    }
}
namespace RenderRazorConsole
{
    public class TestInjection
    {
        public string Value { get; } = "TestInjection Value";
    }
}

My Program class that ties it all together:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection;

namespace RenderRazorConsole
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var sp = ServiceProviderBuilder.BuildServiceProvider();
            var viewEngine = sp.GetRequiredService<IRazorViewEngine>();

            try
            {
                var razorRunner = sp.GetRequiredService<RazorRunner>();
                while (true)
                {
                    var rendered = await razorRunner.Render("custom:\\testapp\\test.cshtml");
                    Console.WriteLine(rendered);

                    rendered = await razorRunner.Render("custom:\\testapp\\model.cshtml", new TestModel { Values = new[] { "test", "model", "array", "stuff" } });
                    Console.WriteLine(rendered);

                    rendered = await razorRunner.Render("custom:\\testapp\\injected.cshtml", new TestModel { Values = new[] { "test-injected", "model-injected", "array-injected", "stuff-injected" } });
                    Console.WriteLine(rendered);

                    Console.ReadLine();
                }
             }
            catch (Exception exception)
            {
                Console.WriteLine("ERROR: " + exception);
            }
            Console.ReadLine();
        }
    }
}

If you notice, the Main method is an async method. There will be a small post on this later. Until then, you need to set your project to build with the latest and greatest compiler. Right click your project, go to Properties. Then open the Build section on the left. Scroll down in the setting pane and click Advanced.... In the Language Version drop down, select C# latest minor version (latest). Click OK.

Conclusion

And that's that, in theory you should be able to run and render those 3 test templates that we created in our VirtualDirectoryContents.

Some parts of the Razor engine was easy to figure out, others took a bit of dumpster diving through the ASP.NET Core code. Using the built-in search in GitHub was frustrating at best. Cloning the repository and opening it up in Visual Studio was infinitely easier and faster.

Links

veccsolutions/RenderRazorConsole
Companion code to https://www.frakkingsweet.com/razor-template-rendering/ - veccsolutions/RenderRazorConsole
dotnet/aspnetcore
ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux. - dotnet/aspnetcore