How to load Views from Assembly in MVC

There can be situations when we require our pages to be loaded from database or assembly, instead of file system. One would want to hide the implementation and place all the resources in an assembly. In this post, I'll show you how to do it in a very simplified way.

Step 1

Register your routing rule like this.

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        name: "Virtual",
        url: "{*viewName}",
        defaults: new { controller = "Frontend", action = "Default" },
        constraints: new { controller = "Frontend", action = "Default" }
    );
}

I have named my Controller and Action, Frontend and Default respectively. Also, defined a constraint that no other controller and / or action will be processed. One more thing you must have noticed is {*viewName}. This means that all URLs will invoke our Default action.

Step 2

I created 4 classes to do some magic, which are as follows.

ViewFile.cs

public class ViewFile : VirtualFile
{
    private string path;

    public ViewFile(string virtualPath) : base(virtualPath)
    {
        path = virtualPath;
    }

    public override Stream Open()
    {
        if (string.IsNullOrEmpty(path))
            return new MemoryStream();

        string content = Falafel.Providers.Pages.GetByVirtualPath(path);
        if (string.IsNullOrEmpty(content))
            return new MemoryStream();

        return new MemoryStream(ASCIIEncoding.UTF8.GetBytes(content));
    }
}

ViewPathProvider.cs

public class ViewPathProvider : VirtualPathProvider
{
    public override bool FileExists(string virtualPath)
    {
        return Pages.IsExistByVirtualPath(virtualPath) || base.FileExists(virtualPath);
    }

    public override VirtualFile GetFile(string virtualPath)
    {
        if (Pages.IsExistByVirtualPath(virtualPath))
        {
            return new ViewFile(virtualPath);
        }

        return base.GetFile(virtualPath);
    }

    public override CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart)
    {
        if (Pages.IsExistByVirtualPath(virtualPath))
            return ViewCacheDependencyManager.Instance.Get(virtualPath);
            
        return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
    }
}

ViewCacheDependency.cs

public class ViewCacheDependency : CacheDependency
{
    public ViewCacheDependency(string virtualPath)
    {
        base.SetUtcLastModified(DateTime.UtcNow);
    }

    public void Invalidate()
    {
        base.NotifyDependencyChanged(this, EventArgs.Empty);
    }
}

ViewCacheDependencyManager.cs

public class ViewCacheDependencyManager
{
    private static Dictionary<string, ViewCacheDependency> dependencies = new Dictionary<string, ViewCacheDependency>();
    private static volatile ViewCacheDependencyManager instance;
    private static object syncRoot = new Object();

    private ViewCacheDependencyManager()
    {
    }

    public static ViewCacheDependencyManager Instance
    {
        get
        {
            if (instance == null)
            {
                lock (syncRoot)
                {
                    if (instance == null)
                    {
                        instance = new ViewCacheDependencyManager();
                    }
                }
            }

            return instance;
        }
    }

    public CacheDependency Get(string virtualPath)
    {
        if (!dependencies.ContainsKey(virtualPath))
            dependencies.Add(virtualPath, new ViewCacheDependency(virtualPath));

        return dependencies[virtualPath];
    }

    public void Invalidate(string virtualPath)
    {
        if (dependencies.ContainsKey(virtualPath))
        {
            var dependency = dependencies[virtualPath];
            dependency.Invalidate();
            dependency.Dispose();
            dependencies.Remove(virtualPath);
        }
    }
}

Step 3

Register the ViewPathProvider class in Global.asax.

HostingEnvironment.RegisterVirtualPathProvider(new ViewPathProvider());

This will tell MVC engine to call this registered class for resources. And of course, this class should implement some basic requirements. MVC engine first calls FileExists method to check if the resource exist in file system - no, it's assembly in our case. If the method returns true, it calls GetFile method to get the content as VirtualFile, which we have inherited in ViewFile class.

Step 4

Now create an helper class which will two methods as shown below.

public class Pages
{
    public static bool IsExistByVirtualPath(string virtualPath)
    {
        if (virtualPath.StartsWith("~/"))
            virtualPath = virtualPath.Substring(1);

        var assembly = Assembly.LoadFrom(HttpContext.Current.Server.MapPath("~/bin") + "\\Falafel.Resources.dll");
        string result = string.Empty;
        virtualPath = "Falafel.Resources" + virtualPath.Replace('/', '.');

        if (virtualPath.EndsWith("/"))
        {
            result = assembly.GetManifestResourceNames().First();
        }
        else
        {
            result = assembly.GetManifestResourceNames().FirstOrDefault(i => i.ToLower() == virtualPath.ToLower());
        }

        return string.IsNullOrEmpty(result) ? false : true;
    }

    public static string GetByVirtualPath(string virtualPath)
    {
        if (virtualPath.StartsWith("~/"))
            virtualPath = virtualPath.Substring(1);

        var assembly = Assembly.LoadFrom(HttpContext.Current.Server.MapPath("~/bin") + "\\Falafel.Resources.dll");
        virtualPath = "Falafel.Resources" + virtualPath.Replace('/', '.');
        virtualPath = assembly.GetManifestResourceNames().FirstOrDefault(i => i.ToLower() == virtualPath.ToLower());

        using (Stream stream = assembly.GetManifestResourceStream(virtualPath))
        {
            using (StreamReader reader = new StreamReader(stream))
            {
                string result = reader.ReadToEnd();
                return result;
            }
        }
    }
}

Finally, you would need a controller, who's one and only one action will always be called.

public class FrontendController : Controller
{
    public ActionResult Default()
    {
        string actionName = (RouteData.Values["viewName"] ?? "Default").ToString();
        return View(actionName);
    }
}

And guess what, you can download sample project from the link below. Happy coding!

Sample Project

comments powered by Disqus

Get weekly updates in your inbox!