web api 缓存类

  1 using System;
  2 using System.Collections.Generic;
  3 using System.Linq;
  4 using System.Text;
  5 using System.Web.Http.Filters;
  6 using System.Net.Http.Headers;
  7 using System.Net.Http;
  8 using System.Threading;
  9 using System.Net.Http.Formatting;
 10 using System.Web.Http.Controllers;
 11 using System.Linq.Expressions;
 12 using System.Reflection;
 13 using System.Threading.Tasks;
 14 using System.Collections;
 15 using System.Net;
 16 using System.Runtime.Caching;
 17 using System.Web.Http;
 18 
 19 
 20 
 21 namespace Cashlibary
 22 {
 23     public class CacheOutputAttribute : ActionFilterAttribute
 24     {
 25         private const string CurrentRequestMediaType = "CacheOutput:CurrentRequestMediaType";
 26         protected static MediaTypeHeaderValue DefaultMediaType = new MediaTypeHeaderValue("application/json") { CharSet = Encoding.UTF8.HeaderName };
 27 
 28         /// <summary>
 29         /// Cache enabled only for requests when Thread.CurrentPrincipal is not set
 30         /// </summary>
 31         public bool AnonymousOnly { get; set; }
 32 
 33         /// <summary>
 34         /// Corresponds to MustRevalidate HTTP header - indicates whether the origin server requires revalidation of a cache entry on any subsequent use when the cache entry becomes stale
 35         /// </summary>
 36         public bool MustRevalidate { get; set; }
 37 
 38         /// <summary>
 39         /// Do not vary cache by querystring values
 40         /// </summary>
 41         public bool ExcludeQueryStringFromCacheKey { get; set; }
 42 
 43         /// <summary>
 44         /// How long response should be cached on the server side (in seconds)
 45         /// </summary>
 46         public int ServerTimeSpan { get; set; }
 47 
 48         /// <summary>
 49         /// Corresponds to CacheControl MaxAge HTTP header (in seconds)
 50         /// </summary>
 51         public int ClientTimeSpan { get; set; }
 52 
 53         /// <summary>
 54         /// Corresponds to CacheControl NoCache HTTP header
 55         /// </summary>
 56         public bool NoCache { get; set; }
 57 
 58         /// <summary>
 59         /// Corresponds to CacheControl Private HTTP header. Response can be cached by browser but not by intermediary cache
 60         /// </summary>
 61         public bool Private { get; set; }
 62 
 63         /// <summary>
 64         /// Class used to generate caching keys
 65         /// </summary>
 66         public Type CacheKeyGenerator { get; set; }
 67 
 68         // cache repository
 69         private IApiOutputCache _webApiCache;
 70 
 71         protected virtual void EnsureCache(HttpConfiguration config, HttpRequestMessage req)
 72         {
 73             _webApiCache = config.CacheOutputConfiguration().GetCacheOutputProvider(req);
 74         }
 75 
 76         internal IModelQuery<DateTime, CacheTime> CacheTimeQuery;
 77 
 78         protected virtual bool IsCachingAllowed(HttpActionContext actionContext, bool anonymousOnly)
 79         {
 80             if (anonymousOnly)
 81             {
 82                 if (Thread.CurrentPrincipal.Identity.IsAuthenticated)
 83                 {
 84                     return false;
 85                 }
 86             }
 87 
 88             if (actionContext.ActionDescriptor.GetCustomAttributes<IgnoreCacheOutputAttribute>().Any())
 89             {
 90                 return false;
 91             }
 92 
 93             return actionContext.Request.Method == HttpMethod.Post;
 94         }
 95 
 96         protected virtual void EnsureCacheTimeQuery()
 97         {
 98             if (CacheTimeQuery == null) ResetCacheTimeQuery();
 99         }
100 
101         protected void ResetCacheTimeQuery()
102         {
103             CacheTimeQuery = new ShortTime(ServerTimeSpan, ClientTimeSpan);
104         }
105 
106         protected virtual MediaTypeHeaderValue GetExpectedMediaType(HttpConfiguration config, HttpActionContext actionContext)
107         {
108             MediaTypeHeaderValue responseMediaType = null;
109 
110             var negotiator = config.Services.GetService(typeof(IContentNegotiator)) as IContentNegotiator;
111             var returnType = actionContext.ActionDescriptor.ReturnType;
112 
113             if (negotiator != null && returnType != typeof(HttpResponseMessage) && (returnType != typeof(IHttpActionResult) || typeof(IHttpActionResult).IsAssignableFrom(returnType)))
114             {
115                 var negotiatedResult = negotiator.Negotiate(returnType, actionContext.Request, config.Formatters);
116 
117                 if (negotiatedResult == null)
118                 {
119                     return DefaultMediaType;
120                 }
121 
122                 responseMediaType = negotiatedResult.MediaType;
123                 if (string.IsNullOrWhiteSpace(responseMediaType.CharSet))
124                 {
125                     responseMediaType.CharSet = Encoding.UTF8.HeaderName;
126                 }
127             }
128             else
129             {
130                 if (actionContext.Request.Headers.Accept != null)
131                 {
132                     responseMediaType = actionContext.Request.Headers.Accept.FirstOrDefault();
133                     if (responseMediaType == null || !config.Formatters.Any(x => x.SupportedMediaTypes.Contains(responseMediaType)))
134                     {
135                         return DefaultMediaType;
136                     }
137                 }
138             }
139 
140             return responseMediaType;
141         }
142 
143         public override void OnActionExecuting(HttpActionContext actionContext)
144         {
145             if (actionContext == null) throw new ArgumentNullException("actionContext");
146 
147             if (!IsCachingAllowed(actionContext, AnonymousOnly)) return;
148 
149             var config = actionContext.Request.GetConfiguration();
150 
151             EnsureCacheTimeQuery();
152             EnsureCache(config, actionContext.Request);
153 
154             var cacheKeyGenerator = config.CacheOutputConfiguration().GetCacheKeyGenerator(actionContext.Request, CacheKeyGenerator);
155 
156             var responseMediaType = GetExpectedMediaType(config, actionContext);
157             actionContext.Request.Properties[CurrentRequestMediaType] = responseMediaType;
158             var cachekey = cacheKeyGenerator.MakeCacheKey(actionContext, responseMediaType, ExcludeQueryStringFromCacheKey);
159 
160             if (!_webApiCache.Contains(cachekey)) return;
161 
162             if (actionContext.Request.Headers.IfNoneMatch != null)
163             {
164                 var etag = _webApiCache.Get<string>(cachekey + Constants.EtagKey);
165                 if (etag != null)
166                 {
167                     if (actionContext.Request.Headers.IfNoneMatch.Any(x => x.Tag == etag))
168                     {
169                         var time = CacheTimeQuery.Execute(DateTime.Now);
170                         var quickResponse = actionContext.Request.CreateResponse(HttpStatusCode.NotModified);
171                         ApplyCacheHeaders(quickResponse, time);
172                         actionContext.Response = quickResponse;
173                         return;
174                     }
175                 }
176             }
177 
178             var val = _webApiCache.Get<byte[]>(cachekey);
179             if (val == null) return;
180 
181             var contenttype = _webApiCache.Get<MediaTypeHeaderValue>(cachekey + Constants.ContentTypeKey) ?? new MediaTypeHeaderValue(cachekey.Split(new[] { ':' }, 2)[1].Split(';')[0]);
182 
183             actionContext.Response = actionContext.Request.CreateResponse();
184             actionContext.Response.Content = new ByteArrayContent(val);
185 
186             actionContext.Response.Content.Headers.ContentType = contenttype;
187             var responseEtag = _webApiCache.Get<string>(cachekey + Constants.EtagKey);
188             if (responseEtag != null) SetEtag(actionContext.Response, responseEtag);
189 
190             var cacheTime = CacheTimeQuery.Execute(DateTime.Now);
191             ApplyCacheHeaders(actionContext.Response, cacheTime);
192         }
193 
194         public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
195         {
196             if (actionExecutedContext.ActionContext.Response == null || !actionExecutedContext.ActionContext.Response.IsSuccessStatusCode) return;
197 
198             if (!IsCachingAllowed(actionExecutedContext.ActionContext, AnonymousOnly)) return;
199 
200             var cacheTime = CacheTimeQuery.Execute(DateTime.Now);
201             if (cacheTime.AbsoluteExpiration > DateTime.Now)
202             {
203                 var httpConfig = actionExecutedContext.Request.GetConfiguration();
204                 var config = httpConfig.CacheOutputConfiguration();
205                 var cacheKeyGenerator = config.GetCacheKeyGenerator(actionExecutedContext.Request, CacheKeyGenerator);
206 
207                 var responseMediaType = actionExecutedContext.Request.Properties[CurrentRequestMediaType] as MediaTypeHeaderValue ?? GetExpectedMediaType(httpConfig, actionExecutedContext.ActionContext);
208                 var cachekey = cacheKeyGenerator.MakeCacheKey(actionExecutedContext.ActionContext, responseMediaType, ExcludeQueryStringFromCacheKey);
209 
210                 if (!string.IsNullOrWhiteSpace(cachekey) && !(_webApiCache.Contains(cachekey)))
211                 {
212                     SetEtag(actionExecutedContext.Response, CreateEtag(actionExecutedContext, cachekey, cacheTime));
213 
214                     var responseContent = actionExecutedContext.Response.Content;
215 
216                     if (responseContent != null)
217                     {
218                         var baseKey = config.MakeBaseCachekey(actionExecutedContext.ActionContext.ControllerContext.ControllerDescriptor.ControllerType.FullName, actionExecutedContext.ActionContext.ActionDescriptor.ActionName);
219                         var contentType = responseContent.Headers.ContentType;
220                         string etag = actionExecutedContext.Response.Headers.ETag.Tag;
221                         //ConfigureAwait false to avoid deadlocks
222                         var content = await responseContent.ReadAsByteArrayAsync().ConfigureAwait(false);
223 
224                         responseContent.Headers.Remove("Content-Length");
225 
226                         _webApiCache.Add(baseKey, string.Empty, cacheTime.AbsoluteExpiration);
227                         _webApiCache.Add(cachekey, content, cacheTime.AbsoluteExpiration, baseKey);
228 
229 
230                         _webApiCache.Add(cachekey + Constants.ContentTypeKey,
231                                         contentType,
232                                         cacheTime.AbsoluteExpiration, baseKey);
233 
234 
235                         _webApiCache.Add(cachekey + Constants.EtagKey,
236                                         etag,
237                                         cacheTime.AbsoluteExpiration, baseKey);
238                     }
239                 }
240             }
241 
242             ApplyCacheHeaders(actionExecutedContext.ActionContext.Response, cacheTime);
243         }
244 
245         protected virtual void ApplyCacheHeaders(HttpResponseMessage response, CacheTime cacheTime)
246         {
247             if (cacheTime.ClientTimeSpan > TimeSpan.Zero || MustRevalidate || Private)
248             {
249                 var cachecontrol = new CacheControlHeaderValue
250                 {
251                     MaxAge = cacheTime.ClientTimeSpan,
252                     MustRevalidate = MustRevalidate,
253                     Private = Private
254                 };
255 
256                 response.Headers.CacheControl = cachecontrol;
257             }
258             else if (NoCache)
259             {
260                 response.Headers.CacheControl = new CacheControlHeaderValue { NoCache = true };
261                 response.Headers.Add("Pragma", "no-cache");
262             }
263         }
264 
265         protected virtual string CreateEtag(HttpActionExecutedContext actionExecutedContext, string cachekey, CacheTime cacheTime)
266         {
267             return Guid.NewGuid().ToString();
268         }
269 
270         private static void SetEtag(HttpResponseMessage message, string etag)
271         {
272             if (etag != null)
273             {
274                 var eTag = new EntityTagHeaderValue(@"""" + etag.Replace(""", string.Empty) + @"""");
275                 message.Headers.ETag = eTag;
276             }
277         }
278     }
279 
280     public interface ICacheKeyGenerator
281     {
282         string MakeCacheKey(HttpActionContext context, MediaTypeHeaderValue mediaType, bool excludeQueryString = false);
283     }
284     public sealed class Constants
285     {
286         public const string ContentTypeKey = ":response-ct";
287         public const string EtagKey = ":response-etag";
288     }
289 
290     public class DefaultCacheKeyGenerator : ICacheKeyGenerator
291     {
292         public virtual string MakeCacheKey(HttpActionContext context, MediaTypeHeaderValue mediaType, bool excludeQueryString = false)
293         {
294             var controller = context.ControllerContext.ControllerDescriptor.ControllerType.FullName;
295             var action = context.ActionDescriptor.ActionName;
296             var key = context.Request.GetConfiguration().CacheOutputConfiguration().MakeBaseCachekey(controller, action);
297             var actionParameters = context.ActionArguments.Where(x => x.Value != null).Select(x => x.Key + "=" + GetValue(x.Value));
298 
299             string parameters;
300 
301             if (!excludeQueryString)
302             {
303                 var queryStringParameters =
304                     context.Request.GetQueryNameValuePairs()
305                            .Where(x => x.Key.ToLower() != "callback")
306                            .Select(x => x.Key + "=" + x.Value);
307                 var parametersCollections = actionParameters.Union(queryStringParameters);
308                 parameters = "-" + string.Join("&", parametersCollections);
309 
310                 var callbackValue = GetJsonpCallback(context.Request);
311                 if (!string.IsNullOrWhiteSpace(callbackValue))
312                 {
313                     var callback = "callback=" + callbackValue;
314                     if (parameters.Contains("&" + callback)) parameters = parameters.Replace("&" + callback, string.Empty);
315                     if (parameters.Contains(callback + "&")) parameters = parameters.Replace(callback + "&", string.Empty);
316                     if (parameters.Contains("-" + callback)) parameters = parameters.Replace("-" + callback, string.Empty);
317                     if (parameters.EndsWith("&")) parameters = parameters.TrimEnd('&');
318                 }
319             }
320             else
321             {
322                 parameters = "-" + string.Join("&", actionParameters);
323             }
324 
325             if (parameters == "-") parameters = string.Empty;
326 
327             var cachekey = string.Format("{0}{1}:{2}", key, parameters, mediaType);
328             return cachekey;
329         }
330 
331         private string GetJsonpCallback(HttpRequestMessage request)
332         {
333             var callback = string.Empty;
334             if (request.Method == HttpMethod.Get)
335             {
336                 var query = request.GetQueryNameValuePairs();
337 
338                 if (query != null)
339                 {
340                     var queryVal = query.FirstOrDefault(x => x.Key.ToLower() == "callback");
341                     if (!queryVal.Equals(default(KeyValuePair<string, string>))) callback = queryVal.Value;
342                 }
343             }
344             return callback;
345         }
346 
347         private string GetValue(object val)
348         {
349             if (val is IEnumerable && !(val is string))
350             {
351                 var concatValue = string.Empty;
352                 var paramArray = val as IEnumerable;
353                 return paramArray.Cast<object>().Aggregate(concatValue, (current, paramValue) => current + (paramValue + ";"));
354             }
355             return val.ToString();
356         }
357     }
358 
359 
360     public class CacheOutputConfiguration
361     {
362         private readonly HttpConfiguration _configuration;
363 
364         public CacheOutputConfiguration(HttpConfiguration configuration)
365         {
366             _configuration = configuration;
367         }
368 
369         public void RegisterCacheOutputProvider(Func<IApiOutputCache> provider)
370         {
371             _configuration.Properties.GetOrAdd(typeof(IApiOutputCache), x => provider);
372         }
373 
374         public void RegisterCacheKeyGeneratorProvider<T>(Func<T> provider)
375             where T : ICacheKeyGenerator
376         {
377             _configuration.Properties.GetOrAdd(typeof(T), x => provider);
378         }
379 
380         public void RegisterDefaultCacheKeyGeneratorProvider(Func<ICacheKeyGenerator> provider)
381         {
382             RegisterCacheKeyGeneratorProvider(provider);
383         }
384 
385         public string MakeBaseCachekey(string controller, string action)
386         {
387             return string.Format("{0}-{1}", controller.ToLower(), action.ToLower());
388         }
389 
390         public string MakeBaseCachekey<T, U>(Expression<Func<T, U>> expression)
391         {
392             var method = expression.Body as MethodCallExpression;
393             if (method == null) throw new ArgumentException("Expression is wrong");
394 
395             var methodName = method.Method.Name;
396             var nameAttribs = method.Method.GetCustomAttributes(typeof(ActionNameAttribute), false);
397             if (nameAttribs.Any())
398             {
399                 var actionNameAttrib = (ActionNameAttribute)nameAttribs.FirstOrDefault();
400                 if (actionNameAttrib != null)
401                 {
402                     methodName = actionNameAttrib.Name;
403                 }
404             }
405 
406             return string.Format("{0}-{1}", typeof(T).FullName.ToLower(), methodName.ToLower());
407         }
408 
409         private static ICacheKeyGenerator TryActivateCacheKeyGenerator(Type generatorType)
410         {
411             var hasEmptyOrDefaultConstructor =
412                 generatorType.GetConstructor(Type.EmptyTypes) != null ||
413                 generatorType.GetConstructors(BindingFlags.Instance | BindingFlags.Public)
414                 .Any(x => x.GetParameters().All(p => p.IsOptional));
415             return hasEmptyOrDefaultConstructor
416                 ? Activator.CreateInstance(generatorType) as ICacheKeyGenerator
417                 : null;
418         }
419 
420         public ICacheKeyGenerator GetCacheKeyGenerator(HttpRequestMessage request, Type generatorType)
421         {
422             generatorType = generatorType ?? typeof(ICacheKeyGenerator);
423             object cache;
424             _configuration.Properties.TryGetValue(generatorType, out cache);
425 
426             var cacheFunc = cache as Func<ICacheKeyGenerator>;
427 
428             var generator = cacheFunc != null
429                 ? cacheFunc()
430                 : request.GetDependencyScope().GetService(generatorType) as ICacheKeyGenerator;
431 
432             return generator
433                 ?? TryActivateCacheKeyGenerator(generatorType)
434                 ?? new DefaultCacheKeyGenerator();
435         }
436 
437         public IApiOutputCache GetCacheOutputProvider(HttpRequestMessage request)
438         {
439             object cache;
440             _configuration.Properties.TryGetValue(typeof(IApiOutputCache), out cache);
441 
442             var cacheFunc = cache as Func<IApiOutputCache>;
443 
444             var cacheOutputProvider = cacheFunc != null ? cacheFunc() : request.GetDependencyScope().GetService(typeof(IApiOutputCache)) as IApiOutputCache ?? new MemoryCacheDefault();
445             return cacheOutputProvider;
446         }
447     }
448 
449     public static class HttpConfigurationExtensions
450     {
451         public static CacheOutputConfiguration CacheOutputConfiguration(this HttpConfiguration config)
452         {
453             return new CacheOutputConfiguration(config);
454         }
455     }
456     public sealed class IgnoreCacheOutputAttribute : Attribute
457     {
458     }
459 
460     public class CacheTime
461     {
462         // client cache length in seconds
463         public TimeSpan ClientTimeSpan { get; set; }
464 
465         public DateTimeOffset AbsoluteExpiration { get; set; }
466     }
467     public interface IApiOutputCache
468     {
469         void RemoveStartsWith(string key);
470 
471         T Get<T>(string key) where T : class;
472 
473         [Obsolete("Use Get<T> instead")]
474         object Get(string key);
475 
476         void Remove(string key);
477 
478         bool Contains(string key);
479 
480         void Add(string key, object o, DateTimeOffset expiration, string dependsOnKey = null);
481 
482         IEnumerable<string> AllKeys { get; }
483     }
484 
485     public class ShortTime : IModelQuery<DateTime, CacheTime>
486     {
487         private readonly int serverTimeInSeconds;
488         private readonly int clientTimeInSeconds;
489 
490         public ShortTime(int serverTimeInSeconds, int clientTimeInSeconds)
491         {
492             if (serverTimeInSeconds < 0)
493                 serverTimeInSeconds = 0;
494 
495             this.serverTimeInSeconds = serverTimeInSeconds;
496 
497             if (clientTimeInSeconds < 0)
498                 clientTimeInSeconds = 0;
499 
500             this.clientTimeInSeconds = clientTimeInSeconds;
501         }
502 
503         public CacheTime Execute(DateTime model)
504         {
505             var cacheTime = new CacheTime
506             {
507                 AbsoluteExpiration = model.AddSeconds(serverTimeInSeconds),
508                 ClientTimeSpan = TimeSpan.FromSeconds(clientTimeInSeconds)
509             };
510 
511             return cacheTime;
512         }
513     }
514     public interface IModelQuery<in TModel, out TResult>
515     {
516         TResult Execute(TModel model);
517     }
518     public class MemoryCacheDefault : IApiOutputCache
519     {
520         private static readonly MemoryCache Cache = MemoryCache.Default;
521 
522         public void RemoveStartsWith(string key)
523         {
524             lock (Cache)
525             {
526                 Cache.Remove(key);
527             }
528         }
529 
530         public T Get<T>(string key) where T : class
531         {
532             var o = Cache.Get(key) as T;
533             return o;
534         }
535 
536         [Obsolete("Use Get<T> instead")]
537         public object Get(string key)
538         {
539             return Cache.Get(key);
540         }
541 
542         public void Remove(string key)
543         {
544             lock (Cache)
545             {
546                 Cache.Remove(key);
547             }
548         }
549 
550         public bool Contains(string key)
551         {
552             return Cache.Contains(key);
553         }
554 
555         public void Add(string key, object o, DateTimeOffset expiration, string dependsOnKey = null)
556         {
557             var cachePolicy = new CacheItemPolicy
558             {
559                 AbsoluteExpiration = expiration
560             };
561 
562             if (!string.IsNullOrWhiteSpace(dependsOnKey))
563             {
564                 cachePolicy.ChangeMonitors.Add(
565                     Cache.CreateCacheEntryChangeMonitor(new[] { dependsOnKey })
566                 );
567             }
568             lock (Cache)
569             {
570                 Cache.Add(key, o, cachePolicy);
571             }
572         }
573 
574         public IEnumerable<string> AllKeys
575         {
576             get
577             {
578                 return Cache.Select(x => x.Key);
579             }
580         }
581     }
582 
583     // 使用方法 [CacheOutput(ClientTimeSpan = 350, ServerTimeSpan = 50)]
584 
585 
586 
587 }
原文地址:https://www.cnblogs.com/hbsfgl/p/5076630.html