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
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 DatabaseFileInfo
class, which is the custom implementation of IFileInfo
for 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.