embedded razor views in mvc 4

11 June, 2013

Here’s a little fun you can have with very little scaffolding that let’s you ship ASP.NET MVC 4 razor views as embedded resources in a shared assembly and allows you to override them as needed.

The technique is to create a VirtualPathProvider that supports translating virtual paths to embedded resources.

For the code below, we’re assuming the FunWithMvc assembly has .cshtml files under a folder called /views in an appropriate subfolder for the controller or for shared views. Each of these files in the view folders must be marked as embedded resources.

To override the embedded resource you merely have to create the replacement .cshtml file in a corresponding website /views folder.

using System;
using System.Collections;
using System.IO;
using System.Linq;
using System.Web.Caching;
using System.Web.Hosting;
using System.Web.Mvc;

namespace FunWithMvc
{
    public class EmbeddedVirtualPathProvider : VirtualPathProvider
    {
        // Nested class representing the "virtual file"
        public class EmbeddedVirtualFile : VirtualFile
        {
            private Stream _stream;

            public EmbeddedVirtualFile(string virtualPath,
                Stream stream) : base(virtualPath)
            {
                if (null == stream)
                    throw new ArgumentNullException("stream");

                _stream = stream;
            }

            public override Stream Open()
            {
                return _stream;
            }
        }

        public EmbeddedVirtualPathProvider()
        {
        }

        public override CacheDependency GetCacheDependency(
            string virtualPath,
            IEnumerable virtualPathDependencies,
            DateTime utcStart)
        {
            string embedded = _GetEmbeddedPath(virtualPath);

            // not embedded? fall back
            if (string.IsNullOrEmpty(embedded))
                return base.GetCacheDependency(virtualPath,
                    virtualPathDependencies, utcStart);

            // there is no cache dependency for embedded resources
            return null;
        }

        public override bool FileExists(string virtualPath)
        {
            string embedded = _GetEmbeddedPath(virtualPath);

            // You can override the embed by placing a real file
            // at the virtual path...
            return base.FileExists(virtualPath)
                || !string.IsNullOrEmpty(embedded);
        }

        public override VirtualFile GetFile(string virtualPath)
        {
            // You can override the embed by placing a real file
            // at the virtual path...
            if (base.FileExists(virtualPath))
                return base.GetFile(virtualPath);

            string embedded = _GetEmbeddedPath(virtualPath);

            // sanity...
            if (string.IsNullOrEmpty(embedded))
                return null;
            
            return new EmbeddedVirtualFile(virtualPath,
                GetType().Assembly
                .GetManifestResourceStream(embedded));
        }

        private string _GetEmbeddedPath(string path)
        {
            // ~/views/sample/x.cshtml
            // => /views/sample/x.cshtml
            // => FunWithMvc.views.sample.x.cshtml

            if (path.StartsWith("~/"))
                path = path.Substring(1);

            path = path.ToLowerInvariant();
            path = "FunWithMvc" + path.Replace('/', '.');

            // this makes sure the "virtual path" exists as an
            // embedded resource
            return GetType().Assembly.GetManifestResourceNames()
                .Where(o => o == path).FirstOrDefault();
        }
    }
}

You would then install the EmbeddedVirtualPathProvider in your application start up code before anything else:

protected void Application_Start()
{
    HostingEnvironment.RegisterVirtualPathProvider(
        new EmbeddedVirtualPathProvider());

    AreaRegistration.RegisterAllAreas();
    // ...
}

This same technique can be used to pull razor views from a database or other sources and I leave that as well as mitigating filesystem i/o pressure under high load as an exercise for the reader.