123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- using System;
- using System.Linq;
- using System.Net;
- using System.Net.Http;
- using System.Net.Http.Formatting;
- using System.Net.Http.Headers;
- using System.Runtime.ExceptionServices;
- using System.Text;
- using System.Threading;
- using System.Threading.Tasks;
- using System.Web.Http;
- using System.Web.Http.Controllers;
- using System.Web.Http.Filters;
- using iiie.CacheControl.Business.HttpExtensions;
- using iiie.CacheControl.Business.OutputCache;
- using iiie.CacheControl.DBO;
-
- namespace iiie.CacheControl.Attributes
- {
- [AttributeUsage(AttributeTargets.Method)]
- public abstract class CacheControlAttribute : FilterAttribute, IActionFilter
- {
- protected static MediaTypeHeaderValue DefaultMediaType = new MediaTypeHeaderValue("application/json");
-
- /// <summary>
- /// Indicates if the client can reuse cached data without asking origin server
- /// </summary>
- protected bool MustRevalidate { get; set; }
-
- /// <summary>
- /// Indicates if the query string must be used to control cache
- /// </summary>
- protected bool ExcludeQueryStringFromCacheKey { get; set; }
-
- /// <summary>
- /// Indicates if the post data must be used to control cache
- /// </summary>
- protected bool ExcludePostFromCacheKey { get; set; }
-
- /// <summary>
- /// Optionnal data used by ouput cache backend
- /// </summary>
- public object OutputCacheData { get; set; }
-
- /// <summary>
- /// Define the cache type used to store cache
- /// </summary>
- protected OutputCacheType CacheType { get; set; }
-
- protected Type CacheKeyGenerator { get; set; }
-
- private MediaTypeHeaderValue _responseMediaType;
-
- private IOutputCache _webCache;
-
- protected abstract bool IsValid(CacheDbo data);
-
- protected virtual CacheDbo CreateCacheUser()
- {
- return new CacheDbo();
- }
-
- protected virtual MediaTypeHeaderValue GetExpectedMediaType(HttpConfiguration config, HttpActionContext actionContext)
- {
- MediaTypeHeaderValue responseMediaType = null;
-
- var negotiator = config.Services.GetService(typeof(IContentNegotiator)) as IContentNegotiator;
- var returnType = actionContext.ActionDescriptor.ReturnType;
-
- if (negotiator != null && returnType != typeof(HttpResponseMessage))
- {
- var negotiatedResult = negotiator.Negotiate(returnType, actionContext.Request, config.Formatters);
- responseMediaType = negotiatedResult.MediaType;
- responseMediaType.CharSet = Encoding.UTF8.HeaderName;
- }
- else
- {
- if (actionContext.Request.Headers.Accept != null)
- {
- responseMediaType = actionContext.Request.Headers.Accept.FirstOrDefault();
- if (responseMediaType == null ||
- !config.Formatters.Any(x => x.SupportedMediaTypes.Contains(responseMediaType)))
- {
- DefaultMediaType.CharSet = Encoding.UTF8.HeaderName;
- return DefaultMediaType;
- }
- }
- }
-
- return responseMediaType;
- }
-
- private void OnActionExecuting(HttpActionContext actionContext)
- {
- if (actionContext == null) throw new ArgumentNullException("actionContext");
-
- var config = actionContext.Request.GetConfiguration();
-
- _webCache = config.CacheOutputConfiguration(CacheType).GetCacheOutputProvider(actionContext.Request, OutputCacheData);
-
- var cacheKeyGenerator = config.CacheOutputConfiguration(CacheType).GetCacheKeyGenerator(actionContext.Request, CacheKeyGenerator);
-
- _responseMediaType = GetExpectedMediaType(config, actionContext);
-
- var cachekey = cacheKeyGenerator.MakeCacheKey(actionContext, _responseMediaType, CacheType,
- ExcludeQueryStringFromCacheKey, ExcludePostFromCacheKey);
-
- var data = _webCache.Get(cachekey);
- if (data == null)
- return;
-
- if (!IsValid(data))
- {
- _webCache.Remove(cachekey);
- return;
- }
-
- if (actionContext.Request.Headers.IfNoneMatch != null)
- {
- if (data.ETag != null)
- {
- if (actionContext.Request.Headers.IfNoneMatch.Any(x => x.Tag == data.ETag))
- {
- var quickResponse = actionContext.Request.CreateResponse(HttpStatusCode.NotModified);
- ApplyCacheHeaders(quickResponse);
- actionContext.Response = quickResponse;
- return;
- }
- }
- }
-
- if (data.Content == null) return;
-
- actionContext.Response = actionContext.Request.CreateResponse();
- actionContext.Response.Content = new ByteArrayContent(data.Content);
-
- actionContext.Response.Content.Headers.ContentType = new MediaTypeHeaderValue(data.ContentType);
- if (data.ETag != null) SetEtag(actionContext.Response, data.ETag);
-
- ApplyCacheHeaders(actionContext.Response);
- }
-
- private async Task OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
- {
- if (actionExecutedContext.ActionContext.Response == null || !actionExecutedContext.ActionContext.Response.IsSuccessStatusCode) return;
-
- var config = actionExecutedContext.Request.GetConfiguration().CacheOutputConfiguration(CacheType);
- var cacheKeyGenerator = config.GetCacheKeyGenerator(actionExecutedContext.Request, CacheKeyGenerator);
-
- var cachekey = cacheKeyGenerator.MakeCacheKey(actionExecutedContext.ActionContext, _responseMediaType,
- CacheType, ExcludeQueryStringFromCacheKey, ExcludePostFromCacheKey);
-
- if (!string.IsNullOrWhiteSpace(cachekey) && !(_webCache.Contains(cachekey)))
- {
- SetEtag(actionExecutedContext.Response, Guid.NewGuid().ToString());
-
- if (actionExecutedContext.Response.Content != null)
- {
- var data = CreateCacheUser();
- data.Content = await actionExecutedContext.Response.Content.ReadAsByteArrayAsync();
- data.ContentType = actionExecutedContext.Response.Content.Headers.ContentType.MediaType;
- data.ETag = actionExecutedContext.Response.Headers.ETag.Tag;
- data.Date = DateTime.Now;
-
- _webCache.Add(cachekey, data);
- }
- }
-
- ApplyCacheHeaders(actionExecutedContext.ActionContext.Response);
- }
-
- private void ApplyCacheHeaders(HttpResponseMessage response)
- {
- if (MustRevalidate)
- {
- response.Headers.CacheControl = new CacheControlHeaderValue
- {
- MustRevalidate = MustRevalidate
- };
- }
- }
-
- private static void SetEtag(HttpResponseMessage message, string etag)
- {
- if (etag != null)
- {
- message.Headers.ETag = new EntityTagHeaderValue(@"""" + etag.Replace("\"", string.Empty) + @"""");
- }
- }
-
- Task<HttpResponseMessage> IActionFilter.ExecuteActionFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
- {
- if (actionContext == null)
- {
- throw new ArgumentNullException("actionContext");
- }
-
- if (continuation == null)
- {
- throw new ArgumentNullException("continuation");
- }
-
- OnActionExecuting(actionContext);
-
- if (actionContext.Response != null)
- {
- return Task.FromResult(actionContext.Response);
- }
-
- return CallOnActionExecutedAsync(actionContext, cancellationToken, continuation);
- }
-
- private async Task<HttpResponseMessage> CallOnActionExecutedAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- HttpResponseMessage response = null;
- Exception exception = null;
- try
- {
- response = await continuation();
- }
- catch (Exception e)
- {
- exception = e;
- }
-
- try
- {
- var executedContext = new HttpActionExecutedContext(actionContext, exception) { Response = response };
- await OnActionExecuted(executedContext);
-
- if (executedContext.Response != null)
- {
- return executedContext.Response;
- }
-
- if (executedContext.Exception != null)
- {
- ExceptionDispatchInfo.Capture(executedContext.Exception).Throw();
- }
- }
- catch (Exception e)
- {
- actionContext.Response = null;
- ExceptionDispatchInfo.Capture(e).Throw();
- }
-
- throw new InvalidOperationException(GetType().Name);
- }
-
- }
- }
|