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);
}
}
}