Search code examples
asp.net-mvcasp.net-coreasp.net-mvc-5outputcache

Insert to asp.net mvc outputcache from console program


I am using Redis for asp.net MVC output cache. Some of my views take a fair bit of processing, currently I have an overnight process that generates the required data for the views and puts it in Redis cache so the views can render much quicker, however the data is only in the cache for the purpose of the initial render of the view and then the view is cached by output cache config.

It would be MUCH better if I could just render the view and put that directly into the cache from the overnight console program. How would I do this? I gather I would need to insert to Redis with the same key that ASP.NET MVC would give and call whatever internal render method that asp.net MVC uses?

I don't need instructions for inserting to Redis, rather what is the render method I need to call and how are the key names constructed for asp.net MVC OutputCache.

I am using asp.net MVC 5, however, bonus kudos if you can also answer for Core to futureproof the answer!

Please no suggestions of generating static files, that's not what I want, Thanks.


Solution

  • How are the key names constructed for asp.net mvc outputcache?

    This part is easy to answer if you consult the source code for OutputCacheAttribute. The keys depend on the settings (e.g. the keys will have more data in them if you have set VaryByParam). You can determine the keys by checking how the attribute populates uniqueID for you use case. Notice that the keys are concatenated and then hashed (since they could get very long) and then base64-encoded. Here is the relevant code:

    internal string GetChildActionUniqueId(ActionExecutingContext filterContext)
    {
        StringBuilder uniqueIdBuilder = new StringBuilder();
    
        // Start with a prefix, presuming that we share the cache with other users
        uniqueIdBuilder.Append(CacheKeyPrefix);
    
        // Unique ID of the action description
        uniqueIdBuilder.Append(filterContext.ActionDescriptor.UniqueId);
    
        // Unique ID from the VaryByCustom settings, if any
        uniqueIdBuilder.Append(DescriptorUtil.CreateUniqueId(VaryByCustom));
        if (!String.IsNullOrEmpty(VaryByCustom))
        {
            string varyByCustomResult = filterContext.HttpContext.ApplicationInstance.GetVaryByCustomString(HttpContext.Current, VaryByCustom);
            uniqueIdBuilder.Append(varyByCustomResult);
        }
    
        // Unique ID from the VaryByParam settings, if any
        uniqueIdBuilder.Append(GetUniqueIdFromActionParameters(filterContext, SplitVaryByParam(VaryByParam)));
    
        // The key is typically too long to be useful, so we use a cryptographic hash
        // as the actual key (better randomization and key distribution, so small vary
        // values will generate dramtically different keys).
        using (SHA256Cng sha = new SHA256Cng())
        {
            return Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(uniqueIdBuilder.ToString())));
        }
    }
    

    You'll notice later the uniqueID is used as a key into the internal cache:

    ChildActionCacheInternal.Add(uniqueId, capturedText, DateTimeOffset.UtcNow.AddSeconds(Duration));
    

    What is the render method I need to call?

    Short answer: ExecuteResult.

    Long answer: Holy crap, you are asking a lot here. Essentially you wish to instantiate some objects within the console process and call methods which will faithfully recreate the output that would have been created if you called it from within the AppDomain where the web site usually runs.

    Web applications often rely on initialization and state that is created when the application starts up (e.g. setting up the composition root/IoC, or setting up Automapper, that sort of thing), so you'd have to run the initialization of your web site. A specific view may rely on contextual information such as the URL, cookies, and querystring parameters; it may rely on configuration; it may call internal services, which also rely on configuration, as well as the AppDomain account being set up a certain way; it may need to use things like client certificates which may be set up in the service account's personal store, etc.

    Here is the general procedure of what the console app would have to do:

    1. Instantiate the site's global object, calling its constructor, which may attempt to wire up events to the pipeline.
    2. You will need to mock the pipeline and handle any events raised by the site. You will also need to raise events in a manner that simulates the way the ASP.NET pipeline works.
    3. You will need to implement any quirks in the ASP.NET pipeline, e.g. in addition to raising events you will also need to call handlers that aren't subscribed to the events if they have certain predefined names, such as Application_Start.
    4. You will need to emulate the HTTP request by constructing or mocking pipeline objects, such as HttpContext.
    5. You will need to fire request-specific events at your code in the correct order to simulate HTTP traffic.
    6. You will need to run your routing logic to determine the appropriate controller to instantiate, then instantiate it.
    7. You will need to read metadata from your action methods to determine which filters to apply, then instantiate them, and allow them to subscribe to yet more events, which you must publish.
    8. In the end you will need to get the ActionResult object that results from the action method and call its ExecuteResult method.

    I don't think this is a feasible approach, but I'd like to hear back from you if you succeed at it.

    What you really ought to do

    Your console application should simply fire HTTP requests at your application to populate the cache in a manner consistent with actual end user usage. This is how everyone else does it.

    If you wish to replace the cached page before it has expired, you can invalidate the cache by restarting the app pool, or by using a dependency.

    If you are worried about your response time statistics, change the manner in which you measure them so that you exclude any time window where this refresh is occuring.

    If you are worried about impacts to a Google crawl, you can modify the host load schedule and set it to 0 during your reset window.

    If you really don't want to exercise the site

    If you insist that you don't want to exercise the site to create the cache, I suggest you make the views lighter weight, and look at caching at lower layers in your application.

    For example, if the reason your views take so long to render is that they must run complicated queries with a lot of joins, consider implementing a database cache in the form of a denormalized table. You can run SQL Agent jobs to populate the denormalized table on a nightly basis, thus refreshing your cache. This way the view can be lightweight and you won't have to cache it on the web server.

    For another example, if your web application calls RESTful services that take a long time to run, consider implementing cache-control headers in your service, and modify your REST client to honor them, so that repeated requests for the same representation won't actually require a service call. See Caching your REST API.