ASP.NET MVC Virtual Path Provider

DEVELOPMENT ON 10 April 2011

I’ve read several articles showing how to implement a custom VirtualPathProvider to load ASP.NET MVC views direct from SQL database storage. This type of implementation really extends the CMS possibilities of ASP.NET MVC and reduces the barrier of entry for designers and UX developers to be able to edit and develop website content using Microsoft’s Razor View Engine from within any web browser.

Like all the articles both you and I have probably already seen, this example also demonstrates how to implement a custom VirtualPathProvider but it additionally demonstrates how to handle the specific issue of updating a “View” whilst ensuring the VirtualPathProvider only reloads the view once it has been updated and not on every single request.

VirtualPathProvider Override Methods

The VirtualPathProvider has two override methods that can be used to handle requests. The first method I looked at was the “GetCacheDependency” method which is poorly documented and seems to be over complicated when all I wanted was a request to reload once it had been updated.

The second override method is “GetFileHash” which returns the time stamp of the requested file so that the VirtualPathProvider reloads the request once the files time stamp value changes. The problem I had with this is how to handle the time stamp value for every request to my VirtualPathProvider. In this article we demonstrate a simple technique that uses a GUID value as both the file hash and also the ID of the requested view.

The Database Schema

Here you can see the database table has an “ID” field and a “GUID” field. We use the ID field throughout the application to handle all the CRUD methods and the GUID value to handle VirtualPathProvider requests using the GetFileHash method.

Updating the View

The following method demonstrates how we assign a new GUID to the GUID field every time the view has been updated.

namespace iO.Controllers
{
    [HandleError]
    public class AdminController : Controller
    {
        [HttpPost, ValidateInput(false)]
        public ActionResult EditView(Template template)
        {
            if (ModelState.IsValid)
            {
                  //assign a new GUID every time the view is updated
                  template.Guid = Guid.NewGuid();
                  yourDataRepository.UpdateTemplate(template);
            }
            return View();
        }
    }
}

The View Controller

The View Controller method shows how we get the view by the ID field and return it’s GUID value as a string to the view result. You could use a friendly UrlKey/Permalink to get the view if required.


namespace iO.Controllers
{
    [HandleError]
    public class ViewController : Controller
    {
        public ActionResult GetView(int ID)
        {
            myModel model = new myModel();
            model.Template = yourDataRepository.GetTemplateByID(ID);
            ViewData.Model = model;
            return View(model.Template.Guid.ToString());
        }
    }
}

The VirtualPathProvider

The VirtualPathProvider shows how we use the virtualPath value to check if the request has been made by the “ViewController” controller and returns the virtualPath in the GetFileHash method. Remember that the virtualPath uses the requested views GUID value which only changes once the view is Updated using the “EditView” method

namespace iO.Mvc.Providers
{
    public class MvcVirtualPathProvider : VirtualPathProvider
    {
        // virtualPath example >>  "~/Views/View/21EC2020-3AEA-1069-A2DD-08002B30309D.cshtml"
        private bool IsPathVirtual(string virtualPath)
        {
            string path = VirtualPathUtility.ToAppRelative(virtualPath);

            // returns true if the path is requested by the "ViewController" controller
            if (path.StartsWith("~/Views/View", StringComparison.InvariantCultureIgnoreCase)
            {
                return true;
            }
            return false;
        }

        public override bool FileExists(string virtualPath)
        {
            if (IsPathVirtual(virtualPath))
            {
                SimpleVirtualFile file = (SimpleVirtualFile)GetFile(virtualPath);
                if (file.Exists)
                {
                    return true;
                }
                return false;
            }
            return Previous.FileExists(virtualPath);
        }

        public override VirtualFile GetFile(string virtualPath)
        {
            if (IsPathVirtual(virtualPath))
            {
                return new SimpleVirtualFile(virtualPath);
            }
            return Previous.GetFile(virtualPath);
        }

        // Simply return the virtualPath on every request.
        public override string GetFileHash(string virtualPath, System.Collections.IEnumerable virtualPathDependencies)
        {
            if (IsPathVirtual(virtualPath))
            {
                    // Returns the virtual path value which is made up of the views GUID value that only
                    // changes once the view has been updated - essentially working like an updated FileHash
                    // but also the ID of the requested view
                    return virtualPath;
            }
            return base.GetFileHash(virtualPath, virtualPathDependencies);
        }

        private class SimpleVirtualFile : VirtualFile
        {
            private IDataRepository yourDataRepository;
            private string content;

            public bool Exists
            {
                get { return (content != null); }
            }

            public SimpleVirtualFile(string virtualPath)
                : base(virtualPath)
            {
                yourDataRepository = new DataRepository();
                GetContent();
            }

            public override Stream Open()
            {
                ASCIIEncoding encoding = new ASCIIEncoding();
                return new MemoryStream(encoding.GetBytes(this.content), false);
            }

            protected void GetContent()
            {
                if (IsPathVirtual(virtualPath))
                {
                        // your GetTemplateByPath method would need to split the virtualPath string to retrieve
                       // the GUID value and request the View from the SQL database using the GUID value instead
                       // of the ID Primary Key field.
                        Template template = yourDataRepository.GetTemplateByPath(VirtualPath);
                        if (template != null)
                        {
                            this.content = "@model dynamic \r\n" + template.Content.ToString();
                        }
                }
            }
        }

        // just implement the default override and return null if the path is a Virtual Path
        public override CacheDependency GetCacheDependency(string virtualPath,
            System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart)
        {
            if (IsPathVirtual(virtualPath))
            {
                return null;
            }
            return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
        }
    }
}

This simple technique works really well and makes your VirtualPathProvider operate in the same way as requesting a physical file from disk in addition to removing the need to implement CacheDependencies or SQLCacheDependencies.

Commenting on our Blog is closed at the moment but feel free to use our contact form if you have any questions, comments or feedback.

This website is built using iO Engine CMS. iO Engine will launch as an open source community project later this year.

Visit iO Engine

This website is hosted on AIR BASE
using an AIR CORE DEDICATED SERVER. AIRBASE is due to launch later this year.

Visit Airbase