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"); /// /// Indicates if the client can reuse cached data without asking origin server /// protected bool MustRevalidate { get; set; } /// /// Indicates if the query string must be used to control cache /// protected bool ExcludeQueryStringFromCacheKey { get; set; } /// /// Indicates if the post data must be used to control cache /// protected bool ExcludePostFromCacheKey { get; set; } /// /// Optionnal data used by ouput cache backend /// public object OutputCacheData { get; set; } /// /// Define the cache type used to store cache /// 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 IActionFilter.ExecuteActionFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func> 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 CallOnActionExecutedAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func> 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); } } }