Thursday, July 18, 2013

Reuse MVC Views Using a Virtual Path Provider

I recently began a project where I wanted to share views across two MVC projects.  Specifically, I have a public-facing MVC app and an intranet MVC app that have some common features.  I wanted to share Editor Templates, Display Templates, and some other partial views and content for re-use across both apps.

I did a fair amount of digging, and there are several ways to do this.  However, each had some drawbacks that I wasn’t happy with.  For example, several examples required you to use a special syntax in the view path names, meaning they couldn’t easily be used for Editor Templates.  In the end, my solution was to write my own Virtual Path Provider to serve up .cshtml files from an embedded resource in a shared library.  If you’re not familiar with VirtualPathProvider, this is a mechanism that has been around in ASP.NET for a while that lets you hook in at a fairly low level to serve up files from sources other than disk.  For example, you might use one to serve files from a database (gag!) or cloud storage medium.  Since the provider hooks in at such a low level in the pipeline, most parts of ASP.NET won’t “know” that the file is not just another file in the file system.

In this case, I wanted to serve up .cshtml files that are embedded in a .dll that gets shared between two ASP.NET MVC Apps.  To accomplish this, I created an EmbeddedVirtualPathProvider:

public class EmbeddedVirtualPathProvider : VirtualPathProvider
{
    private readonly Assembly assembly = typeof(EmbeddedVirtualPathProvider).Assembly;
    private readonly string[] resourceNames;
 
    public EmbeddedVirtualPathProvider()
    {
        this.resourceNames = assembly.GetManifestResourceNames();
    }
 
    private bool IsEmbeddedResourcePath(string virtualPath)
    {
        var checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
        var resourceName = this.GetType().Namespace + "." + checkPath.Replace("~/", "").Replace("/", ".");
        return this.resourceNames.Contains(resourceName);
    }
 
    public override bool FileExists(string virtualPath)
    {
        return IsEmbeddedResourcePath(virtualPath) || base.FileExists(virtualPath);
    }
 
    public override VirtualFile GetFile(string virtualPath)
    {
        if (IsEmbeddedResourcePath(virtualPath))
        {
            return new EmbeddedVirtualFile(virtualPath);
        }
        return base.GetFile(virtualPath);
    }
 
    public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
    {
        if (IsEmbeddedResourcePath(virtualPath))
        {
            return null;
        }
        return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
    }
}
}

and VirtualFile:

    public class EmbeddedVirtualFile : VirtualFile
    {
        private readonly string virtualPath;
        private readonly Assembly assembly;

        public EmbeddedVirtualFile(string virtualPath)
            : base(virtualPath)
        {
            this.assembly = this.GetType().Assembly;
            this.virtualPath = VirtualPathUtility.ToAppRelative(virtualPath);
        }

        public override System.IO.Stream Open()
        {
            var resourceName = this.GetType().Namespace + "." + virtualPath.Replace("~/", "").Replace("/", ".");
            return assembly.GetManifestResourceStream(resourceName);

        }
    }

Placed in the root of a shared library, these will serve up any embedded resource in the library just as though the resource were in the same path of the ASP.NET app.

To use, simply add this to the start of the ASP.NET app’s global.asax Application_Start:

HostingEnvironment.RegisterVirtualPathProvider(new EmbeddedVirtualPathProvider());

and then create folders and views in the class library, setting the view files to ‘Embedded Resource’ (rt click file –> Build Action –> Embedded Resource).  For the most part, views should “just work”.  In some cases, though, it is important to specify namespaces explicitly. For views with typed models, it is important to specify @model as the _first_ directive in the view, otherwise compilation fails.

It’s probably safe to say that there is some performance overhead with this approach, and that it could probably be optimized a bit to cache the files, etc.  Enjoy!