Loading ASP.NET Core MVC Views From A Database Or Other Location

For the vast majority of ASP.NET Core MVC applications, the conventional method of locating and loading views from the file system does the job nicely. But you can also load views from other sources including a database. This is useful in scenarios where you want to give users the option to craft or modify Razor files but don't want to give them access to the file system. This article looks at the work required to create a component to obtain views from other sources, using the database as a working example.

The Razor View Engine uses components called FileProviders to obtain the content of views. The view engine will iterate its collection of locations that it searches for views (ViewLocationFormats) and then present those locations to each of the registered FileProviders in turn until one returns the view content. At startup, a PhysicalFileProvider is registered with the view engine, which is designed to look for physical .cshtml files in the various locations, starting with the customary Views folder found in every MVC project template. An EmbeddedFileProvider is available for obtaining view content from embedded resources. If you want to store views in another location, such as a database, you can create your own FileProvider and register it with the view engine.

FileProviders must implement the IFileProvider interface. The IFileProvider interface specifies the following members:

IDirectoryContents GetDirectoryContents(string subpath);
IFileInfo GetFileInfo(string subpath);
IChangeToken Watch(string filter);

The most important of these is the GetFileInfo method which returns an object that implements the IFileInfo interface representing a file implementation. The Watch method returns an implementation of the IChangeToken interface. When the view engine first finds a view, it has to compile it. It caches the compiled view so that it doesn't have to be compiled again for subsequent requests. The view engine needs some way in which it can be notified that changes have taken place to the original view so that the cache can be refreshed with the latest version. The IChangeToken instance provides that notification. So, in order to get views from a database, we need an implementation of IFileProvider, an implementation of IFileInfo, and and implementation of IChangeToken.

Database Schema

The minimum schema for the database table required for storing views is illustrated below together with the DDL for creating the table

Views from database

CREATE TABLE [dbo].[Views](
    [Location] [nvarchar](150) NOT NULL,
    [Content] [nvarchar](max) NOT NULL,
    [LastModified] [datetime] NOT NULL,
    [LastRequested] [datetime]
) 

The Location field contains a unique identifier for the view. The view engine looks for views using subpaths, so it makes sense to use them to identify the individual view. So the Location value for the home page will be one of the paths that the view engine expects to find the view for the Index method of the Home controller e.g./views/home/index.cshtml. The Content field contains the Razor and HTML from the view file. The LastModified field defaults to GetUtcDate when the view is created, and is updated whenever the view content is modified. The LastRequested field is updated with the current UTC date and time whenever the view engine successfully retrieves the content. These two fields are used to calculate whether any modifications have taken place since the file was last retrieved, compiled and cached. You would set the default value for LastModified to GetDate(), and then reset the value whenever you edit the file as part of the CRUD procedure.

IFileProvider

I have named my implementation DatabaseFileProvider. It has a constructor taking a string that represents the connection string for a database. I haven't provided an implementation for the GetDirectoryContents method as one is not needed for this use-case. The GetFileInfo method returns my custom IFileInfo if a result matching the specified path is found, or a NotFoundFileInfo object, which tells the view engine to try another provider, or another view location. The Watch method returns my custom IChangeToken object.

IFileInfo

The IFileInfo interface features the following members:

public interface IFileInfo
{
    //
    // Summary:
    //     True if resource exists in the underlying storage system.
    bool Exists { get; }
    //
    // Summary:
    //     True for the case TryGetDirectoryContents has enumerated a sub-directory
    bool IsDirectory { get; }
    //
    // Summary:
    //     When the file was last modified
    DateTimeOffset LastModified { get; }
    //
    // Summary:
    //     The length of the file in bytes, or -1 for a directory or non-existing files.
    long Length { get; }
    //
    // Summary:
    //     The name of the file or directory, not including any path.
    string Name { get; }
    //
    // Summary:
    //     The path to the file, including the file name. Return null if the file is not
    //     directly accessible.
    string PhysicalPath { get; }

    //
    // Summary:
    //     Return file contents as readonly stream. Caller should dispose stream when complete.
    //
    // Returns:
    //     The file stream
    Stream CreateReadStream();
}

I have left the comments from the source code in as they explain the purpose of each member quite nicely. The important ones are the Name, Exists, Length properties and the CreateReadStream method. Here is the DatabaseFileInfoclass, which is the custom implementation of IFileInfofor getting view content from the database:

The real work is done in the GetView method, which is called in the constructor. It checks the database for the existence of an entry matching the file path provided by the view engine. If a match is found, Exists is set to true and the content is made available as a Stream via the CreateReadStream method. I've chosen to use plain ADO.NET for this example, but other data access technologies are available.

IChangeToken

The final component in the chain is the implementation of IChangeToken. This is responsible for notifying the view engine that a view has been modified, and that the cached version should be replaced with the updated version.

The key member of the interface is the HasChanged property. The value of this is determined by comparing the last requested time and the last modified time of a matching file entry. If the file has been modified since it was last requested, the property is set to true which results in the view engine retrieving the modified version.

The only thing left to do now is to register the DatabaseFileProvider with the view engine so that it knows to use it. This is done in the ConfigureServices method in Startup.cs:

The are some points to note. The PhysicalFileProvider will be invoked first since it has been registered first. If you have a .cshtml file in one of the locations that get checked, it will be returned and the DatabaseFileProvider (or any subsequent providers) will not be invoked for that request. In its current form, the IChangeToken will be invoked for every location that the view engine checks. For that reason, it would be sensible perhaps to cache the paths where database entries exist, and to prevent the database request being executed if the requested path is not in the cache.

Summary

The Razor view engine has been designed to be fully extensible, enabling you to plug in your own FileProvider so that you can locate and load view from any source you can write a provider for. This article shows how you can do that using a database as a source. The sample site is available from GitHub.