Preload ASP.NET MVC views in IIS warmup step
Asked Answered
M

3

8

I recently began playing around with the ability of IIS to apply a warmup step step to my web application through use of the IProcessHostPreloadClient interface (look here for for guidance on how to set this up). This worked out great, or at least I thought it did, because one of the 'clever' things I did was to try and preload my views by iterating over my controllers and rendering them.

After a bit of trial and error, I got it to work and all was well. That is, until I noticed that all validation for my system no longer worked, neither client nor server validation. I assume that the validation is normally hooked up to the views when MVC retrieves a view for the first time and I failed to do so. Does anyone have an idea how this could be included in my solution or perhaps done in another way?

The code:

public class Warmup : IProcessHostPreloadClient
{
    public void Preload(string[] parameters)
    {
        //Pre-render all views
        AutoPrimeViewCache("QASW.Web.Mvc.Controllers", @"Views\");
        AutoPrimeViewCache("QASW.Web.Mvc.Areas.Api.Controllers", @"Areas\Api\Views\", "Api");
    }

    private void AutoPrimeViewCache(string controllerNamespace, string relativeViewPath, string area = null)
    {
        var controllerTypes = typeof(Warmup).Assembly.GetTypes().Where(t => t.Namespace == controllerNamespace && (t == typeof(Controller) || t.IsSubclassOf(typeof(Controller))));
        var controllers = controllerTypes.Select(t => new { Instance = (Controller)Activator.CreateInstance(t), Name = t.Name.Remove("Controller") });

        foreach (var controller in controllers)
        {
            var viewPath = Path.Combine(HostingEnvironment.ApplicationPhysicalPath, relativeViewPath + controller.Name);
            var viewDir = new DirectoryInfo(viewPath);
            if (viewDir.Exists)
            {
                var viewNames = viewDir.EnumerateFiles("*.cshtml").Select(f => f.Name.Remove(".cshtml")).ToArray();
                PreloadController(controller.Instance, area, viewNames);
            }
        }
    }

    private void PreloadController(Controller controller, string area, params string[] views)
    {
        var viewEngine = new RazorViewEngine();

        var controllerName = controller.GetType().Name.Remove("Controller");
        var http = new HttpContextWrapper(new HttpContext(new HttpRequest(null, "http://a.b.com", null), new HttpResponse(TextWriter.Null)));
        var routeDescription = area == null ? "{controller}/{action}/{id}" : area + "/{controller}/{action}/{id}";
        var route = new RouteCollection().MapRoute(
            "Default", // Route name
            routeDescription, // URL with parameters
            new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );

        var routeData = new RouteData(route, route.RouteHandler);
        routeData.Values.Add("controller", controllerName);
        if (area != null)
        {
            routeData.Values.Add("area", area);
            routeData.DataTokens.Add("area", area);
        }
        routeData.DataTokens.Add("controller", controllerName);
        routeData.Values.Add("id", 1);
        routeData.DataTokens.Add("id", 1);
        var controllerContext = new ControllerContext(http, routeData, controller);
        var vDic = new ViewDataDictionary();
        var vTemp = new TempDataDictionary();

        foreach (var view in views)
        {
            var viewResult = viewEngine.FindView(controllerContext, view, null, false);
            if (viewResult.View == null)
                throw new ArgumentException("View not found: {0} (Controller: {1})".Args(view, controllerName));
            var viewContext = new ViewContext(controllerContext, viewResult.View, vDic, vTemp, TextWriter.Null);
            try { viewResult.View.Render(viewContext, TextWriter.Null); }
            catch { }
        }
    }
}
Milliemillieme answered 3/10, 2011 at 11:18 Comment(0)
M
3

The problem is not with the code in the question but the time at which it is executed. Moving the code into an action allows me to perform the warmup step without problems. In my case, I guess I'll just get the installation process to call the warmup action after the system has been configured.

Milliemillieme answered 9/11, 2011 at 9:25 Comment(0)
M
3

Not direct answer to your question, but I think you should take a look at Precompiling MVC Razor views using RazorGenerator by David Ebbo

One reason to do this is to avoid any runtime hit when your site starts, since there is nothing left to compile at runtime. This can be significant in sites with many views.

Manchester answered 3/10, 2011 at 12:11 Comment(2)
I use some global Razor helper methods and I can't get the Razor generator to work with them. Otherwise, it looks to be a very useful tool.Milliemillieme
I am not sure that precompiling views is worth the effort. I think the new IIS application initialization with its support for overlapping process recycling solves the problem better.Evident
M
3

The problem is not with the code in the question but the time at which it is executed. Moving the code into an action allows me to perform the warmup step without problems. In my case, I guess I'll just get the installation process to call the warmup action after the system has been configured.

Milliemillieme answered 9/11, 2011 at 9:25 Comment(0)
E
3

There is a new module from Microsoft that is part of IIS 8.0 that supercedes the previous warm-up module. This Application Initialization Module for IIS 7.5 is available a separate download.

The module will create a warm-up phase where you can specify a number of requests that must complete before the server starts accepting requests. These requests will execute and compile all your views in a more robust manner than what you are trying to achieve.

I have answered a similar question with more details at How to warm up an ASP.NET MVC application on IIS 7.5?.

Evident answered 3/10, 2012 at 19:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.