受欢迎的博客标签

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

Published
https://www.mikesdotnetting.com/article/301/loading-asp-net-core-mvc-views-from-a-database-or-other-location source code:https://github.com/freeboygirl/RazorEngineViewOptionsFileProviders   For the vast majority of 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, aPhysicalFileProvider 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. AnEmbeddedFileProvider 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 IFileInfointerface representing a file implementation. The Watch method returns an implementation of the IChangeTokeninterface. 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. TheIChangeToken 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 LastModifiedfield defaults to GetUtcDate when the view is created, and is updated whenever the view content is modified. TheLastRequested 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 Name, Exists, Length properties and the CreateReadStream method. Here is theDatabaseFileInfoclass, 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 trueand 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.  .