Donut Output Caching in ASP.NET MVC 3

Donut caching is the one form of output caching that is conspicuously absent from ASP.NET MVC 3 (and MVC4 as we speak) and is greatly missed by many developers. This post describes MvcDonutCaching, a new open-source NuGet package that adds donut caching to MVC3 in a simple and performant manner.

Output Caching in ASP.NET MVC

Output caching in MVC is very easy to understand. In fact, it is not so different from the output caching in ASP.NET WebForms that most people will already been familiar with. In MVC, output caching allows you to store the output of controller actions and return these cached results instead of actually executing the controller actions on subsequent calls. Given that most pages in a commercial web application will typically include data that comes from one or more services or databases, it is easy to see that returning a cached result instead of calling these services will be significantly quicker and will also allow the server to respond to considerably more requests.

Some people seem to be rather dismissive of output caching, believing that it is not suitable or offers no benefit for their particular web apps but in fact, the vast majority of sites can benefit from output caching.

Two important points to make are:

  1. Very small cache durations of a few seconds can have a dramatic impact on a high-traffic site. This means that even near real-time applications can make use of output caching.

  2. Even if you are caching heavily within the application, output caching can still add significant performance boosts. Retrieving data from a cache and then rendering the page using this data is typically slower than retrieving the whole page from the output cache.

What is Donut Caching?

When we talk about output caching, there are three different scenarios that we may encounter:

Full page caching

Full page caching is where you cache the entire page with no substitutions. It is handled with the built-in OutputCache attribute that has been available since ASP.NET MVC 1.

Partial page caching (Donut hole caching)

Donut hole caching is where you cache one or more parts of a page, but not the entire page. It is handled by using the built-in OutputCache attribute on one or more child actions (called using Html.Action or Html.RenderAction from the parent view). It has been available since ASP.NET MVC 3.

Partial page caching (Donut caching)

Donut caching is where you cache the entire page except for one or more parts. It was possible in ASP.NET MVC 1 by using the substitution control, but is not available out of the box in ASP.NET MVC 3.

The omission of donut caching in MVC3 is unfortunate as it is an extremely popular requirement. In fact, any site that allows a user to log in will usually not be able to use full page caching (at least, not without employing secondary ajax requests as a workaround).

How should donut caching work?

In ASP.NET MVC, output caching is implemented using an action filter attribute which can be applied to a controller or to individual controller actions. When an action is executed for the first time, the OutputCacheAttribute intercepts the ActionResult before the output is returned to the client and stores this output for a configurable period of time. On subsequent requests, the output is returned from the cache rather than executing the controller action, significantly reducing the amount of work necessary to service the request.

To implement donut caching, the concept is fairly simple. We still cache the page output and return it from the cache on subsequent requests, but we do require some extra steps. Before we cache the page, we need to identify the areas that we do not want to cache (the donut holes). These areas must be marked in some way so that when we retrieve the page from the cache, we can replace these areas with non-cached versions before returning the page to the client.

Sounds easy...so what's the problem?

Whilst ASP.NET MVC is built on top of the ASP.NET framework, it has some quite significant differences that have made the re-use of ASP.NET features rather difficult in MVC. One such difficulty is the way that output caching works. If you look at the MVC3 source code, you will find that full page output caching and partial page output caching are implemented in completely different ways. Whilst full page caching hooks into standard ASP.NET page caching and uses an HttpModule (OutputCacheModule), partial page caching uses a separate code path that saves the output using the MemoryCache object introduced in .NET 4. When retrieving an item, there is code in the OutputCacheAttribute to get a partial page from the MemoryCache. For a full page however, the OutputCacheAttribute never even gets hit and the page is retrieved using the OutputCacheModule without even touching the MVC framework.

Once you know this, it is easier to understand why some features are available in full page caching that do not work in partial page caching (see next section). In terms of implementing donut caching, hooking into the OutputCacheModule HttpModule would be very difficult/impossible, so instead we must rewrite the OutputCacheAttribute completely, saving both full and partial pages in the same caching store and fixing some of the inconsisitencies seen in the current implementation.

Other limitations of output caching in MVC3.

Many people have already described several limitations with child action caching in MVC 3 including this post by Greg Roberts. Greg proposes some nice solutions which we will include in our new cache attribute.

In summary, the four main limitations are:

Cache Profiles are not supported

It is very useful to allow each output cache to share a common configuration rather than hard-coding a numeric duration for each one. The standard mechanism for this in ASP.NET is to use cache profiles. Cache profiles allow you to use the web.config to specify one or more profiles for your caching requirement. Each output cache definition then references a profile. This allows you to change the cache configuration for multiple items at once and without requiring a code deployment.

<caching>
  <outputCacheSettings>
    <outputCacheProfiles>
      <add name="Level3" duration="300" varyByParam="*" />
    </outputCacheProfiles>
  </outputCacheSettings>
</caching>

In ASP.NET MVC 3, cache profiles are supported for standard controller actions, but not for child actions.

Disabling caching in the web.config is ignored

The standard ASP.NET output caching framework allows you to disable output caching by changing a web.config value. This is extremely useful for running your code locally where caching can interfere with debugging.

<caching>
  <outputCache enableOutputCache="false"/>
  <outputCacheSettings>
  ...
  </outputCacheSettings>
</caching>

Unfortunately, while full page caching does adhere to this value, partial page caching completely ignores it.

With the advent of web.config transformations, it is now very simple to automate the process of enabling caching when releasing code. Simply add the following to web.config.release and when you build your application in release mode, your web.config will automatically be transformed to remove the enableOutputCache="false" configuration

<system.web>
  <compilation xdt:Transform="RemoveAttributes(debug)" />
  <caching>
    <outputCache xdt:Transform="Remove" />
  </caching>    
</system.web>

It is not easy to remove output cache items, particularly for child actions

It is often quite useful to be able to remove items from the outputcache dynamically. If you have this ability, it is possible to ramp up cache durations quite significantly because you can automatically invalidate the cache when you know that data has changed.

In MVC, for non web farm scenarios, we can make use of the ASP.NET RemoveOutputCacheItem method on the Response where the path parameter is the relative path to the page. Because it is URL based, if you have an action with arguments that are part of the route such as a page number, it is not possible to remove all cached pages for that particular action. Instead, you need to call the method for each different argument which is not ideal.

Response.RemoveOutputCacheItem(path)

For child actions, it is even worse. The only method that I could find was posted on the ASP.NET forums

OutputCacheAttribute.ChildActionCache = new MemoryCache("NewDefault");

Clearly this is not ideal. Instantiating a new MemoryCache will prevent the OutputCache from retrieving ALL previously cached child actions, not just the one(s) that we wanted to remove. Another problem is the fact that these items are not actually removed from the cache, so they are still in memory. Because the OutputCacheAttribute uses MemoryCache.Default, no garbage collection will take place and the newly instantiated MemoryCache will slowly fill up will duplicate items.

Using a custom output cache store is problematic

Whilst the standard ASP.NET in-process caching mechanism is sufficient for single web servers, enterprise level applications typically run on large web farms. In these kind of environments, a distributed cache such as memcached or Windows Server AppFabric Caching makes much more sense. A useful feature introduced in .NET 4.0 is the ability to write your own implementation of OutputCacheProvider and configure ASP.NET to use it via the web.config. This feature makes is extremely easy to change your site to use distributed caching. Unfortunately, the build-in OutputCacheAttribute does not use the configured OutputCacheProvider for child actions. Instead, the MVC team have added a static property on the OutputCacheAttribute class named ChildActionCache of type ObjectCache.

Although none of these issues are dealbreakers, it makes sense to address these issues if we are going to rewrite the OutputCacheAttribute. In the next section, we will see exactly how we have done this.

Introducing the MvcDonutCaching NuGet package

As we have already discussed, in order to implement donut caching, we first need a way of expressing that a particular piece of content should not be cached within a page. There are many different approaches that we could take. Maarten Balliauw's donut caching article from 2008 uses a substitution HTML helper which allows you to specify a static method to call, but we are going to take a slightly different approach.

We are going to add multiple overloads to the Html.Action helper method to allow a user to specify that the action should be excluded from the page cache.

public static MvcHtmlString Action(this HtmlHelper html, string actionName, 
  string controllerName, RouteValueDictionary routeValues, 
  bool excludeFromParentCache)
{
  if (excludeFromParentCache)
  {
    var settings = new ActionSettings
    {
      ActionName = actionName,
      ControllerName = controllerName,
      RouteValues = routeValues
    };

    var serialisedSettings = new ActionSettingsSerialiser().Serialise(settings);

    return new MvcHtmlString(serialisedSettings);
  }

  return html.Action(actionName, controllerName, routeValues);
}

If the excludeFromParentCache parameter is set to true, instead of actually rendering the action, we serialise the action arguments and return an html comment containing the arguments. This comment will be included in the cached page and when we come to the substitution code later on, we will be able to find all the areas that need replacing very easily.

So that was very easy and the code is pretty clean. The next thing to look at is the OutputCacheAttribute itself. As we have already discussed, the built-in OutputCacheAttribute hooks into the ASP.NET output caching mechanism which uses an HttpModule to intercept requests for cached content very early in the request pipeline. Trying to add substitution to this mechanism would be very difficult, hence the reason that Microsoft has not implemented donut caching in the last few releases of ASP.NET MVC. Therefore, we cannot simply subclass OutputCacheAttribute and instead need to build our caching action filter by subclassing ActionFilterAttribute and implementing the caching logic from scratch.

Note that I am not going to show all the code here, just the interesting bits, but you can download the full source code from CodePlex if you are curious.

When subclassing ActionFilterAttribute, we have four methods that we can override:

Our caching action filter has two main functions. Firstly, it must check for a cached action output and secondly, it must cache the output if it is not already cached. Let's look at each function in turn.

Retrieving a cached action output

The code that attempts to retrieve a cached action output must execute as early as possible to avoid unnecessary code execution. Therefore the OnActionExecuting method is used.

public override void OnActionExecuting(ActionExecutingContext filterContext)
{
    _cacheSettings = BuildCacheSettings();

    if (_cacheSettings.IsCachingEnabled)
    {
        _cacheKey = _keyGenerator.GenerateKey(filterContext, _cacheSettings);

        var content = _outputCacheManager.GetItem(_cacheKey);

        if (content != null)
        {
            filterContext.Result = new ContentResult 
            {
              Content = _donutHoleFiller.ReplaceDonutHoleContent(content, filterContext) 
            };
        }
    }
}
	

If caching is disabled or the cached content is not found, the filterContext.Result is not set and the MVC framework will continue on to executing the controller action. But by setting the filterContext.Result when a cached item is found, the controller action will not get executed and the cached page will be served. Note that we are passing the cached output into the ReplaceDonutHoleContent method which is used to make the substitutions.

public string ReplaceDonutHoleContent(string content, ControllerContext filterContext)
{
    if (filterContext.IsChildAction)
    {
        return content;
    }

    return DonutHoles.Replace(content, new MatchEvaluator(match =>
    {
        var settings = _actionSettingsSerialiser.Deserialise(match.Groups[1].Value);

        return InvokeAction(filterContext.Controller, settings.ActionName, 
settings.ControllerName, settings.RouteValues);
    }));
}

The ReplaceDonutHoleContent uses a regular expression to find all actions that need substituting. Remember that we are using HTML comments to pass information about the actions that need to be executed. For each match found, We deserialise the action settings and replace the HTML comment with the result of the InvokeAction method. This means that the page that is returned does not include the HTML comment so the end user has no idea of the caching mechanism being used.

private static string InvokeAction(ControllerBase controller, string actionName, 
                       string controllerName, object routeValues)
{
  var viewContext = new ViewContext(controller.ControllerContext, 
                    new WebFormView(controller.ControllerContext, "tmp"),
                    controller.ViewData, controller.TempData, 
                    TextWriter.Null);

  var htmlHelper = new HtmlHelper(viewContext, new ViewPage());

  return htmlHelper.Action(actionName, controllerName, routeValues).ToString();
}

The code in InvokeAction is a bit hacky but it is all necessary in order to be able to execute the html helper action method from within the action filter, rather than from the view where it was intended to be called from. Obviously, we do not have a viewContext available, so we must build a fake one in order to instantiate the HtmlHelper.

Caching the action output

I initially assumed that I would use one of the OnResultXXX methods for caching the action output. It makes logical sense to allow the action to execute normally and then just before returning the result to the client, we save the result to the cache. If this was all that we needed to do, then this approach would suffice, but there is a bit more work to be done. Remember that if a page uses one of our Html.Action overloads then instead of rendering the action, it will render an HTML comment instead. Therefore, after we save the action output containing the HTML comment to the cache, we need to replace the comment with the actual output in the same way that we are doing in OnActionExecuting. Remember that the code in OnActionExecuting will be used once the item is in the cache whereas the OnActionExecuted code will be used the first time the action is requested and the cache is empty.

If we try this approach though then it will soon become apparent that the OnResultXXX do not allow you to change the filterContext.Result making it impossible to do the substitutions. This means that we need to use OnActionExecuted instead and rather than using the framework to execute the action, we must do it ourselves.

public override void OnActionExecuted(ActionExecutedContext filterContext)
{
    var viewResult = filterContext.Result as ViewResultBase; // only cache views/partials

    if (viewResult != null)
    {
        var content = _actionOutputBuilder.GetActionOutput(viewResult, filterContext);

        if (_cacheSettings.IsCachingEnabled)
        {
            _outputCacheManager.AddItem(_cacheKey, content, 
                DateTime.Now.AddSeconds(_cacheSettings.Duration));
        }

        filterContext.Result = new ContentResult 
        {
          Content = _donutHoleFiller.ReplaceDonutHoleContent(content, filterContext) 
        };
    }
}

Again the code is very simple. First we cast the filterContext result to ViewResultBase. ViewResultBase is the base class of both View and PartialView (the only ActionResults we are supporting), so if the cast is not null, we proceed. The call to ActionOutputBuilder returns us the HTML output of the action. Any donut holes will be marked as HTML comments. At this stage, unless caching is disabled via the web.config, we cache the output, complete with donut hole comments so we can find a replace them with updated content on subsequent calls. Next we make a call to the DonutHoleFiller (my favourite class name ever) which replaces the donut hole comments with the actual HTML content. The final HTML is then wrapper up in a content result and returned.

Usage

Using the MvcDonutCaching NuGet package is very simple. Add the package to your MVC project from within Visual Studio. To do this, select Tools | Library Package Manager and then choose either Package Manager Console or Manage NuGet Packages. Via the console, just type install-package MvcDonutCaching and hit return. From the GUI, just search for MvcDonutCaching and click the install button.

Once installed, you can add the DonutOutputCache attribute to one or more controller actions or to a whole controller if you want all actions within the controller to use donut caching. Many of the options from the built in OutputCache attribute are available to use, so all of the following are valid:

[DonutOutputCache(Duration=5)]
public ActionResult Index()

[DonutOutputCache(CacheProfile="FiveMins")]
public ActionResult Index()

[DonutOutputCache(Duration=50, VaryByCustom="whatever")]
public ActionResult Index()

[DonutOutputCache(Duration=50, VaryByParam="something;that")]
public ActionResult Index(string something)

[DonutOutputCache(Duration=50, VaryByParam="none")]
public ActionResult Index(int referrerId)

If you are using a cache profile, do not forget to add the profile to the web.config

<caching>
  <outputCacheSettings>
    <outputCacheProfiles>
      <add name="FiveMins" duration="300" varyByParam="*" />
    </outputCacheProfiles>
  </outputCacheSettings>
</caching>

You can read more about the OutputCacheAttribute options on MSDN.

If you run your application at this stage, the DonutOutputCache will just act in the same way as the built-in OutputCache and cache the full page. In order to start donut caching, we need to introduce a donut hole. Remember that we have implemented this by adding several overloads to the Action HTML helper. In all overloads, we just need to add a final true parameter to the call indicating that we do not want it cached with the parent page.

@Html.Action("Login", "Account", true)

Run the application again and you will see the donut caching in action. Whilst the parent page will be cached for whatever duration you specified, the donut hole action will be executed every request. Alternatively, you can add the OutputCacheAttribute or DonutCacheAttribute to the child action as well but use a lower cache duration. This allows you to have, for example, part of the page cached for five minutes, but the rest of the page cached for one hour.

To remove an item from the output cache, you can use the OutputCacheManager class which exposes a number of different methods and overloads that can be used for cache item removal.

var cacheManager = new OutputCacheManager();

//remove a single cached action output (Index action)
cacheManager.RemoveItem("Home", "Index");

//remove a single cached action output (page 1 of List action)
cacheManager.RemoveItem("Home", "List", new { page = 1 });

//remove all cached actions outputs for List action in Home controller
cacheManager.RemoveItems("Home", "List");

//remove all cached actions outputs for Home controller
cacheManager.RemoveItems("Home");

//remove all cached action outputs
cacheManager.RemoveItems();

Conclusion

Output caching can significantly increase the speed and throughput of a web server, even if you are caching heavily elsewhere in your application. Many web pages have some user customisation making them unsuitable for full page caching, but by using the new MvcDonutCaching NuGet package, you can now benefit from output caching on these pages as well. Add the MvcDonutCaching NuGet Package to your application to get started or download the code from codeplex if you are curious or want to contribute.

Next time, we will look at how easily we can configure an application using MvcDonutCaching to use distributed caching.

Useful or Interesting?

If you liked the article, I would really appreciate it if you could share it with your Twitter followers.

Share on Twitter

Comments

Avatar for Chris Marisic Chris Marisic wrote on 11 Nov 2011

This looks completely awesome. I was floored by how much working with OutputCache sucks in MVC3.

With your DonutCache does this mean I can write code like:

[LogThis]
[DonutOutputCache(Duration=5)]
public ActionResult Index()

And my LogThis action filter will execute before your caching? This was a major problem I faced with Output caching, wanting to do request logging for all requests, even if the request could be served from the cache.

Avatar for Paul Hiles Paul Hiles wrote on 14 Nov 2011

@Chris - Yes, unlike the built-in OutputCacheAttribute, the action filters will execute even when a page is retrieved from the cache. The only caveat to add is that you do need to be careful about the filter order.

If your action filter implements OnResultExecuting or OnResultExecuted then these methods will be executed in all cases, but for OnActionExecuting and OnActionExecuted, they will only be executed if the filter runs before the DonutOutputCacheAttribute. This is due to the way that MVC prevents subsequent filters from executing when you set the filterContext.Result property which is what we need to do for output caching.

I do not think that you can rely on the order in which action filters are defined on an action or controller. To ensure that one filter runs before another, you can make use of the Order property that is present on all ActionFilterAttribute implementations. Any actions without the order property set, default to an value of -1, meaning that they will execute before filters which have an explicit Order value.

Therefore, in your case, you can just add Order=100 to the DonutOutputCache attribute and all other filters will execute before the caching filter.

[LogThis]
[DonutOutputCache(Duration=5, Order=100)]
public ActionResult Index()

Avatar for Joe Reynolds Joe Reynolds wrote on 14 Nov 2011

I'm confused as to how I could use this in my scenario.

I use a controller like so:

[HttpGet, DonutOutputCache(Duration=300, VaryByParam="Forum")]
public ActionResult About(string Forum)
{

var model = new AboutViewModel
{
f = ForumInfoBase,
u = UserMasterInfo,
aboutToEdit = _FIrepository.GetAbout(Forum),
};

return View(model);
}

Then in my About page I use
@Html.Raw(Model.aboutToEdit.About)

How would I create a donut hole where the entire About page is cached but aboutToEdit in the model is not cached?

Avatar for Paul Hiles Paul Hiles wrote on 15 Nov 2011

@Joe - I am a little confused by your example. Your action takes in a forum argument and you are also varying the cache by this parameter using VaryByParam. This suggests to me that you do not need donut caching at all - just by using VaryByParam, you will have separate cached pages for each forum.

Donut caching is often used for user-specific items such as shopping baskets or logged in status - things that are not directly related to the request. In these cases, we use Html.Action which calls a separate sub request to a different controller action. For donut caching to be possible, the donut holes need to be separate actions from the parent, so they can be called independently.

In your example, if you do want aboutToEdit to be a donut hole, you will need to create a separate action and viewmodel for it. The action would return a partial view containing the @Html.Raw(Model.aboutToEdit.About) call. You would then call this action from your original About view.

You can read more on MSDN http://msdn.microsoft.com/en-us/library/ee839451.aspx

Avatar for Joe Reynolds Joe Reynolds wrote on 15 Nov 2011

Thanks Paul. I think I see the idea now. It's just that I have never used Html.Action. Thanks for your help.

Avatar for Larry Ruckman Larry Ruckman wrote on 15 Nov 2011

Nice post can't wait to try it out on my next project.

Avatar for O O wrote on 17 Nov 2011

Is true how sad is in version 3 is still caching very, very weak :-(

Avatar for Aviv Aviv wrote on 22 Nov 2011

I sometimes get a Object Reference not set to an instance of an object error from DevTrends.MvcDonutCaching.

DevTrends.MvcDonutCaching.KeyGenerator.GenerateKey(ControllerContext context, CacheSettings cacheSettings) +644
DevTrends.MvcDonutCaching.DonutOutputCacheAttribute.OnActionExecuting(ActionExecutingContext filterContext) +75


I'm using the same methods you are providing in this example. Are you aware of any bugs of the sort? I really would like to see something like this work for MVC3.

Avatar for Paul Hiles Paul Hiles wrote on 22 Nov 2011

@Aviv - I have not experienced this error myself but would be grateful if you could report the bug on the codeplex site and I will look into it asap. If possible, please provide a code example or at least the route details (controller name, action name, route values) that you are using, the outputcache definition (including cache profile setting if applicable) and the call(s) to html.action for the donut hole(s).

http://mvcdonutcaching.codeplex.com

Avatar for Antti Antti wrote on 30 Nov 2011

Thank you! This was just what I needed (and what I automatically thought the framework would/and definitely should have)!

Avatar for Gimme Rank Gimme Rank wrote on 02 Dec 2011

I also met the "Object reference not set to an instance of an object" error. Not sure why, but I found the error is from return htmlHelper.Action(actionName, controllerName, routeValues).ToString(); at InvokeAction of DonutHoleFiller.cs. I use normal controller, action and use Areas. The error caused by _layout.cshtml code "@Html.Action("RenderLogon", "PartialRender", new { Area = "" }, true)"

Avatar for Gimme Rank Gimme Rank wrote on 02 Dec 2011

To be more specify, the error will appear when only place Cache Attribute on the child action. If no Attribute on child action, no error.

Avatar for Korayem Korayem wrote on 05 Dec 2011

Finally, someone with the guts to do this nuget!! Excellent work :)

Can I use OutputCacheManager to invalidate cache of actions who use default attribute [OutputCache] and not [DonutOutputCache]?

Avatar for Paul Hiles Paul Hiles wrote on 05 Dec 2011

@Korayem - The OutputCacheManager is only for use with [DonutOutputCache]. You should be able to use [DonutOutputCache] in place of [OutputCache] in all page or partial page scenarios though. In other words, you can use the attribute on actions returning views or partial views regardless of whether you are doing donut caching or full caching.

Avatar for Korayem Korayem wrote on 06 Dec 2011

@Paul Hiles: WOW! Well that in itself is a selling point. The fact that DonutOutputCaching solves the problem of invalidating cache when needed.

So even if people don't want Donut Caching, they can solve another problem :)

I am already using it on my project and its working flawlessly. Thumbs Up

Avatar for Korayem Korayem wrote on 06 Dec 2011

Apparently, DonutOutputCaching does NOT set the following HTTP headers:
-Cache-Control: public, max-age=XXXX
-Expires: Tue, 06 Dec 2011 18:18:59 GMT

It sets only Cache-Control: private


:(

Avatar for Paul Hiles Paul Hiles wrote on 07 Dec 2011

@Korayem - Yes, but if you think about it, we cannot allow caching at the client.

Donut caching and cache invalidation would not be possible if the browser was caching the content. If you want to cache the whole page and do not want to be able to remove cached pages, you can just use the built-in OutputCache which will allow caching at the client and the server.

Avatar for Korayem Korayem wrote on 07 Dec 2011

@Paul Hiles: you have a point indeed, but I would rather support the "Location" attribute of [OutputCache] and let the developer choose (after explaining that invalidation only invalidates server cache and not client) than to completely ignore it in [DonutOutputCache]

Let me elaborate: my content team work daily to update information on the website. For them, they want to see their content immediately. I can easily ask them to hit CTRL+F5 to force browser to GET new version of the page. That's only possible with [DonutOutputCache].

For normal visitors, I am ok with them seeing their own browser-cached version which might not be in sync with the server cache.


So in my case, I want both contradicting features as it's my choice and I know the limitations :)

What do you think?

Avatar for Chris Marisic Chris Marisic wrote on 09 Dec 2011

I would recommend putting the nuget package install block like they use on http://nuget.org/packages/MvcDonutCaching at the top of your post.

Avatar for Chris Marisic Chris Marisic wrote on 09 Dec 2011

@Paul I started trying to use donut caching in a more complex scenario where I'm leveraging VaryByCustom, I've been debugging and seeing the output of my GetVaryByCustomString is returning identical strings however it seems like NOTHING is cached.

I also have this connected through caching profiles

<add name="varyByAction" duration="300" varyByCustom="varyByAction" />
[DonutOutputCache(CacheProfile = "varyByAction")]

Any thoughts?

Avatar for Paul Hiles Paul Hiles wrote on 10 Dec 2011

@Chris - I have emailed you a simple example of VaryByCustom that works on my machine.

Maybe you could confirm that this works for you and look for any differences between your code.

If you do confirm it to be a bug, let me know (issue tracker at http://mvcdonutcaching.codeplex.com/ is best) and I'll fix it as soon as I can.

Avatar for Ken Dale Ken Dale wrote on 12 Dec 2011

@Paul Following up for Chris:

The DonutOutputCache attribute doesn't seem to be caching when returning json. Is this expected behavior or is there some sort of issue?

Avatar for Paul Hiles Paul Hiles wrote on 15 Dec 2011

@Ken - This is expected. Currently MvcDonutCaching only supports full and partial views but as many people seem to be using it as a general OutputCache replacement rather than just for donut caching, I will try and support this is the next release.

Avatar for Paul Hiles Paul Hiles wrote on 15 Dec 2011

@Korayem - sounds reasonable. I'll try and add it to the next release.

Avatar for Nicolas Nicolas wrote on 19 Dec 2011

@Paul I wanted to use your attribute on a child action just to be able to later call the RemoveItem feature of your package. However, if the attribute is not also used on the parent action, then your Html.Action override (with true as the last param) does not call the action and nothing is rendered.

Since this is a child action, it can be called by many parent views. Some could use the DonutOuputCache attribute and some won't. That's why I don't understand this necessity to use it on the parent action...

Thanks for your help

Avatar for Paul Hiles Paul Hiles wrote on 19 Dec 2011

@Nicolas - This is an oversight (bug) that someone else raised on CodePlex last week. Don't worry though, I'll be fixing it in the next release.

Avatar for Nicolas Nicolas wrote on 19 Dec 2011

Great! Your package is a godsend btw. Thank you!

Avatar for Nicolas Nicolas wrote on 20 Dec 2011

Oh, and when do you expect this new release btw?

Avatar for Vijay Vijay wrote on 21 Dec 2011

Does it work with windows azure cache ?

Avatar for Paul Hiles Paul Hiles wrote on 23 Dec 2011

@Nicolas - I hope to have it out in the next week or two.

Avatar for Andrea Andrea wrote on 18 Jan 2012

Is it possible to use either cacheManager.RemoveItem(); or cacheManager.RemoveItems(); to delete a controller action cache based only on one of multiple parameters?

For instance, I've this possible url: /Search/Results/0B656687E843C66B71E8D60EF46D7FEE/1/100/

"0B656687E843C66B71E8D60EF46D7FEE" is a specific identifier, 1 and 100 are pagination parameters.

I'd like to delete all cached contents that have an url starting with "/Search/Results/0B656687E843C66B71E8D60EF46D7FEE".

Do you think this could be possible?

Avatar for Paul Hiles Paul Hiles wrote on 24 Jan 2012

@Andrea - there's nothing built in to support this now but will definitely add it soon as I can see it being very useful. In the mean time, you can roll your own:

public void RemoveItems(string controllerName, string actionName, string id)
{
var enumerableCache = OutputCache.Instance as IEnumerable<KeyValuePair<string, object>>;

var key = new KeyBuilder().BuildKey(controllerName, actionName);

var keyFragment = string.Format("id={0}#", id);

var keysToDelete =
enumerableCache.Where(x => x.Key.StartsWith(key) && x.Key.Contains(keyFragment)).Select(x => x.Key);

foreach (var keyToDelete in keysToDelete)
{
OutputCache.Instance.Remove(keyToDelete);
}
}

Avatar for Arthur Arthur wrote on 24 Jan 2012

Thanks for your article. I was hopeful that I could use this very awesome tool to do some caching of a SL4 project (Bing Map) within an MVC 3 application.

Here's my situation: I created a bare bones MVC 3 app with Home/About views. I proceeded to create a Silverlight Helper (Helpers.cshtml placed in the App_Code folder) which contains the Javascript to serve up the Silverlight app passing in the Bing key as an init parameter. Got this from Pete Brown's site. This runs fine but of course no caching occurs when going from the Home page to the About page and then back to the Home page (the SL4 map is reloaded).

In trying to apply your caching solution I've made some changes:
o added the following code to the _Layout.cshtml (the mapcontainer div was added):

<div id="main">
@RenderBody()
<div id="mapcontainer">
@Html.Action("DisplayMap","Home")
</div>
<div id="footer">
</div>
</div>
o created a partial view _Map.cshtml and placed the code that renders the map in it:
<div style="height:675px;">
@Helpers.Silverlight("~/ClientBin/BingMaps.Silverlight.xap", "4.0")
</div>

o added the code to the controller for the DisplayMap action along with the donut caching attribute:
[DonutOutputCache(Duration=600)]
public ActionResult DisplayMap()
{
return PartialView("_Map");
}

I was hoping it would cache the partial view _Map.cshtml such that it didn't reload the map. I'm working on something that can potentially have several map layers each with lots of pushpins on it - wouldn't want to reload that each time.

Is there something obvious that I'm missing? Can you think of any other way (global variables, e.g., isMapLoaded, etc.) that I can use to accomplish this?

Any guidance will be appreciated.
Thanks.

Avatar for catalin catalin wrote on 27 Jan 2012

Hello,
I am caching a partial view, like this:

[DonutOutputCache(Duration = 3600, VaryByParam = "product_Id")]
public ActionResult ProductInfo(Guid product_Id)
{
// code
}

If i try to remove the cache like this, is not working:

var cacheManager = new OutputCacheManager();
cacheManager.RemoveItem("Common", "ProductInfo", new { product_Id = model.Product_Id });

However, if i remove the cache without specifying the parameter, it works

cacheManager.RemoveItem("Common", "ProductInfo");

How can i resolve this problem? Please help me,
Thank you

Avatar for Paul Hiles Paul Hiles wrote on 27 Jan 2012

@catalin - there is a case-sensitivity bug for route values that will be fixed shortly. In the meantime, you can get it working by changing the parameter in the route from .../{product_Id} to .../{product_id}.

Avatar for catalin catalin wrote on 27 Jan 2012

Thank you so much!

I just spend some time trying to figure out the problem and i realized that
OutputCache was not saving the parameter value in the key so it was something to do with saving the cache, not removing it.

I first thought it needs to be just a string type, then the parameter length must not be too big (like a Guid)..i just couldn't figure it out.

Thank you!

This Cache provider helps me a lot, and you do a great job!

Avatar for ooshwa ooshwa wrote on 03 Feb 2012

Great solution!

This may be functioning as design, but just incase it is not obvious to others (it wasn't to me). A child action will automatically varybyparam to any paramater submitted to the parent action, REGARDLESS of whether that parameter is defined for the child action or the parent action. This can easily be overridden by a VaryByParam = "none" in the attribute.

Avatar for Jp Jp wrote on 16 Feb 2012

When I use HTML.action with this the result is nothing. Is this spposedto work with child actions?

Avatar for Paul Hiles Paul Hiles wrote on 17 Feb 2012

@Jp - Yes, it should work. There was an issue in earlier releases that stopped the child action from being rendered if the parent action did not use the DonutOutputCache attribute, but this has now been fixed. If you are still having issues, please report it on Codeplex and add some more detail.

Avatar for Nick Nick wrote on 20 Feb 2012

Got a problem with VaryByParam aswell..
[DonutOutputCache(Duration = 60, VaryByParam = "query")]
public ActionResult Index(string query)

Now it works with outputcache.
And fiddler even sees the query string changing.
But DonutOutputCache simply isn't seeing the change and sends the previous output.
Any fixes on this?

For that previous case your answer seemed to fix it. But that doesn't seem to be the case here right?

Avatar for Paul Hiles Paul Hiles wrote on 20 Feb 2012

@Nick - are you using the latest release (1.1.2) released 4 days ago?

If not, please update as there were several issues with previous versions that could be causing the behaviour you mention.

Avatar for nick nick wrote on 20 Feb 2012

Yea got that one.
Slight update.
It worked when I entered the query directly to the url and changed a route to this.

routes.MapRoute(
"Index", // Route name
"Home/{query}", // URL with parameters
new { controller = "Home", action = "Index" } // Parameter defaults
);

I should also point out that the parameter if not directly put in the url comes a form with a simple textbox and send button.

Avatar for Paul Hiles Paul Hiles wrote on 20 Feb 2012

@Nick - Is working fine for me in both cases. I presume your form is set up to do a GET rather than a POST, so the URL that the form is going to is something like:

/Home/Index?query=whatever

when you do not put a parameter in the route. Can you confirm and if so, check it caches ok if you go to the url?

Avatar for Nick Nick wrote on 20 Feb 2012

Caching works just fine. But only once :D, the first time. Inside the outputcachemanager there's only 1 value. The newer ones simply get discarded. This is without any routes thing. The query gets send with a value in it to the server (in fiddler).
And even with routing adjusted the form doesn't work only the input from the adress bar works. (this gets registered in the manager)

@using (Html.BeginForm("Index", "Home", FormMethod.Post))
{
@Html.TextBox("query")
<input type="submit" value="Send" />
}
This is the form.
Maybe the textbox value gets cached? But then again there should the same value as before in fiddler which isn't the case.

So much hassle for 1 simple partial view that does not to be cached from the page damn :D

Without parameters it gets cached, but the same thing happens as before... New querys from the form get discarded or ignored

Avatar for Paul Hiles Paul Hiles wrote on 22 Feb 2012

@Nick - OK I see the problem now. Your form is doing a POST rather than a GET so the behaviour when using the form will be very different from typing a URL (i.e. a GET) in the browser. Currently, MvcDonutCaching only uses route values and query string parameters when calculating a key for the cached content, so when posting the form, the key will be the same every time, despite posting different data. To be honest, I wasn't even aware that the built-in OutputCache also looked at POST parameters, but since you and another user have raised it, I will get this functionality added to the next version.

Avatar for Mahdi Taghizadeh Mahdi Taghizadeh wrote on 24 Feb 2012

What about an overload for Html.RenderAction helper? Html.Action doesn't do the job I need in my project.

Avatar for Adam Adam wrote on 12 Mar 2012

This looks great, but I too really need to use Html.RenderAction (rather than Html.Action), is this something you plan to support?

Avatar for Rory Rory wrote on 13 Mar 2012

I'm not a big MVC developer so please excuse
My question is: What is the advantage of this over say caching via AppFabric cache. This donut caching is in the same process as the MVC app so will die when the app is stopped.

Avatar for Paul Hiles Paul Hiles wrote on 14 Mar 2012

@Mahdi and Adam - I will try and get this in to the next release.

Avatar for Paul Hiles Paul Hiles wrote on 14 Mar 2012

MvcDonutCaching is extensible so whilst out of the box, it uses in-process caching (just as the built-in OutputCache does), you can easily move it out-of process. This is actually even easier than using the built-in caching support which makes it very difficult to move partial page caching out of process.

Regardless of whether you use inproc or out of process caching, you cannot do donut caching without third party support such as that provided by MvcDonutCaching. You can also benefit from some of the other features outlined above such as support for child action cache profiles and a robust cache invalidation API.

This article explains how to use MvcDonutCaching with AppFabric:

http://www.devtrends.co.uk/blog/asp.net-mvc-output-caching-with-windows-appfabric-cache

Avatar for Adam Adam wrote on 14 Mar 2012

Thanks Paul, much appreciated.

For the time being I'm testing it using Html.Action and although it works fine for the standard cache provider, my custom OutputCacheProvider (which is based heavily on this one - http://www.codeproject.com/Articles/182691/Using-Membase-for-ASP-NET-Output-Caching) doesn't work with your DonutOutputCache. It works with normal OutputCache though.

Any pointers?

Thanks

Adam

Avatar for Paul Hiles Paul Hiles wrote on 14 Mar 2012

@Adam - somebody reported a serialisation issue when using Memcached which will be fixed in next release. Don't know if you have a similar problem with your provider. If you can find an error message, you can report it on CodePlex and I will take a look.

http://mvcdonutcaching.codeplex.com/workitem/list/basic

Avatar for Adam Adam wrote on 15 Mar 2012

Thanks again Paul, I should have seen that myself.

In my scenario, I have a page which I want to be "live" (it has user bits on it) but it also contains various elements which I want cached. This scenario is possible with the normal OutputCache on child actions introduced in MVC3 but I want to use Membase as my cache store, hence I'm using your solution. In this scenario the donut hole wrapper comments are left in place which for my pages means quite a lot of extra data being transmitted over the wire, is there anyway to have these removed before rendering the content?

Thanks

Adam

Avatar for Ram Ram wrote on 15 Mar 2012

I use cache like this ( on a view) ( controller box action index)
[DonutOutputCache(Duration=TimeConsts.Hour,VaryByParam="BoxUid",VaryByCustom="Auth")]

when i do something i want to invalidate the cache so i follow the example and put this:
var cacheManager = new OutputCacheManager();
cacheManager.RemoveItem("Box", "Index", new { BoxUid = FileDto.BoxUid });

that didnt clear my cache ( on debug i see the one elemnt remove from the cache but the page still going to cahce and not to code)


without the route values it still dont remove cache

only when i put
cacheManager.RemoveItems();

the cache removed and the code in the controller triggered ( and this of course is no good ).

what am i doing wrong?
thanks

Avatar for Paul Hiles Paul Hiles wrote on 15 Mar 2012

@Ram - RemoveItem will only remove an item that exactly matches the values supplied. Because you have a VaryByCustom, this will not be the case.

In the current NuGet package, there is no (nice) way around this but I have already checked in an update to CodePlex that addresses this issue. The NuGet package will be updated within the next week. This will provide two options for you.

Firstly, if you want to remove a single item based on route value(s) and a VaryByCustom value, you will be able to do that.

cacheManager.RemoveItem("Box", "Index", new { BoxUid = FileDto.BoxUid, Auth = "whatever" });

where "whatever" is the value from your VaryByCustom method that you want to use.

Alternatively, if you want is to remove items from the cache based on a partial match (i.e. any page with a certain routeValue regardless of any other routeValues or varybycustom data), this is possible with a new RemoveItems overload:

cacheManager.RemoveItems("Box", "Index", new { BoxUid = FileDto.BoxUid });

Avatar for Paul Hiles Paul Hiles wrote on 15 Mar 2012

@Adam - if you are using MvcDonutCaching as a replacement for MVC3 child action caching then you do not need to use the Html.Action overloads (with the true final parameter). Just use the out of the box Html.Action methods and the donut placeholder comments will not be rendered.

Avatar for Adam Adam wrote on 16 Mar 2012

Thanks again Paul, that's sorted it!

Avatar for Lenny Lenny wrote on 21 Mar 2012

Hello,
I'm caching an action result using the DonutOutputCache attribute.
The question is, it is possible to not cache the action result according certain conditions?
For example if the user is not logged on then the action should not be executed and the cached output should be returned, but if the user is logged on the action should be executed in order to return a different output for each user (This output must not be cached, the action must be called always for logged in users).

Thanks.

Avatar for Brad Urani Brad Urani wrote on 18 Apr 2012

How could I generate a Guid every time a donut is cached so I could retrieve it in my global action filters and log it with every page view that renders that cached donut. I'd like to be able to correlate page views in my logs with the cached version of my donut so that if I accidently cache some pages with bad data in them I can see how it affects my traffic. Thanks!

Avatar for Jan Jan wrote on 20 Apr 2012

It works in IE and Chrome but it doesn't work for me in Firefox 11.0 and Safari.

Avatar for Sergii Sergii wrote on 23 Apr 2012

Hi
I have the same question as @Lenny

in the default OutputCache it is possible to use httpContext.Response.Cache.AddValidationCallback method for this purpose
Is there some alternative here?

Avatar for Hello Hello wrote on 25 Apr 2012

First of all, great job!. I do however have a question about updating the cache. I'm hecking to see if partial views are cached using the SQL profiler. if the page is cached, no sql statements are beeing profiled, which is good.

However, when the user updates an entity, changes are commited to the database and the specific cached view is removed from the cache. So far so good.

When the partial view is called SQL statements are profiled again since the view no longer is cached. So the user gets an updated partial view.

Now the problem I ran into is that after this, caching no longer seems to be working for any view / action / controller whatsoever. So it seems that as soon as i have called cacheManager.RemoveItem(Controller, action, routevalues), caching no longer seems to be working in my case.

if additional data is needed please let me know. for now I'm using:
1. cached profile with: varyByParam="*" location="Server" enabled="true"
2. the action result has two params (int entityId, string entityName)
3. I use cachemanager.RemoveItem(controllername, actionname, routedata) where routedata is the signature of the above action result method.

Thanks in advance.

Avatar for Paul Hiles Paul Hiles wrote on 26 Apr 2012

@Lenny and Sergii - not right now. MvcDonutCaching takes a different approach and allows you to always cache the page, but omit certain parts of the page that are user specific (donut holes). These holes will then get rendered differently for each user.

I am happy to add in this functionality though if you let me know your requirements and explain how your scenario does not fit with the standard donut hole approach.

Avatar for Paul Hiles Paul Hiles wrote on 26 Apr 2012

@Jan - if you are talking about the standard donut caching, I cannot see how the browser would make any difference - it is all server-side. If you are talking about Location or NoCache option, let me know and I will take a look.

Avatar for Sergii Sergii wrote on 27 Apr 2012

@Paul
Thank you!
> I am happy to add in this functionality though if you let me know your requirements and explain how your scenario does not fit with the standard donut hole approach.

What I need is ability to cache page (completely or with holes) or partial page for anonymous users - on my resource this is the most used functionality.
On the other hand If the user is logged in and have certain roles (i.e. Site Admin or Site Editor) then caching is forbidden because there are some admin buttons and information on the page.

If something was changed on a page (i.e. an article) by editor or by user then it is required to remove page form the cache for anonymous users - it will be recreated on next loading.

Avatar for Jan Jan wrote on 27 Apr 2012

@Paul,

I have Page1 and Page2 and they both contain:
@Html.Action("Index", "Account", true)
with 'true' parameter I specified the donut hole.

Index action in the Account controller doesn't get called in FireFox and Safari when navigating with back button from Page2 to Page1

Thanks for having a look.

Avatar for WillT WillT wrote on 10 Aug 2012

Would it be hard to add the ability to have conditional attributes when invalidating a cache? For instance you already have:

//remove a single cached action output (page 1 of List action)
cacheManager.RemoveItem("Home", "List", new { page = 1 });

But say I have a controller method that also takes in a categoryId.

ActionResult List(int categoryId, int page = 1) {
}

Say I update the category with the categoryId of 1. From here I want to invalidate all pages of the category that has the category of 1, but not all other categories. Make sense?

Avatar for Sydney Sydney wrote on 05 Sep 2012

This is a lot of work - thanks so much. Can this handle the following scenario:

[HttpPost]
[DonutOutputCache(xxx, VaryByParam(paramOne;paramTwo)]
public ActionMethod MyMethodWithThreeParameters(string paramOne, string paramTwo, string paramThree)

wherein the stuff that is cached is related to parameters one and two but paramThree needs to be set in an element on the page?

Normal output caching obviously loses paramThree if it is not included in the VaryByParam list, but this parameter is user input I need to handle.

Thanks!

Avatar for William William wrote on 28 Sep 2012

Guys, thanks for a great work! It sure made my job easier!

Avatar for Ronald Steen Ronald Steen wrote on 23 Oct 2012

Hi,

I'd like to use this project for a high availability and high traffic website. Would you advice me on doing that? Is the project ready for use in a production website?

Cheers,
Ronald

Avatar for Ram Ram wrote on 13 Dec 2012

Hi.

When using vary by custom with couple of variables (for example a=5;b=3)

with output cache manager RemoveItems never deletes this specific variables
when i put
new RouteValueDictionary(new { a= 5, b= 3 }

cheer Ram

Avatar for Paul HIles Paul HIles wrote on 13 Dec 2012

@Ram - get rid of the new RouteValueDictionary part and just use new { a= 5, b= 3 }

Avatar for Ram Ram wrote on 17 Dec 2012

Hi Paul.

Sorry but it doesn't work.

still see the cache key after removing it like you suggested
this is the cache key: #lang;user=en-us3#

for now i did this
var routeValueEn = new RouteValueDictionary();
routeValueEn.Add("lang;user", "en-us" + id);

and then the cache manager remove it.

btw on RemoveItems - there isnt an overload that takes an object this is why i choose the routevaluedictionary.

Thanks
Ram

Avatar for Ferson Ferson wrote on 17 Dec 2012

Hi.

We have controller name duplicates in our mvc3 website in different areas.
The index action of each is not duplicate in the cache.

There is any solution for this?

Thanks.

Avatar for Leke Leke wrote on 26 Mar 2013

I've currently got a custom outputcacheprovider working with donut caching and it stores the values in mongo.

How would I make use of the donut overloads as specified in your blog to remove and add items within the outputcacheprovider?

For example how can I use my custom outputcacheprovider to expire my index action result as below?

var cacheManager = new OutputCacheManager();
cacheManager.RemoveItem("Home", "Index");

Avatar for Sven Sven wrote on 12 Jun 2013

Is there any plan to support the OutputCacheParameters.VaryByHeader Property? Reference at msdn dot microsoft dot com/en-us/library/system.web.ui.outputcacheparameters.varybyheader.aspx

Comments are now closed for this article.