Load Razor Views From Database in ASP.NET MVC 5

2020-07-23 06:36发布

We are trying to develop a corporate CMS application in ASP.NET MVC 5. The user needs to create a new page, change an existing page content or delete a page alltogether from an admin application. Another requirement states that some of the pages can include custom widgets which were written in Razor syntax.

These 2 requirements lead me to load razor views from the database. I googled and find a couple of examples.

The first one is to extend the VirtualPathProvider and register it with the expression below in the Application_Start method.

protected void Application_Start()
{
    ...
    HostingEnvironment.RegisterVirtualPathProvider(new MyVirtualPathProvider());
}

When I try to run this, it throws the error below

[InvalidOperationException: The view at '~/Views/Home/Index.aspx' must derive from ViewPage, ViewPage, ViewUserControl, or ViewUserControl.]
    System.Web.Mvc.WebFormView.RenderView(ViewContext viewContext, TextWriter writer, Object instance) +180
    System.Web.Mvc.BuildManagerCompiledView.Render(ViewContext viewContext, TextWriter writer) +107
    System.Web.Mvc.ViewResultBase.ExecuteResult(ControllerContext context) +291
    System.Web.Mvc.ControllerActionInvoker.InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult) +13
    System.Web.Mvc.ControllerActionInvoker.InvokeActionResultFilterRecursive(IList`1 filters, Int32 filterIndex, ResultExecutingContext preContext, ControllerContext controllerContext, ActionResult actionResult) +56
    System.Web.Mvc.ControllerActionInvoker.InvokeActionResultFilterRecursive(IList`1 filters, Int32 filterIndex, ResultExecutingContext preContext, ControllerContext controllerContext, ActionResult actionResult) +420
    System.Web.Mvc.ControllerActionInvoker.InvokeActionResultWithFilters(ControllerContext controllerContext, IList`1 filters, ActionResult actionResult) +52
    System.Web.Mvc.Async.c__DisplayClass2b.b__1c() +173
    System.Web.Mvc.Async.c__DisplayClass21.b__1e(IAsyncResult asyncResult) +100
    System.Web.Mvc.Async.WrappedAsyncResult`1.CallEndDelegate(IAsyncResult asyncResult) +10
    System.Web.Mvc.Async.WrappedAsyncResultBase`1.End() +49
    System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeAction(IAsyncResult asyncResult) +27
    System.Web.Mvc.Controller.b__1d(IAsyncResult asyncResult, ExecuteCoreState innerState) +13
    System.Web.Mvc.Async.WrappedAsyncVoid`1.CallEndDelegate(IAsyncResult asyncResult) +29
    System.Web.Mvc.Async.WrappedAsyncResultBase`1.End() +49
    System.Web.Mvc.Controller.EndExecuteCore(IAsyncResult asyncResult) +36
    System.Web.Mvc.Controller.b__15(IAsyncResult asyncResult, Controller controller) +12
    System.Web.Mvc.Async.WrappedAsyncVoid`1.CallEndDelegate(IAsyncResult asyncResult) +22
    System.Web.Mvc.Async.WrappedAsyncResultBase`1.End() +49
    System.Web.Mvc.Controller.EndExecute(IAsyncResult asyncResult) +26
    System.Web.Mvc.Controller.System.Web.Mvc.Async.IAsyncController.EndExecute(IAsyncResult asyncResult) +10
    System.Web.Mvc.MvcHandler.b__5(IAsyncResult asyncResult, ProcessRequestState innerState) +21
    System.Web.Mvc.Async.WrappedAsyncVoid`1.CallEndDelegate(IAsyncResult asyncResult) +29
    System.Web.Mvc.Async.WrappedAsyncResultBase`1.End() +49
    System.Web.Mvc.MvcHandler.EndProcessRequest(IAsyncResult asyncResult) +28
    System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.EndProcessRequest(IAsyncResult result) +9
    System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +9644097
    System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +155

Another example is

protected void Application_Start()
{
    ...
    ViewEngines.Engines.Clear();
    ViewEngines.Engines.Add(new MyViewEngine());
}

private class MyViewEngine : RazorViewEngine
{
    public MyViewEngine()
    {
        this.VirtualPathProvider = new MyVirtualPathProvider();
    }
}

This one works when the specified file (e.g. /View/Home/Index.cshtml) is in the file system even it has nothing in it. When I delete the file from the file system, the error below is thrown

[InvalidOperationException: The view 'Index' or its master was not found or no view engine supports the searched locations. The following locations were searched:
    ~/Views/Home/Index.cshtml
    ~/Views/Home/Index.vbhtml
    ~/Views/Shared/Index.cshtml
    ~/Views/Shared/Index.vbhtml]
    System.Web.Mvc.ViewResult.FindView(ControllerContext context) +382
    System.Web.Mvc.ViewResultBase.ExecuteResult(ControllerContext context) +116
    System.Web.Mvc.ControllerActionInvoker.InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult) +13
    System.Web.Mvc.ControllerActionInvoker.InvokeActionResultFilterRecursive(IList`1 filters, Int32 filterIndex, ResultExecutingContext preContext, ControllerContext controllerContext, ActionResult actionResult) +56
    System.Web.Mvc.ControllerActionInvoker.InvokeActionResultFilterRecursive(IList`1 filters, Int32 filterIndex, ResultExecutingContext preContext, ControllerContext controllerContext, ActionResult actionResult) +420
    System.Web.Mvc.ControllerActionInvoker.InvokeActionResultWithFilters(ControllerContext controllerContext, IList`1 filters, ActionResult actionResult) +52
    System.Web.Mvc.Async.c__DisplayClass2b.b__1c() +173
    System.Web.Mvc.Async.c__DisplayClass21.b__1e(IAsyncResult asyncResult) +100
    System.Web.Mvc.Async.WrappedAsyncResult`1.CallEndDelegate(IAsyncResult asyncResult) +10
    System.Web.Mvc.Async.WrappedAsyncResultBase`1.End() +49
    System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeAction(IAsyncResult asyncResult) +27
    System.Web.Mvc.Controller.b__1d(IAsyncResult asyncResult, ExecuteCoreState innerState) +13
    System.Web.Mvc.Async.WrappedAsyncVoid`1.CallEndDelegate(IAsyncResult asyncResult) +29
    System.Web.Mvc.Async.WrappedAsyncResultBase`1.End() +49
    System.Web.Mvc.Controller.EndExecuteCore(IAsyncResult asyncResult) +36
    System.Web.Mvc.Controller.b__15(IAsyncResult asyncResult, Controller controller) +12
    System.Web.Mvc.Async.WrappedAsyncVoid`1.CallEndDelegate(IAsyncResult asyncResult) +22
    System.Web.Mvc.Async.WrappedAsyncResultBase`1.End() +49
    System.Web.Mvc.Controller.EndExecute(IAsyncResult asyncResult) +26
    System.Web.Mvc.Controller.System.Web.Mvc.Async.IAsyncController.EndExecute(IAsyncResult asyncResult) +10
    System.Web.Mvc.MvcHandler.b__5(IAsyncResult asyncResult, ProcessRequestState innerState) +21
    System.Web.Mvc.Async.WrappedAsyncVoid`1.CallEndDelegate(IAsyncResult asyncResult) +29
    System.Web.Mvc.Async.WrappedAsyncResultBase`1.End() +49
    System.Web.Mvc.MvcHandler.EndProcessRequest(IAsyncResult asyncResult) +28
    System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.EndProcessRequest(IAsyncResult result) +9
    System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +9644097
    System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +155

The RazorViewEngine tries to find the file in the file system even if you override the VirtualPathProvider to load view data from the database.

I guess the examples work in the previous versions of ASP.NET MVC (I think 2 or 3). The best solution that I got is to write a custom view engine from scratch, but there are too much work to be done to make it work like the razor view engine. Is there any other trick to make these examples work, or any other alternatives?