Using RazorGenerator to share MVC views across assemblies

Razor Templates uses dynamic compilation in much the same way as Web Forms, this is good for development to prevent the need for re-compilation on every template change.

It can also be beneficial to have the ability to make small UI changes without a full re-deployment in a production environment. As sites get larger and more complex this becomes problematic because the dynamic compilation can make the site unresponsive either after changes are made or if a user is unlucky enough to be the first to hit a page after an app pool re-cycle.

Sharing common views across assemblies is often a requirement and in some scenarios it is also nice to be able to override common views for project specific ones.

It is possible to set the publisher to pre-compile the site but this can also cause issues. A better solution is to use a custom tool like RazorGenerator to compile the templates prior to deployment, this has several benefits:

  • Fewer physical files need to be copied during deployment.
  • Faster application start up no dynamic compilation required.
  • Faster initial response times on pages.
  • Compiled views can be shared and re-used in other projects.
  • Razor view compilation errors can be caught before run time. 

To use the RazorGenerator first install the nuget package:

PM> Install-Package RazorGenerator.Mvc

This will add the custom tool and a new file to the App_Start folder called RazorGeneratorMvcStart.cs that looks like this

public static void Start() 
{
    var engine = new PrecompiledMvcEngine(typeof(RazorGeneratorMvcStart).Assembly) 
    { 
       UsePhysicalViewsIfNewer = HttpContext.Current.Request.IsLocal
    };

    ViewEngines.Engines.Insert(0, engine);

    VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
}

This file just inserts a custom View Engine that will look for precompiled Razor files before the default View Engines.

By default not all Razor files will be pre-compiled so they need to be set to use the custom tool that was just installed. This can be done by going to properties for the view, then in Custom Tool put RazorGenerator. Right clicking on the razor file and clicking run custom tool will compile the view and add the resulting class file under the view. You can select multiple views at once when doing this so at least it only needs to be done once per folder.

To use views from different assemblies you actually need to modify the view engine registration to add a new PrecompiledMvcEngine per assembly. This is required to avoid naming conflicts between namespaces and also more importantly it allows overrides in consuming assemblies.

For example there may be a number of common partial views in a shared library which are compiled into a referenced dll. The consuming project wants to override the view for one or more of these components. By registering multiple view engines in order of precedence the consuming library can implement its own version of the view like so:

ViewEngines.Engines.Clear();

var viewLocationFormats = new[] {
    "~/Views/{1}/{0}.cshtml"
};

var partialViewLocationFormats = new[] {
    "~/Views/Shared/{0}.cshtml",
    "~/Views/Partials/{0}.cshtml"
};

// Add the current project compiled view engine
var engine = new PrecompiledMvcEngine(typeof(RazorGeneratorMvcStart).Assembly);

engine.ViewLocationFormats = viewLocationFormats;
engine.PartialViewLocationFormats = partialViewLocationFormats;

ViewEngines.Engines.Add(engine);

// Add the common library compiled view engine
var commonViewsEngine = new PrecompiledMvcEngine(typeof(myNamespace.Common).Assembly);

commonViewsEngine.ViewLocationFormats = viewLocationFormats;
commonViewsEngine.PartialViewLocationFormats = partialViewLocationFormats;

ViewEngines.Engines.Add(commonViewsEngine);

VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);

The key point is that when the view engine is instantiated the assembly reference is passed in so it knows where to look.

You do not even need to keep the same structure in all cases since you can control the location formats when creating the view engines.

Another benefit of this is that you can mix non-compiled views by adding the standard RazorViewEngine last. In this example all the default View Engines have been removed to keep things efficient so the default razor view engine needs to be added back in last.

var razorViewEngine = new RazorViewEngine();
ViewEngines.Engines.Add(razorViewEngine);

Now if the site is needs to find a partial view for a component it will first look in the current project namespace for a compiled view, if nothing is found, the compiled common views namespace is checked, finally the current project physical non-compiled files will be checked as final fall back.