public class MyOutputCacheAttribute : ActionFilterAttribute { MemoryCacheDefault _cache = new MemoryCacheDefault(); /// <summary> /// 客戶端緩存(用戶代理)緩存相對過時時間 /// </summary> public int ClientCacheExpiration { set; get; } /// <summary> /// 服務端緩存過時時間 /// </summary> public int ServerCacheExpiration { set; get; } /// <summary> /// /// </summary> /// <param name="clientCacheExpiration">客戶端過時時間。單位爲秒,默認爲600秒(10分鐘)</param> /// <param name="cerverCacheExpiration">服務端過時時間。單位爲秒,默認爲7200秒(120分鐘)</param> public MyOutputCacheAttribute(int clientCacheExpiration = 600, int serverCacheExpiration = 7200) { this.ClientCacheExpiration = clientCacheExpiration; this.ServerCacheExpiration = serverCacheExpiration; } /// <summary> /// 斷定是否用緩存中的內容,仍是執行action是去取內容 /// 注:一旦給actionContext.Response賦值了,則會使用這個值來輸出響應 /// </summary> /// <param name="actionContext"></param> public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext) { // **************************************** 穿透緩存 ********************************************* // 若無緩存,則直接返回 string cacheKey = getCacheKey(actionContext); if (_cache.Contains(cacheKey) == false) return; if (_cache.Contains(cacheKey + ":etag") == false) return; // **************************************** 使用緩存 ********************************************* // 獲取緩存中的etag var etag = _cache.Get<string>(cacheKey + ":etag"); // 若etag沒有被改變,則返回304, if (actionContext.Request.Headers.IfNoneMatch.Any(x => x.Tag == etag)) //IfNoneMatch爲空時,返回的是一個集合對象 { actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.NotModified); // 響應對象尚未生成,須要先生成 // 304 not modified actionContext.Response.Headers.CacheControl = GetDefaultCacheControl(); actionContext.Response.Headers.ETag = new EntityTagHeaderValue(etag); return; } else //從緩存中返回響應 { actionContext.Response = actionContext.Request.CreateResponse(); // 響應對象尚未生成,須要先生成 // 設置協商緩存:etag actionContext.Response.Headers.ETag = new EntityTagHeaderValue(etag); //用緩存中的值(爲最新的)更新它 // 設置user agent的本地緩存 actionContext.Response.Headers.CacheControl = GetDefaultCacheControl(); // 從緩存中取中響應內容 var content = _cache.Get<byte[]>(cacheKey); actionContext.Response.Content = new ByteArrayContent(content); return; } } /// <summary> /// 將輸出保存在緩存中。 /// </summary> /// <param name="actionExecutedContext"></param> /// <param name="cancellationToken"></param> /// <returns></returns> public override async System.Threading.Tasks.Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, System.Threading.CancellationToken cancellationToken) { // 響應對象已經生成,能夠直接調用 actionExecutedContext.Response // 設置協商緩存:etag EntityTagHeaderValue etag = new EntityTagHeaderValue("\"" + Guid.NewGuid().ToString() + "\""); // 根據http協議, ETag的值必須用引號包含起來 actionExecutedContext.Response.Headers.ETag = etag; // 設置user agent的本地緩存 actionExecutedContext.Response.Headers.CacheControl = GetDefaultCacheControl(); // actionExecutedContext.Response.Headers.Remove("Content-Length"); // 改變了值,它會發生變化。刪除它的話,後續的程序會自動地再計算 // 保存到緩存 string cacheKey = getCacheKey(actionExecutedContext.ActionContext); var contentBytes = await actionExecutedContext.Response.Content.ReadAsByteArrayAsync(); _cache.Add(cacheKey + ":etag", etag.Tag, DateTimeOffset.Now.AddSeconds(this.ServerCacheExpiration)); _cache.Add(cacheKey, contentBytes, DateTimeOffset.Now.AddSeconds(this.ServerCacheExpiration)); } /// <summary> /// 默認的用戶代理本地緩存配置,10分鐘的相對過時時間 /// 對應響應header中的 Cache-Control /// 這裏設置裏面的子項max-age。如:Cache-Control: max-age=3600 /// </summary> /// <returns></returns> CacheControlHeaderValue GetDefaultCacheControl() { CacheControlHeaderValue control = new CacheControlHeaderValue(); control.MaxAge = TimeSpan.FromSeconds(this.ClientCacheExpiration); // 它對應響應頭中的 cacheControl :max-age=10項 return control; } /// <summary> /// /// </summary> /// <param name="actionContext"></param> /// <returns></returns> string getCacheKey(System.Web.Http.Controllers.HttpActionContext actionContext) { string cacheKey = null; cacheKey = actionContext.Request.RequestUri.PathAndQuery; // 路徑和查詢部分,如: /api/values/1?ee=33&oo=5 // 對路徑中的參數進行重排,升序排列 //string controllerName = actionContext.ControllerContext.ControllerDescriptor.ControllerName; //string actionName = actionContext.ActionDescriptor.ActionName; //if (actionContext.ActionArguments.ContainsKey("id") == false) //{ // cacheKey = controllerName + "/" + actionName; //} //else //{ // string id = actionContext.ActionArguments["id"].ToString(); // cacheKey = controllerName + "/" + actionName + "/" + id; //} return cacheKey; } }
上面的這段代碼嚴格遵循RFC2626中定義的緩存協議。redis
定義一個服務器端緩存實現api
這裏採用MemoryCache,也能夠採用memcached, redis之類的。緩存
public class MemoryCacheDefault { private static readonly MemoryCache _cache = MemoryCache.Default; public void RemoveStartsWith(string key) { lock (_cache) { _cache.Remove(key); } } public T Get<T>(string key) where T : class { var o = _cache.Get(key) as T; return o; } [Obsolete("Use Get<T> instead")] public object Get(string key) { return _cache.Get(key); } public void Remove(string key) { lock (_cache) { _cache.Remove(key); } } public bool Contains(string key) { return _cache.Contains(key); } public void Add(string key, object o, DateTimeOffset expiration, string dependsOnKey = null) { var cachePolicy = new CacheItemPolicy { AbsoluteExpiration = expiration }; if (!string.IsNullOrWhiteSpace(dependsOnKey)) { cachePolicy.ChangeMonitors.Add( _cache.CreateCacheEntryChangeMonitor(new[] { dependsOnKey }) ); } lock (_cache) { _cache.Add(key, o, cachePolicy); } } public IEnumerable<string> AllKeys { get { return _cache.Select(x => x.Key); } } }
將filter應用到action中服務器
public class ValuesController : ApiController { [MyOutputCache(10)] public string Get(int id) { return "value" + id.ToString(); } }