How to turn output caching off for authenticated users in ASP.NET MVC?
Asked Answered
C

3

16

I have an ASP.NET MVC application. I need to cache some pages however only for non-authenticated users.

I've tried to use VaryByCustom="user" with the following GetVaryByCustomString implementation:

public override string GetVaryByCustomString(HttpContext context, string custom)
{
  if (custom == "user")
  {
      if (context.User.Identity.IsAuthenticated)
      {
        return context.User.Identity.Name;
      }
      else
      {
        return "";
      }
  }  

  return base.GetVaryByCustomString(context, custom);
}

However this isn't exactly what I need because pages are still cached. Only difference is that now is cached for each user separately.

One possible solution is to return Guid.NewGuid() each time when user is Authenticated, but it looks like a huge waste of resources to me.

So do you have any tips for me?

Cycling answered 21/1, 2010 at 14:31 Comment(0)
C
32

So here is what I done:

public class NonAuthenticatedOnlyCacheAttribute : OutputCacheAttribute
{
    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
      var httpContext = filterContext.HttpContext;

      if (httpContext.User.Identity.IsAuthenticated)
      {
        // it's crucial not to cache Authenticated content
        Location = OutputCacheLocation.None;
      }

      // this smells a little but it works
      httpContext.Response.Cache.AddValidationCallback(IgnoreAuthenticated, null);

      base.OnResultExecuting(filterContext);
    }

    // This method is called each time when cached page is going to be
    // served and ensures that cache is ignored for authenticated users.
    private void IgnoreAuthenticated(HttpContext context, object data, ref HttpValidationStatus validationStatus)
    {
      if (context.User.Identity.IsAuthenticated)            
        validationStatus = HttpValidationStatus.IgnoreThisRequest;          
      else          
        validationStatus = HttpValidationStatus.Valid;          
    }
}

Many thanks to Craig Stuntz who pointed me to correct direction and whose answer I unwittingly downvoted.

Cycling answered 22/1, 2010 at 18:18 Comment(5)
Interesting - have you had any problems with this method since this post (1 year ago)? ThanksPastry
@UpTheCreek: We use slightly more complicated version of this code in our product. Obviously I guarantee nothing but in my experience it works.Unmask
It works great for Pages but unfortunately doesn't work for Partial ViewsEpiboly
-1. This won't work. Once this logic executes for an authenticated request, all successive requests will have no caching. Remember, the attribute is a single instance that lives for the lifetime of the AppDomain, across all HttpApplication instances. To alter caching behavior from a FilterAttribute per request, you have to re-implement the logic in base.OnResultExecuting to supply a separate instance to Page.InitOutputCache. See the source.Tarnish
gWiz is right -- once you set the location in that IsAuthenticated check, all subsequent requests won't be cached. To fix this, cache the original location like so: https://mcmap.net/q/715147/-how-to-turn-output-caching-off-for-authenticated-users-in-asp-net-mvc and then use that for the unauthed requests.Dungaree
G
12

Attributes in general are cached, then you need to store original Location. If you access the page Logged, it set Location to None, then when you access as anonymous, it still None.

public class AuthenticatedOnServerCacheAttribute : OutputCacheAttribute
{
    private OutputCacheLocation? originalLocation;

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        var httpContext = filterContext.HttpContext;

        if (httpContext.User.Identity.IsAuthenticated)
        {
            originalLocation = originalLocation ?? Location;
            Location = OutputCacheLocation.None;
        }
        else
        {
            Location = originalLocation ?? Location;
        }

        base.OnResultExecuting(filterContext);
    }
}
Gabriel answered 14/3, 2012 at 2:3 Comment(1)
..but you still need the "IgnoreAuthenticated" part from the accepted answerIleac
E
2

The accepted answer is correct but it doesn't work for caching in this way Partial views. I have combined both variants: GetVaryByCustomString and set Duration to the minimum - for Partial Views and AddValidationCallback method for pages. Actually it is possible to use only the first method but the second one looks not such expensive - it doesn't call OnResultExecuting each time but only registered handler.

So the custom cache attribute class

public class CacheAttribute : OutputCacheAttribute
{   

    public CacheAttribute()
    {
      Duration = 300;  /*default cache time*/
    }

    private bool _partialView;

    /// <summary>
    /// Set true if Partial view is cached
    /// </summary>
    public bool PartialView
    {
      get { return _partialView; }
      set
      {
        _partialView = value;
        if ( _partialView ) {
          VaryByCustom = "Auth";
        }
      }
    }

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        if ( PartialView ) OnCachePartialEnabled( filterContext );
        else OnCacheEnabled(filterContext);

        base.OnResultExecuting( filterContext );     
    }

    private OutputCacheLocation? originalLocation;
    private int? _prevDuration;
    protected void OnCachePartialEnabled(ResultExecutingContext filterContext)
    {
      var httpContext = filterContext.HttpContext;

      if ( !_prevDuration.HasValue) _prevDuration = Duration;
      Duration = httpContext.User.Identity.IsAuthenticated ? 1 : _prevDuration.Value;
    }

    protected void OnCacheEnabled(ResultExecutingContext filterContext)
    {
      var httpContext = filterContext.HttpContext;

      if ( httpContext.User.Identity.IsAuthenticated ) {
        // it's crucial not to cache Authenticated content
        originalLocation = originalLocation ?? Location;
        Location = OutputCacheLocation.None;
      }
      else {
      Location = originalLocation ?? Location;
    }

      // this smells a little but it works
      httpContext.Response.Cache.AddValidationCallback( IgnoreAuthenticated, null );      
    }

    // This method is called each time when cached page is going to be
    // served and ensures that cache is ignored for authenticated users.
    private void IgnoreAuthenticated(HttpContext context, object data, ref HttpValidationStatus validationStatus)
    {
      validationStatus = context.User.Identity.IsAuthenticated 
        ? HttpValidationStatus.IgnoreThisRequest 
        : HttpValidationStatus.Valid;
    }
}

Override GetVaryByCustomString method in Global.asax.cs

public override string GetVaryByCustomString(HttpContext context, string custom)
{ 
    if ( custom == "Auth" ) {
      //do not cache when user is authenticated
      if ( context.User.Identity.IsAuthenticated ) {
        return base.GetVaryByCustomString( context, custom );
      }
      return "NotAuth";
    }     
    return base.GetVaryByCustomString( context, custom );
}

Use it like this:

[Cache]
public virtual ActionResult Index()
{
    return PartialView();
}

[ChildActionOnly, Cache(PartialView=true)]
public virtual ActionResult IndexPartial()
{
    return PartialView();
}

Updated: I have also added Fujiy's fix here

Epiboly answered 25/4, 2012 at 13:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.