mvc3 OutputCache RemoveOutputCacheItem RenderAction
Asked Answered
S

2

7

I did my research but haven't found any answers.

I'm using Html.RenderAction in a masterpage ( to render page header with links specific to user permissions ). Action is decorated with OutputCache, returns partial control and gets cached as expected.

When the event happens ( let's say permissions are changed ) I want to programmatically invalidate cached partial control.

I'm trying to use RemoveOutputCacheItem method. It takes a path as a parameter. I'm trying to set the path to the action used in Html.RenderAction. That doesn't invalidate the action.

How can I programmatically invalidate the action?

Thanks

Stollings answered 16/3, 2011 at 13:45 Comment(0)
B
9

The cache for child actions is stored in the OutputCacheAttribute.ChildActionCache property. The problem is that the API generating ids for child actions and storing them in this object is not public (WHY Microsoft??). So if you try to loop through the objects in this collection you will discover that it will also contain the cached value for your child action but you won't be able to identify it unless you reverse engineer the algorithm being used to generate keys which looks something like this (as seen with Reflector):

internal string GetChildActionUniqueId(ActionExecutingContext filterContext)
{
    StringBuilder builder = new StringBuilder();
    builder.Append("_MvcChildActionCache_");
    builder.Append(filterContext.ActionDescriptor.UniqueId);
    builder.Append(DescriptorUtil.CreateUniqueId(new object[] { this.VaryByCustom }));
    if (!string.IsNullOrEmpty(this.VaryByCustom))
    {
        string varyByCustomString = filterContext.HttpContext.ApplicationInstance.GetVaryByCustomString(HttpContext.Current, this.VaryByCustom);
        builder.Append(varyByCustomString);
    }
    builder.Append(GetUniqueIdFromActionParameters(filterContext, SplitVaryByParam(this.VaryByParam)));
    using (SHA256 sha = SHA256.Create())
    {
        return Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(builder.ToString())));
    }
}

So you could perform the following madness:

public ActionResult Invalidate()
{
    OutputCacheAttribute.ChildActionCache = new MemoryCache("NewDefault");
    return View();
}

which obviously will invalidate all cached child actions which might not be what you are looking for but I am afraid is the only way other than of course reverse engineering the key generation :-).

@Microsoft, please, I am begging you for ASP.NET MVC 4.0:

  1. introduce the possibility to do donut caching in addition to donut hole caching
  2. introduce the possibility to easily expire the result of a cached controller action (something more MVCish than Response.RemoveOutputCacheItem)
  3. introduce the possibility to easily expire the result of a cached child action
  4. if you do 1. then obviously introduce the possibility to expire the cached donut portion.
Branching answered 16/3, 2011 at 14:17 Comment(4)
How can I loop through the collection you mentioned I wonder?Stollings
@user662589, using a foreach loop: foreach (var item in OutputCacheAttribute.ChildActionCache) { ... }.Branching
@Darin - Your prayers have been answered :-) Points 1-4 are all covered by open source MvcDonutCaching NuGet package. devtrends.co.uk/blog/donut-output-caching-in-asp.net-mvc-3Target
@TheFlowerGuy: post that link as an answer. I found it here by mere chance! it deserves to be an answer!Lipetsk
A
2

You might want to approach this a different way. You could create a custom AuthorizeAttribute -- it would simply allow everyone -- and add override the OnCacheValidation method to incorporate your logic. If the base OnCacheValidation returns HttpValidationStatus.Valid, then make your check to see if the state has changed and if so, return HttpValidationStatus.Invalid instead.

public class PermissionsChangeValidationAttribute : AuthorizeAttribute
{
     public override OnAuthorization( AuthorizationContext filterContext )
     {
        base.OnAuthorization( filterContext );
     }

     public override HttpValidationStatus OnCacheAuthorization( HttpContextBase httpContext )
     {
         var status = base.OnCacheAuthorization( httpContext );
         if (status == HttpValidationStatus.Valid)
         {
            ... check if the permissions have changed somehow
            if (changed)
            {
                status = HttpValidationStatus.Invalid;
            }
         }
         return status;
     }
}

Note that there are ways to pass additional data in the cache validation process if you need to track the previous state, but you'd have to replicate some code in the base class and add your own custom cache validation handler. You can mine some ideas on how to do this from my blog post on creating a custom authorize attribute: http://farm-fresh-code.blogspot.com/2011/03/revisiting-custom-authorization-in.html

Alpaca answered 16/3, 2011 at 14:29 Comment(6)
Sounds like a solution. But it's quite ugly since it forces me to store the list of events somewhere and then query the list from here to optionally invalidate the HttpValidationStatus...Stollings
@user - if possible I'd store the conditions under which the item was cached, then simply check if those conditions have changed. Look at the SetCachePolicy call in my blog post. The extra data parameter could be used to store the current conditions. You'd need a CacheValidationHandler that could unpack these and compare them to the current conditions when a cache request is made.Alpaca
@user -- you might also want to see if you could make the VaryByCustom property on the OutputCacheAttribute work for you. Perhaps have a method that produces a unique value for each set of conditions?Alpaca
I use VaryByCustom to return UserId.Stollings
@user - can you extend it to encapsulate other things that might change? Say a string that encapsulates the user's id and their permissions? "134:EditUser:ViewUser:DeleteUser" -- then if any of the permissions change or it's a different user you get an uncached page.Alpaca
Yes I will probably have to go that way. Hopefully this will be resolved in the MVC framework relatively soon because it's a great pain at the moment...Stollings

© 2022 - 2024 — McMap. All rights reserved.