2

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.

jps
  • 20,041
  • 15
  • 75
  • 79
Darren
  • 9,014
  • 2
  • 39
  • 50
  • Why not simply have the console application make a http request for the page with the view? – NineBerry Feb 25 '17 at 01:25
  • That would confuse my statistics for page response times and make it difficult to measure the improvement and if google was crawling the site at the same time it would ruin performance stats they have. – Darren Feb 25 '17 at 02:43
  • I also want to replace the cached page BEFORE it has expired. – Darren Feb 25 '17 at 02:47

1 Answers1

3

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.

Community
  • 1
  • 1
John Wu
  • 50,556
  • 8
  • 44
  • 80
  • Restarting the app pool will not clear the server side output cache when using a different provider for storing the data (as the Questioner does, they use a provider that stores the data in Redis.) What you can do is create a specific controller action to be called from externally to clear the cache. This controller action can then use `HttpResponse.RemoveOutputCacheItem` to clear the cache for a specific other controller action. – NineBerry Mar 01 '17 at 14:40
  • Ok it seems that it would be better to fire off a request immediately after invalidating the cache for the same url, using the method suggested by @NineBerry. I mark this as the answer though as don't imagine it is much easier with .NET core either. – Darren Mar 03 '17 at 03:06
  • BTW I just left it as it was as I was already doing the overnight process that generates the required data for the views, actually I tweaked that a bit. – Darren Oct 26 '17 at 22:43