Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

mvc3 OutputCache RemoveOutputCacheItem RenderAction

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

like image 636
mb666 Avatar asked Mar 16 '11 13:03

mb666


2 Answers

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.
like image 130
Darin Dimitrov Avatar answered Nov 15 '22 04:11

Darin Dimitrov


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

like image 39
tvanfosson Avatar answered Nov 15 '22 04:11

tvanfosson