embedded razor views in mvc 4
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.