使用自定义 FilterAttribute 实现 MVC 输出缓存

在ASP.Net表单中,MVC有一些很方便的用 OutputCacheAttribute实现的缓存机制,然而用经典的的ASP.Net,我们很快认识到它在创建复杂系统时就捉襟见肘了。特别的,它在你的应用中经常会出现各种刷新或是清除缓存的事件。

举个列子,现在主菜单里有个给用户授权的'管理员'按钮,当你的管理员第一次浏览这个页面,系统将HTML缓存,其中也包括‘管理员’链接。如果你后来又取消了这个特权,尽管在技术上已经不在授权访问这个站点了,但网站仍然会通过缓存来提供特权服务的。

这样不好。

因此,我来折腾折腾并改进一下,我完成了属于自己的FilterAttribute。实现它的好处是你可以传递任何参数,就像是直接访问HttpContext,这样你就可以依次检查用户指定的数据,访问数据库等等你想要做的。


它是如何工作的

FilterAttribute类只包含了几个必要的方法,这几个方法都是覆盖了IResultFilter和IActionFilter类的方法。

  • OnActionExecuting.  这个方法在你的Action执行之前执行,通过检查缓存的值是否存在,当你的Action方法或者视图渲染方法中存在某些耗时较长的代码的时候,我们就可以放弃执行这部分代码。
  • OnResultExecuting.  这个方法在HTML页面就要被渲染到输出流之前执行。就在这里我们注入缓存内容(如果存在的话)。否则,我们就捕捉下一次响应的输出。

代码

我已经在下面提交了代码,这样大家就可以明白是怎么回事了。我不想写太多的细节,这样如果你想直接将这些代码拷贝/粘帖进你的工程是没有用的,根据命名空间的引用,你的工程是不会编译成功的。我同时用Microsoft Unity来进行依赖注入,所以不要让ICurrentUser把你搞混乱了。

最后,我还有个自定义缓存类,但我没有把源代码包含进来,你就直接拿你的缓存代码替换我空出的行就行了。

001 using System;
002 using System.Collections.Generic;
003 using System.IO;
004 using System.Linq;
005 using System.Text;
006 using System.Web;
007 using System.Web.Mvc;
008 using BlackBall.Common;
009 using BlackBall.Common.Localisation;
010 using BlackBall.Contracts.Cache;
011 using BlackBall.Contracts.Enums;
012 using BlackBall.Contracts.Exporting;
013 using BlackBall.Contracts.Localisation;
014 using BlackBall.Contracts.Security;
015 using BlackBall.Common.Extensions;
016 using BlackBall.Logic.Cache;
017  
018  
019 namespace BlackBall.MVC.Code.Mvc.Attributes
020 {
021     public class ResultOutputCachingAttribute : FilterAttribute, IResultFilter, IActionFilter
022     {
023  
024         #region Properties & Constructors
025  
026         private string ThisRequestOutput = "";
027         private bool VaryByUser = true;
028  
029         private ICurrentUser _CurrentUser = null;
030         private ICurrentUser CurrentUser
031         {
032             get
033             {
034                 if (_CurrentUser == null) _CurrentUser = Dependency.Resolve<ICurrentUser>();
035                 return _CurrentUser;
036             }
037         }
038  
039         public ResultOutputCachingAttribute(bool varyByUser = true)
040         {
041             this.VaryByUser = varyByUser;
042         }
043  
044         private string _CacheKey = null;
045         private string CacheKey
046         {
047             get return _CacheKey; }
048             set
049             {
050                 _CacheKey = value;
051             }
052         }
053  
054         #endregion
055  
056         /// <summary>
057         /// Queries the context and writes the HTML depending on which type of result we have (View, PartialView etc)
058         /// </summary>
059         /// <param name="filterContext"></param>
060         /// <returns></returns>
061         private void CacheResult(ResultExecutingContext filterContext)
062         {
063             using (var sw = new StringWriter())
064             {
065                 if (filterContext.Result is PartialViewResult)
066                 {
067                     var partialView = (PartialViewResult)filterContext.Result;
068                     var viewResult = ViewEngines.Engines.FindPartialView(filterContext.Controller.ControllerContext, partialView.ViewName);
069                     var viewContext = newViewContext(filterContext.Controller.ControllerContext, viewResult.View, filterContext.Controller.ViewData, filterContext.Controller.TempData, sw);
070                     viewResult.View.Render(viewContext, sw);
071                 }else if (filterContext.Result is ViewResult)
072                 {
073                     var partialView = (ViewResult)filterContext.Result;
074                     var viewResult = ViewEngines.Engines.FindView(filterContext.Controller.ControllerContext, partialView.ViewName, partialView.MasterName);
075                     var viewContext = newViewContext(filterContext.Controller.ControllerContext, viewResult.View, filterContext.Controller.ViewData, filterContext.Controller.TempData, sw);
076                     viewResult.View.Render(viewContext, sw);
077                 }
078                 var html = sw.GetStringBuilder().ToString();
079  
080                 // Add data to cache for next time
081                 if (!string.IsNullOrWhiteSpace(html))
082                 {
083                     var cache = new CacheManager<CachableString>();
084                     var cachedObject = newCachableString() { CacheKey = CreateKey(filterContext), Value = html };
085                     cachedObject.AddTag(CacheTags.Project, CurrentUser.CurrentProjectID);
086                     if(this.VaryByUser) cachedObject.AddTag(CacheTags.Person, this.CurrentUser.PersonID);
087                     cache.Save(cachedObject);
088                 }
089             }
090         }
091  
092  
093         /// <summary>
094         /// The result is beginning to execute
095         /// </summary>
096         /// <param name="filterContext"></param>
097         public void OnResultExecuting(ResultExecutingContext filterContext)
098         {
099             var cacheKey = CreateKey(filterContext);
100  
101             if (!string.IsNullOrWhiteSpace(this.ThisRequestOutput))
102             {
103                 filterContext.HttpContext.Response.Write("<!-- Cache start " + cacheKey + " -->");
104                 filterContext.HttpContext.Response.Write(this.ThisRequestOutput);
105                 filterContext.HttpContext.Response.Write("<!-- Cache end " + cacheKey + " -->");
106                 return;
107             }
108  
109             // Intercept the response and cache it
110             CacheResult(filterContext);
111         }
112  
113         /// <summary>
114         /// Action executing
115         /// </summary>
116         /// <param name="filterContext"></param>
117         public void OnActionExecuting(ActionExecutingContext filterContext)
118         {
119             // Break if no setting
120             if (!Configuration.Current.UseOutputCaching) return;
121  
122             // Our function returns nothing because the HTML is not calculated yet - that is done in another Filter
123             Func<string, CachableString> func = (ck) => new CachableString() { CacheKey = ck };
124  
125             // This is the earliest entry point into the action, so we check the cache before any code runs
126             var cache = new CacheManager<CachableString>();
127             var cacheKey = new CachableString() { CacheKey = CreateKey(filterContext) };
128             var cachedObject = cache.Load(cacheKey, func);
129             this.ThisRequestOutput = cachedObject.Value;
130  
131             // Cancel processing by setting result to some non-null value. Refer http://andrewlocatelliwoodcock.com/2011/12/15/canceling-the-actionexecutingcontext-in-the-onactionexecuting-actionfilter/
132             if (!string.IsNullOrWhiteSpace(this.ThisRequestOutput))
133             {
134                 filterContext.Result = new ContentResult();
135             }
136         }
137  
138         public void OnActionExecuted(ActionExecutedContext filterContext)
139         {
140  
141         }
142  
143         public void OnResultExecuted(ResultExecutedContext filterContext)
144         {
145  
146         }
147  
148         /// <summary>
149         /// Creates a unique key for this context
150         /// </summary>
151         /// <param name="filterContext"></param>
152         /// <returns></returns>
153         private string CreateKey(ControllerContext filterContext)
154         {
155              
156             // Append general info about the state of the system
157             var cacheKey = new StringBuilder();
158             cacheKey.Append(Configuration.Current.AssemblyVersion + "_");
159             if(this.VaryByUser) cacheKey.Append(this.CurrentUser.PersonID.GetValueOrDefault(0) + "_");
160  
161             // Append the controller name
162             cacheKey.Append(filterContext.Controller.GetType().FullName + "_");
163             if (filterContext.RouteData.Values.ContainsKey("action"))
164             {
165                 cacheKey.Append(filterContext.RouteData.Values["action"].ToString() + "_");
166             }
167  
168             // Add each parameter (if available)
169             foreach (var param in filterContext.RouteData.Values)
170             {
171                 cacheKey.Append((param.Key ?? "") + "-" + (param.Value == null "null": param.Value.ToString()) + "_");
172             }
173  
174             return cacheKey.ToString();
175         }
176     }
177 }
好了,希望这能帮到你-再也没有比HTML缓存能更好的创建出世界上最好的网站了。

你可能感兴趣的:(mvc,namespace,Microsoft,processing,缓存机制)