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 }