Search code examples
c#asp.net-mvccachingoutputcacheauthorize-attribute

.net mvc: custom authorizeattribute and customoutputcache provider


I wrote a custom outputcache provider that save the output on disk, it's work correctly, except in the actions decorated with the AuthorizeAttribute.
Looking on the problem, the solution seems to have a custom AuthorizeAttribute that manage the cache.
Then I've added my custom AuthorizeAttribute, but unfortunately, I'm getting the error
"When using a custom output cache provider like 'FileCacheProvider', only the following expiration policies and cache features are supported: file dependencies, absolute expirations, static validation callbacks and static substitution callbacks."

The code:

The custom OutputCache provider (FileCacheProvider)

public class FileCacheProvider : OutputCacheProvider
{

    public string CacheLocation
    {
        get
        {
            if (ConfigurationManager.AppSettings["FileCacheLocationRelativePath"] == null)
            {
                throw new ApplicationException("The FileCacheLocationRelativePath AppSettings key is not configured.");
            }
            string strCacheLocation = ConfigurationManager.AppSettings["FileCacheLocationRelativePath"];
            strCacheLocation = HttpContext.Current.Server.MapPath(strCacheLocation);
            return strCacheLocation + @"\";
        }
    }

    public override object Add(string key, object entry, DateTime utcExpiry)
    {
        object obj = this.Get(key);
        if (obj != null)
        {
            return obj;
        }
        else
        {
            this.Set(key, entry, utcExpiry);
            return entry;
        }
    }


    public override void Remove(string key)
    {
        string filePath = GetFullPathForKey(key);
        if (File.Exists(filePath))
        {
            File.Delete(filePath);
        }
    }


    public override object Get(string key)
    {
        string filePath = GetFullPathForKey(key);
        if (!File.Exists(filePath))
        {
            return null;
        }
        CacheItem item = null;
        FileStream fileStream = File.OpenRead(filePath);
        BinaryFormatter formatter = new BinaryFormatter();
        item = (CacheItem)formatter.Deserialize(fileStream);
        fileStream.Close();
        if (item == null || item.Expiry <= DateTime.UtcNow)
        {
            Remove(key);
            return null;
        }
        return item.Item;
    }


    public override void Set(string key, object entry, DateTime utcExpiry)
    {
        string filePath = GetFullPathForKey(key);
        CacheItem item = new CacheItem { Expiry = utcExpiry, Item = entry };
        FileStream fileStream = File.OpenWrite(filePath);
        BinaryFormatter formatter = new BinaryFormatter();
        formatter.Serialize(fileStream, item);
        fileStream.Close();
    }

    private string GetFullPathForKey(string key)
    {
        string temp = key.Replace('/', '$');
        return CacheLocation + temp;
    }
}


[Serializable]
public class CacheItem
{
    public object Item { get; set; }
    public DateTime Expiry { get; set; }
}

The custom AuthorizeAttribute (DFAuthorizeAttribute)

[AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class DFAuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
{


    private AuthenticationManager authentication = new AuthenticationManager();


    protected void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
    {
        validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
    }

    protected void SetCachePolicy(AuthorizationContext filterContext)
    {
        HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
        cachePolicy.SetProxyMaxAge(new TimeSpan(0));
        cachePolicy.AddValidationCallback(CacheValidateHandler, null);
    }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        if (filterContext == null)
        {
            throw new ArgumentNullException("filterContext");
        }

        if (AuthorizeCore(filterContext.HttpContext))
        {
            SetCachePolicy(filterContext);
        }
        else if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
        {
            // auth failed, redirect to login page
            filterContext.Result = new HttpUnauthorizedResult();
        }
        else if (authentication != null || authentication.AuthenticationData != null)
        {
            SetCachePolicy(filterContext);
        }
        else
        {
            ViewDataDictionary viewData = new ViewDataDictionary();
            viewData.Add("Message", "You do not have sufficient privileges for this operation.");
            filterContext.Result = new ViewResult { ViewName = "Error", ViewData = viewData };
        }

    }


}

Web.config

<caching>
  <outputCache defaultProvider="FileCacheProvider">
    <providers>
      <add name="FileCacheProvider" type="MyNameSpace.FileCacheProvider"/>
    </providers>
  </outputCache>
  <outputCacheSettings>
    <outputCacheProfiles>
      <add name="Index" duration="3600" />
    </outputCacheProfiles>
  </outputCacheSettings>
</caching>

Action

[OutputCache(CacheProfile = "Index")]
[MyNameSpace.DFAuthorize]
public ActionResult Index(string pageId)
{
    ....
}

Any help will be appreciated


Solution

  • The problem is due to the fact that your CacheValidateHandler is not a static method. I tested it and if you comment its contents and change it to static, the error goes away.

    However, it also doesn't hit a breakpoint in the method when you do it that way, so I don't think that is a workable solution.

    There seems to be a lot of discussion about it on this thread, but there doesn't seem to be any true answer there.

    I believe that customized output caching was not designed to be used in conjunction with AuthorizeAttribute or this is an MVC bug of some kind. Keep in mind that MVC is much newer than OutputCache (which was from .NET 2.0), so this is probably just an incompatibility that couldn't be worked out without introducing breaking API changes. If you feel this is important enough, you could report this to the MVC team.

    However, IMHO, you should just use the System.Runtime.Caching.ObjectCache abstract class which also can be extended to be file-based rather than output cache to handle this scenario. It doesn't actually cache the page content (you would just cache chunks of data), but it can still prevent round-trips to the database if that is what you are attempting to solve.

    Do note that you can still use your FileCacheProvider for public pages that are not behind a login, but every action that requires AuthorizeAttribute should use the System.Runtime.Caching provider instead. Also, it is a bit of an unusual situation to cache pages that are behind a login because they tend to require a live view of the data most of the time.