Working with the Output Cache and other Action Filters
Asked Answered
P

4

13

I have added Output Caching to a couple of actions in my app for some easy performance boosts. However, these actions also need to increment a counter after each request (it's a views counter) by hitting a Redis db.

At first, I figured I could just adjust the order in which the action filters execute to ensure the view is counted:

public class CountersAttribute : ActionFilterAttribute
{
    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        //increment my counter all clever like
        base.OnResultExecuted(filterContext);
    }
}

But that didn't work; apparently the OutputCacheAttribute doesn't behave like a normal action filter. Then I tried implementing a custom output cache:

public class OutputCacheWithCountersAttribute : OutputCacheAttribute
{
    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        //straight to the source to get my headcount!
        base.OnResultExecuted(filterContext);
    }
}

Nope, didn't work either; action filters appear to be entirely ignored once an action is cached. Bummer.

So, uh, is there any way (without implementing a custom output caching provider) for me to ensure my views are counted properly that is clean and sensible?

Palstave answered 12/6, 2012 at 3:52 Comment(0)
I
14

The OutputCacheAttribute has limitations by the way and there is a custom attribute named DonutOutputCache developed by Paul Hiles helps to overcome the limitations.

One of the important feature it supports is you can have an action filter that can be called all the times even the action is marked with cache attribute or not.

For ex. you want to cache an action for the duration 5 seconds and at the same time you want to log every time the action receives a request using a LogThis filter you can achieve that simply by below,

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

From Paul,

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.

Immovable answered 12/6, 2012 at 4:46 Comment(4)
This works wonderfully, except now my VaryByCustom param isn't working. GetVaryByCustomString() is called in globals, but Donut doesn't seem to honor the Response.Cache changes I made (namely, turning the cache off for authenticated users).Palstave
context.Response.Cache.SetCacheability(HttpCacheability.NoCache); context.Response.Cache.SetExpires(DateTime.Now.AddMinutes(-1)); context.Response.Cache.SetNoStore(); context.Response.Cache.SetNoServerCaching(); Those are my Response.Cache changes; Donut seems to ignore them and cache it all anyway.Palstave
I moved page views to an ajax call, instead of messing with VaryByCustom any more. However, the answer given is a great solution to this problem. Thank you :D.Palstave
@Palstave the problem with moving page views to a delayed AJAX call is that your metrics will get completely skewed. Users that hit the page and bounce (before the AJAX call finishes) will not be counted. Maybe you don't care about them in the first place... but then you'd lose insight on how your page load performance is affecting bounce rates (it can be VERY impactful these days ie: very low user tolerance to delays)Angst
T
1

You can make an AJAX call from the Layout View and track your visitors even if the page is cached. This is what Google Analytics does. I recommend doing it from the Layout View because it's gonna be executed in all the view that uses that layout. One more comment, let's say that you have two Layout Views: one for the public part of the site and one for the back-end (employees only). You'll probably be interested in tracking users, not employees so this is another benefit of tracking at Layout View. If in the future you want to track what the employees are doing, you can add a different tracker for the back-end Layout View. I hope it helps.

Thorazine answered 29/7, 2014 at 20:29 Comment(0)
D
0

The reason is actually in the .NET source, and nothing to do with the DonutOutputCache:

public void SetCacheability(HttpCacheability cacheability)
{
  if (cacheability < HttpCacheability.NoCache || HttpCacheability.ServerAndPrivate < cacheability)
    throw new ArgumentOutOfRangeException("cacheability");
  if (HttpCachePolicy.s_cacheabilityValues[(int) cacheability] >= HttpCachePolicy.s_cacheabilityValues[(int) this._cacheability])
    return;
  this.Dirtied();
  this._cacheability = cacheability;
}

In other words, if you set NoCache first (a value of 1), it will always return if you try to set a higher value, such as 4 (public).

The only solution is to fork the project and extend it to how you require, or perhaps send a pull request to mark protected ICacheHeadersHelper CacheHeadersHelper in DonutOutputCacheAttribute

Douzepers answered 25/3, 2013 at 21:59 Comment(0)
R
0

Use a "Validation Callback" that is executed ALWAYS even if the cached page should be served

public class MyCacheAttribute : OutputCacheAttribute
{
    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        SaveToLog();

        httpContext.Response.Cache.AddValidationCallback(MyCallback, null);

        base.OnResultExecuting(filterContext);
    }

    // This method is called each time when cached page is going to be served
    private void MyCallback(HttpContext context, object data, ref HttpValidationStatus validationStatus)
    {
        SaveToLog();
    }
}

NOTE: the SaveToLog() is called in two places, that's by design (first call when cache is bypassed, seconds call when cached version is served)

Russellrusset answered 4/11, 2018 at 17:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.