To review, OutputCaching is part of the ASP.NET Pipeline and is the same mechanism that you can use in ASP.NET Pages using the @OutputCache directive. Using OutputCache with a handler rather than a Page is a little less discoverable than the page directive, but it provides the same functionality for any code running in the pipeline. OutputChacing is implemented as a Module in the pipeline and depending on the type of request that is being processed (headers, length, whether it’s a POST operation etc.) the caching can get elevated to the IIS Kernel Cache (although information on when and how that actually occurs is very scarce). OutputCaching is probably the most efficient way to cache request because it is handled internally and doesn’t have to hit custom code if cached content is served. The content of a cached entry is fully held in the cache including headers, encoding and the content so there’s no requirement to re-encode or re-send headers etc. as you would when caching content on your own using the element cache as I did originally in the blog post code.
OutputCache is applied in its own step in the ASP.NET Event Pipeline via the ResolveRequestCache event which fires after request authorization (because you still want to authenticate/authorize cached requests) and before the request state is retrieved and a handler is loaded. For new entries the cache is then updated towards the end of the request in the UpdateRequestCache Event. You can see where in the pipeline these events fire on this old slide:
All this adds up to a very efficient mechanism of serving cached content because it fires very early in the pipeline processing and is handled internally by ASP.NET or (in IIS7) by IIS itself.
Writing to OutputCache is pretty straight forward and looks something like this:
HttpResponse Response = HttpContext.Current.Response;
// client cache
Response.ExpiresAbsolute = DateTime.UtcNow.AddDays(30);
// OutputCache settings
HttpCachePolicy cache = Response.Cache;
cache.VaryByParams["LocaleId"] = true;
cache.VaryByParams["ResoureType"] = true;
cache.VaryByParams["IncludeControls"] = true;
cache.VaryByParams["VarName"] = true;
cache.VaryByParams["IsGlobal"] = true;
DateTime now = DateTime.Now;
cache.SetExpires(now + TimeSpan.FromDays(365.0));
Response.ContentType = contentType;
Response.Charset = "utf-8";
Response.End(); // No caching if specified
Watch out for Response.End()
Now as it turns out the code as I have it written here DOES NOT work – as you can surmise from the highlighting the Response.End() actually makes this code run, but the output never makes it into the OutputCache. The reason for the Response.End() in the code here is that the code in question is a method in the handler rather than directly defined in ProcessRequest. Originally there was a bit of error checking code and cleanup that followed the output generation code in case there was a problem. There are also a number of error generation routines that create 404 requests on missing resources and errors on invalid syntax and so on – all of which also fire a Response.End(). So to be consistent I thought that using Response.End() everwhere would be the right thing to do.
Au contraire – using Response.End() in this fashion causes the processing of the pipeline to abort complete after output generation. In other words by specifying the Response.End() code at the end of the request causes UpdateRequestCache() to never fire and put the content into the cache. Response.End() (and HttpApplication.CompleteRequest()) causes the request to stop right at the point of the Response.End() call and jump to the EndRequest() handler, bypassing any events in between including UpdateRequestCache() which is needed to write the cache content into the OutputCache.
So, after a quick refactoring of the method and how post processing is handle I could remove the Response.End() call and voila the caching started working. The error method still do a Response.End() to exit early, but that’s acceptable because they should not cache anyway – they never actually hit the cache code (which is obviously important – you wouldn’t want to cache an error :-}).
This is pretty obvious once you know what’s happening, but I can tell you I looked at the code for quite some time agonizing over WHY the content was not getting into OutputCache even though it was staring me straight into the face. The Response.End() looked innocent enough.
Hopefully this post will help me – and maybe even one or two of you – jog my memory and avoid this problem in the future.
I'll be at DevIntersection in Vegas this fall giving sessions on ASP.NET Core with Angular and Localization. Thinking of coming? Use discount code STRAHL and save a few bucks. If you do be sure to stop by and say hello!
Other Posts you might also like