diff --git a/.nuget/NuGet.exe b/.nuget/NuGet.exe index 8d13fd8..3ffdd33 100644 Binary files a/.nuget/NuGet.exe and b/.nuget/NuGet.exe differ diff --git a/.nuget/NuGet.targets b/.nuget/NuGet.targets index d3befda..f943812 100644 --- a/.nuget/NuGet.targets +++ b/.nuget/NuGet.targets @@ -2,7 +2,7 @@ $(MSBuildProjectDirectory)\..\ - + false @@ -11,16 +11,16 @@ true - + false - + - + @@ -28,37 +28,48 @@ $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) - $([System.IO.Path]::Combine($(ProjectDir), "packages.config")) - $([System.IO.Path]::Combine($(SolutionDir), "packages")) - + $(SolutionDir).nuget - packages.config - $(SolutionDir)packages + + + + $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config + $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config + + + + $(MSBuildProjectDirectory)\packages.config + $(PackagesProjectConfig) - $(NuGetToolsPath)\nuget.exe + $(NuGetToolsPath)\NuGet.exe @(PackageSource) - + "$(NuGetExePath)" mono --runtime=v4.0.30319 $(NuGetExePath) $(TargetDir.Trim('\\')) - + -RequireConsent + -NonInteractive + + "$(SolutionDir) " + "$(SolutionDir)" + - $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(RequireConsentSwitch) -o "$(PackagesDir)" - $(NuGetCommand) pack "$(ProjectPath)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols + $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir) + $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols - + RestorePackages; - $(ResolveReferencesDependsOn); - + $(BuildDependsOn); + @@ -70,37 +81,36 @@ - - + - + - + - - + - + @@ -119,7 +129,7 @@ Log.LogMessage("Downloading latest version of NuGet.exe..."); WebClient webClient = new WebClient(); - webClient.DownloadFile("https://nuget.org/nuget.exe", OutputFilename); + webClient.DownloadFile("https://www.nuget.org/nuget.exe", OutputFilename); return true; } @@ -131,23 +141,4 @@ - - - - - - - - - - - - - - \ No newline at end of file + diff --git a/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs b/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs index ee6fbca..f4df32b 100644 --- a/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs +++ b/src/WebApi.OutputCache.V2/CacheOutputAttribute.cs @@ -17,261 +17,266 @@ namespace WebApi.OutputCache.V2 { - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] - public class CacheOutputAttribute : ActionFilterAttribute - { - private const string CurrentRequestMediaType = "CacheOutput:CurrentRequestMediaType"; - protected static MediaTypeHeaderValue DefaultMediaType = new MediaTypeHeaderValue("application/json") {CharSet = Encoding.UTF8.HeaderName}; - - /// - /// Cache enabled only for requests when Thread.CurrentPrincipal is not set - /// - public bool AnonymousOnly { get; set; } - - /// - /// 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 - /// - public bool MustRevalidate { get; set; } - - /// - /// Do not vary cache by querystring values - /// - public bool ExcludeQueryStringFromCacheKey { get; set; } - - /// - /// How long response should be cached on the server side (in seconds) - /// - public int ServerTimeSpan { get; set; } - - /// - /// Corresponds to CacheControl MaxAge HTTP header (in seconds) - /// - public int ClientTimeSpan { get; set; } - - /// - /// Corresponds to CacheControl NoCache HTTP header - /// - public bool NoCache { get; set; } - - /// - /// Corresponds to CacheControl Private HTTP header. Response can be cached by browser but not by intermediary cache - /// - public bool Private { get; set; } - - /// - /// Class used to generate caching keys - /// - public Type CacheKeyGenerator { get; set; } - - // cache repository - private IApiOutputCache _webApiCache; - - protected virtual void EnsureCache(HttpConfiguration config, HttpRequestMessage req) - { - _webApiCache = config.CacheOutputConfiguration().GetCacheOutputProvider(req); - } - - internal IModelQuery CacheTimeQuery; - - protected virtual bool IsCachingAllowed(HttpActionContext actionContext, bool anonymousOnly) - { - if (anonymousOnly) - { - if (Thread.CurrentPrincipal.Identity.IsAuthenticated) - { - return false; - } - } - - if (actionContext.ActionDescriptor.GetCustomAttributes().Any()) - { - return false; - } - - return actionContext.Request.Method == HttpMethod.Get; - } - - protected virtual void EnsureCacheTimeQuery() - { - if (CacheTimeQuery == null) ResetCacheTimeQuery(); - } - - protected void ResetCacheTimeQuery() - { - CacheTimeQuery = new ShortTime( ServerTimeSpan, ClientTimeSpan ); - } - - 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) && (returnType != typeof(IHttpActionResult) || typeof(IHttpActionResult).IsAssignableFrom(returnType))) - { - var negotiatedResult = negotiator.Negotiate(returnType, actionContext.Request, config.Formatters); - - if (negotiatedResult == null) - { - return DefaultMediaType; - } - - responseMediaType = negotiatedResult.MediaType; - if (string.IsNullOrWhiteSpace(responseMediaType.CharSet)) - { - 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))) - { - return DefaultMediaType; - } - } - } - - return responseMediaType; - } - - public override void OnActionExecuting(HttpActionContext actionContext) - { - if (actionContext == null) throw new ArgumentNullException("actionContext"); - - if (!IsCachingAllowed(actionContext, AnonymousOnly)) return; - - var config = actionContext.Request.GetConfiguration(); - - EnsureCacheTimeQuery(); - EnsureCache(config, actionContext.Request); - - var cacheKeyGenerator = config.CacheOutputConfiguration().GetCacheKeyGenerator(actionContext.Request, CacheKeyGenerator); - - var responseMediaType = GetExpectedMediaType(config, actionContext); - actionContext.Request.Properties[CurrentRequestMediaType] = responseMediaType; - var cachekey = cacheKeyGenerator.MakeCacheKey(actionContext, responseMediaType, ExcludeQueryStringFromCacheKey); - - if (!_webApiCache.Contains(cachekey)) return; - - if (actionContext.Request.Headers.IfNoneMatch != null) - { - var etag = _webApiCache.Get(cachekey + Constants.EtagKey); - if (etag != null) - { - if (actionContext.Request.Headers.IfNoneMatch.Any(x => x.Tag == etag)) - { - var time = CacheTimeQuery.Execute(DateTime.Now); - var quickResponse = actionContext.Request.CreateResponse(HttpStatusCode.NotModified); - ApplyCacheHeaders(quickResponse, time); - actionContext.Response = quickResponse; - return; - } - } - } - - var val = _webApiCache.Get(cachekey); - if (val == null) return; - - var contenttype = _webApiCache.Get(cachekey + Constants.ContentTypeKey) ?? new MediaTypeHeaderValue(cachekey.Split(new[] { ':' }, 2)[1].Split(';')[0]); - - actionContext.Response = actionContext.Request.CreateResponse(); - actionContext.Response.Content = new ByteArrayContent(val); - - actionContext.Response.Content.Headers.ContentType = contenttype; - var responseEtag = _webApiCache.Get(cachekey + Constants.EtagKey); - if (responseEtag != null) SetEtag(actionContext.Response, responseEtag); - - var cacheTime = CacheTimeQuery.Execute(DateTime.Now); - ApplyCacheHeaders(actionContext.Response, cacheTime); - } - - public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) - { - if (actionExecutedContext.ActionContext.Response == null || !actionExecutedContext.ActionContext.Response.IsSuccessStatusCode) return; - - if (!IsCachingAllowed(actionExecutedContext.ActionContext, AnonymousOnly)) return; - - var cacheTime = CacheTimeQuery.Execute(DateTime.Now); - if (cacheTime.AbsoluteExpiration > DateTime.Now) - { - var httpConfig = actionExecutedContext.Request.GetConfiguration(); - var config = httpConfig.CacheOutputConfiguration(); - var cacheKeyGenerator = config.GetCacheKeyGenerator(actionExecutedContext.Request, CacheKeyGenerator); - - var responseMediaType = actionExecutedContext.Request.Properties[CurrentRequestMediaType] as MediaTypeHeaderValue ?? GetExpectedMediaType(httpConfig, actionExecutedContext.ActionContext); - var cachekey = cacheKeyGenerator.MakeCacheKey(actionExecutedContext.ActionContext, responseMediaType, ExcludeQueryStringFromCacheKey); - - if (!string.IsNullOrWhiteSpace(cachekey) && !(_webApiCache.Contains(cachekey))) - { - SetEtag(actionExecutedContext.Response, CreateEtag(actionExecutedContext, cachekey, cacheTime)); - - var responseContent = actionExecutedContext.Response.Content; - - if (responseContent != null) - { - var baseKey = config.MakeBaseCachekey(actionExecutedContext.ActionContext.ControllerContext.ControllerDescriptor.ControllerType.FullName, actionExecutedContext.ActionContext.ActionDescriptor.ActionName); - var contentType = responseContent.Headers.ContentType; - string etag = actionExecutedContext.Response.Headers.ETag.Tag; - //ConfigureAwait false to avoid deadlocks - var content = await responseContent.ReadAsByteArrayAsync().ConfigureAwait(false); - - responseContent.Headers.Remove("Content-Length"); - - _webApiCache.Add(baseKey, string.Empty, cacheTime.AbsoluteExpiration); - _webApiCache.Add(cachekey, content, cacheTime.AbsoluteExpiration, baseKey); - - - _webApiCache.Add(cachekey + Constants.ContentTypeKey, - contentType, - cacheTime.AbsoluteExpiration, baseKey); - - - _webApiCache.Add(cachekey + Constants.EtagKey, - etag, - cacheTime.AbsoluteExpiration, baseKey); - } - } - } - - ApplyCacheHeaders(actionExecutedContext.ActionContext.Response, cacheTime); - } - - protected virtual void ApplyCacheHeaders(HttpResponseMessage response, CacheTime cacheTime) - { - if (cacheTime.ClientTimeSpan > TimeSpan.Zero || MustRevalidate || Private) - { - var cachecontrol = new CacheControlHeaderValue - { - MaxAge = cacheTime.ClientTimeSpan, - MustRevalidate = MustRevalidate, - Private = Private - }; - - response.Headers.CacheControl = cachecontrol; - } - else if (NoCache) - { - response.Headers.CacheControl = new CacheControlHeaderValue { NoCache = true }; - response.Headers.Add("Pragma", "no-cache"); - } - } - - protected virtual string CreateEtag(HttpActionExecutedContext actionExecutedContext, string cachekey, CacheTime cacheTime) - { - return Guid.NewGuid().ToString(); - } - - private static void SetEtag(HttpResponseMessage message, string etag) - { - if (etag != null) - { - var eTag = new EntityTagHeaderValue(@"""" + etag.Replace("\"", string.Empty) + @""""); - message.Headers.ETag = eTag; - } - } - } -} + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public class CacheOutputAttribute : ActionFilterAttribute + { + private const string CurrentRequestMediaType = "CacheOutput:CurrentRequestMediaType"; + protected static MediaTypeHeaderValue DefaultMediaType = new MediaTypeHeaderValue("application/json") { CharSet = Encoding.UTF8.HeaderName }; + + /// + /// Cache enabled only for requests when Thread.CurrentPrincipal is not set + /// + public bool AnonymousOnly { get; set; } + + /// + /// 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 + /// + public bool MustRevalidate { get; set; } + + /// + /// Do not vary cache by querystring values + /// + public bool ExcludeQueryStringFromCacheKey { get; set; } + + /// + /// How long response should be cached on the server side (in seconds) + /// + public int ServerTimeSpan { get; set; } + + /// + /// Corresponds to CacheControl MaxAge HTTP header (in seconds) + /// + public int ClientTimeSpan { get; set; } + + /// + /// Corresponds to CacheControl NoCache HTTP header + /// + public bool NoCache { get; set; } + + /// + /// Corresponds to CacheControl Private HTTP header. Response can be cached by browser but not by intermediary cache + /// + public bool Private { get; set; } + + /// + /// Class used to generate caching keys + /// + public Type CacheKeyGenerator { get; set; } + + // cache repository + private IApiOutputCache _webApiCache; + + protected virtual void EnsureCache(HttpConfiguration config, HttpRequestMessage req) + { + _webApiCache = config.CacheOutputConfiguration().GetCacheOutputProvider(req); + } + + internal IModelQuery CacheTimeQuery; + + protected virtual bool IsCachingAllowed(HttpActionContext actionContext, bool anonymousOnly) + { + if (anonymousOnly) + { + if (Thread.CurrentPrincipal.Identity.IsAuthenticated) + { + return false; + } + } + + if (actionContext.ActionDescriptor.GetCustomAttributes().Any()) + { + return false; + } + + + var verbsAttrs = actionContext.ActionDescriptor.GetFilterPipeline().Select(a => a.Instance).OfType(); + + return verbsAttrs.Any() + ? verbsAttrs.SelectMany(a => a.Verbs).Any(a => actionContext.Request.Method == new HttpMethod(a)) + : actionContext.Request.Method == HttpMethod.Get; + } + + protected virtual void EnsureCacheTimeQuery() + { + if (CacheTimeQuery == null) ResetCacheTimeQuery(); + } + + protected void ResetCacheTimeQuery() + { + CacheTimeQuery = new ShortTime(ServerTimeSpan, ClientTimeSpan); + } + + 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) && (returnType != typeof(IHttpActionResult) || typeof(IHttpActionResult).IsAssignableFrom(returnType))) + { + var negotiatedResult = negotiator.Negotiate(returnType, actionContext.Request, config.Formatters); + + if (negotiatedResult == null) + { + return DefaultMediaType; + } + + responseMediaType = negotiatedResult.MediaType; + if (string.IsNullOrWhiteSpace(responseMediaType.CharSet)) + { + 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))) + { + return DefaultMediaType; + } + } + } + + return responseMediaType; + } + + public override void OnActionExecuting(HttpActionContext actionContext) + { + if (actionContext == null) throw new ArgumentNullException("actionContext"); + + if (!IsCachingAllowed(actionContext, AnonymousOnly)) return; + + var config = actionContext.Request.GetConfiguration(); + + EnsureCacheTimeQuery(); + EnsureCache(config, actionContext.Request); + + var cacheKeyGenerator = config.CacheOutputConfiguration().GetCacheKeyGenerator(actionContext.Request, CacheKeyGenerator); + + var responseMediaType = GetExpectedMediaType(config, actionContext); + actionContext.Request.Properties[CurrentRequestMediaType] = responseMediaType; + var cachekey = cacheKeyGenerator.MakeCacheKey(actionContext, responseMediaType, ExcludeQueryStringFromCacheKey); + + if (!_webApiCache.Contains(cachekey)) return; + + if (actionContext.Request.Headers.IfNoneMatch != null) + { + var etag = _webApiCache.Get(cachekey + Constants.EtagKey); + if (etag != null) + { + if (actionContext.Request.Headers.IfNoneMatch.Any(x => x.Tag == etag)) + { + var time = CacheTimeQuery.Execute(DateTime.Now); + var quickResponse = actionContext.Request.CreateResponse(HttpStatusCode.NotModified); + ApplyCacheHeaders(quickResponse, time); + actionContext.Response = quickResponse; + return; + } + } + } + + var val = _webApiCache.Get(cachekey); + if (val == null) return; + + var contenttype = _webApiCache.Get(cachekey + Constants.ContentTypeKey) ?? new MediaTypeHeaderValue(cachekey.Split(new[] { ':' }, 2)[1].Split(';')[0]); + + actionContext.Response = actionContext.Request.CreateResponse(); + actionContext.Response.Content = new ByteArrayContent(val); + + actionContext.Response.Content.Headers.ContentType = contenttype; + var responseEtag = _webApiCache.Get(cachekey + Constants.EtagKey); + if (responseEtag != null) SetEtag(actionContext.Response, responseEtag); + + var cacheTime = CacheTimeQuery.Execute(DateTime.Now); + ApplyCacheHeaders(actionContext.Response, cacheTime); + } + + public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) + { + if (actionExecutedContext.ActionContext.Response == null || !actionExecutedContext.ActionContext.Response.IsSuccessStatusCode) return; + + if (!IsCachingAllowed(actionExecutedContext.ActionContext, AnonymousOnly)) return; + + var cacheTime = CacheTimeQuery.Execute(DateTime.Now); + if (cacheTime.AbsoluteExpiration > DateTime.Now) + { + var httpConfig = actionExecutedContext.Request.GetConfiguration(); + var config = httpConfig.CacheOutputConfiguration(); + var cacheKeyGenerator = config.GetCacheKeyGenerator(actionExecutedContext.Request, CacheKeyGenerator); + + var responseMediaType = actionExecutedContext.Request.Properties[CurrentRequestMediaType] as MediaTypeHeaderValue ?? GetExpectedMediaType(httpConfig, actionExecutedContext.ActionContext); + var cachekey = cacheKeyGenerator.MakeCacheKey(actionExecutedContext.ActionContext, responseMediaType, ExcludeQueryStringFromCacheKey); + + if (!string.IsNullOrWhiteSpace(cachekey) && !(_webApiCache.Contains(cachekey))) + { + SetEtag(actionExecutedContext.Response, CreateEtag(actionExecutedContext, cachekey, cacheTime)); + + var responseContent = actionExecutedContext.Response.Content; + + if (responseContent != null) + { + var baseKey = config.MakeBaseCachekey(actionExecutedContext.ActionContext.ControllerContext.ControllerDescriptor.ControllerType.FullName, actionExecutedContext.ActionContext.ActionDescriptor.ActionName); + var contentType = responseContent.Headers.ContentType; + string etag = actionExecutedContext.Response.Headers.ETag.Tag; + //ConfigureAwait false to avoid deadlocks + var content = await responseContent.ReadAsByteArrayAsync().ConfigureAwait(false); + + responseContent.Headers.Remove("Content-Length"); + + _webApiCache.Add(baseKey, string.Empty, cacheTime.AbsoluteExpiration); + _webApiCache.Add(cachekey, content, cacheTime.AbsoluteExpiration, baseKey); + + + _webApiCache.Add(cachekey + Constants.ContentTypeKey, + contentType, + cacheTime.AbsoluteExpiration, baseKey); + + + _webApiCache.Add(cachekey + Constants.EtagKey, + etag, + cacheTime.AbsoluteExpiration, baseKey); + } + } + } + + ApplyCacheHeaders(actionExecutedContext.ActionContext.Response, cacheTime); + } + + protected virtual void ApplyCacheHeaders(HttpResponseMessage response, CacheTime cacheTime) + { + if (cacheTime.ClientTimeSpan > TimeSpan.Zero || MustRevalidate || Private) + { + var cachecontrol = new CacheControlHeaderValue + { + MaxAge = cacheTime.ClientTimeSpan, + MustRevalidate = MustRevalidate, + Private = Private + }; + + response.Headers.CacheControl = cachecontrol; + } + else if (NoCache) + { + response.Headers.CacheControl = new CacheControlHeaderValue { NoCache = true }; + response.Headers.Add("Pragma", "no-cache"); + } + } + + protected virtual string CreateEtag(HttpActionExecutedContext actionExecutedContext, string cachekey, CacheTime cacheTime) + { + return Guid.NewGuid().ToString(); + } + + private static void SetEtag(HttpResponseMessage message, string etag) + { + if (etag != null) + { + var eTag = new EntityTagHeaderValue(@"""" + etag.Replace("\"", string.Empty) + @""""); + message.Headers.ETag = eTag; + } + } + } +} diff --git a/src/WebApi.OutputCache.V2/CacheOutputHttpVerbsAttribute.cs b/src/WebApi.OutputCache.V2/CacheOutputHttpVerbsAttribute.cs new file mode 100644 index 0000000..ca1c249 --- /dev/null +++ b/src/WebApi.OutputCache.V2/CacheOutputHttpVerbsAttribute.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Web.Http.Filters; + +namespace WebApi.OutputCache.V2 +{ + /// + /// When this attribute is used, only the specified vebrs will participate in cache. + /// Otherwise GET method only. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] + public class CacheOutputHttpVerbsAttribute : ActionFilterAttribute + { + /// + /// Gets or sets the list of HTTP verbs that needs to participate in cache. + /// + public string[] Verbs { get; private set; } + + + public CacheOutputHttpVerbsAttribute(params string[] verbs) + { + Verbs = verbs; + } + } + + + + /// + /// A shorthand version to CacheOutputHttpVerbsAttribute. + /// + /// + public sealed class CacheOutputHttpGetAttribute : CacheOutputHttpVerbsAttribute + { + public CacheOutputHttpGetAttribute() + : base("GET") + { + } + } + + + + /// + /// A shorthand version to CacheOutputHttpVerbsAttribute. + /// + /// + public sealed class CacheOutputHttpPostAttribute : CacheOutputHttpVerbsAttribute + { + public CacheOutputHttpPostAttribute() + : base("POST") + { + } + } + + +} \ No newline at end of file diff --git a/src/WebApi.OutputCache.V2/CacheOutputKeyAttribute.cs b/src/WebApi.OutputCache.V2/CacheOutputKeyAttribute.cs new file mode 100644 index 0000000..04db1a3 --- /dev/null +++ b/src/WebApi.OutputCache.V2/CacheOutputKeyAttribute.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; + +namespace WebApi.OutputCache.V2 +{ + /// + /// Enables custom cache key generation on a type. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class CacheKeyComponentAttribute : Attribute + { + public Type GeneratorType { get; private set; } + + + public CacheKeyComponentAttribute(Type generatorType) + { + GeneratorType = generatorType; + } + } + + + + + public interface ICacheKeyComponentGenerator + { + string Generate(object value); + } + + + + + + /// No recursive generation. + public abstract class ComplexValueCacheKeyComponentGenerator : ICacheKeyComponentGenerator + { + public string Generate(object value) + { + return string.Format("{0}{{{1}}}", value.GetType().FullName, + string.Join(";", GetNamedValues(value).Select(a => a.Key + "=" + a.Value))); + } + + /// + /// Usually will return a dictionary of property name/value. + /// + protected abstract IDictionary GetNamedValues(object complexValue); + } + + + + + internal class EnumerableCacheKeyComponentGenerator : ICacheKeyComponentGenerator + { + public string Generate(object value) + { + return string.Join(";", (IEnumerable)value); + } + } + + + internal class AnyCacheKeyComponentGenerator : ICacheKeyComponentGenerator + { + public string Generate(object value) + { + return value.ToString(); + } + } + +} \ No newline at end of file diff --git a/src/WebApi.OutputCache.V2/DefaultCacheKeyGenerator.cs b/src/WebApi.OutputCache.V2/DefaultCacheKeyGenerator.cs index cf02a29..701ee6c 100644 --- a/src/WebApi.OutputCache.V2/DefaultCacheKeyGenerator.cs +++ b/src/WebApi.OutputCache.V2/DefaultCacheKeyGenerator.cs @@ -1,80 +1,97 @@ -using System.Collections; +using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; +using System.Reflection; using System.Text; using System.Web.Http.Controllers; namespace WebApi.OutputCache.V2 { - public class DefaultCacheKeyGenerator : ICacheKeyGenerator - { - public virtual string MakeCacheKey(HttpActionContext context, MediaTypeHeaderValue mediaType, bool excludeQueryString = false) - { - var controller = context.ControllerContext.ControllerDescriptor.ControllerType.FullName; - var action = context.ActionDescriptor.ActionName; - var key = context.Request.GetConfiguration().CacheOutputConfiguration().MakeBaseCachekey(controller, action); - var actionParameters = context.ActionArguments.Where(x => x.Value != null).Select(x => x.Key + "=" + GetValue(x.Value)); + public class DefaultCacheKeyGenerator : ICacheKeyGenerator + { + public virtual string MakeCacheKey(HttpActionContext context, MediaTypeHeaderValue mediaType, bool excludeQueryString = false) + { + var controller = context.ControllerContext.ControllerDescriptor.ControllerType.FullName; + var action = context.ActionDescriptor.ActionName; + var key = context.Request.GetConfiguration().CacheOutputConfiguration().MakeBaseCachekey(controller, action); + var actionParameters = context.ActionArguments.Where(x => x.Value != null).Select(x => x.Key + "=" + GetValue(x.Value)); - string parameters; + string parameters; - if (!excludeQueryString) - { - var queryStringParameters = - context.Request.GetQueryNameValuePairs() - .Where(x => x.Key.ToLower() != "callback") - .Select(x => x.Key + "=" + x.Value); - var parametersCollections = actionParameters.Union(queryStringParameters); - parameters = "-" + string.Join("&", parametersCollections); + if (!excludeQueryString) + { + var queryStringParameters = + context.Request.GetQueryNameValuePairs() + .Where(x => x.Key.ToLower() != "callback") + .Select(x => x.Key + "=" + x.Value); + var parametersCollections = actionParameters.Union(queryStringParameters); + parameters = "-" + string.Join("&", parametersCollections); - var callbackValue = GetJsonpCallback(context.Request); - if (!string.IsNullOrWhiteSpace(callbackValue)) - { - var callback = "callback=" + callbackValue; - if (parameters.Contains("&" + callback)) parameters = parameters.Replace("&" + callback, string.Empty); - if (parameters.Contains(callback + "&")) parameters = parameters.Replace(callback + "&", string.Empty); - if (parameters.Contains("-" + callback)) parameters = parameters.Replace("-" + callback, string.Empty); - if (parameters.EndsWith("&")) parameters = parameters.TrimEnd('&'); - } - } - else - { - parameters = "-" + string.Join("&", actionParameters); - } + var callbackValue = GetJsonpCallback(context.Request); + if (!string.IsNullOrWhiteSpace(callbackValue)) + { + var callback = "callback=" + callbackValue; + if (parameters.Contains("&" + callback)) parameters = parameters.Replace("&" + callback, string.Empty); + if (parameters.Contains(callback + "&")) parameters = parameters.Replace(callback + "&", string.Empty); + if (parameters.Contains("-" + callback)) parameters = parameters.Replace("-" + callback, string.Empty); + if (parameters.EndsWith("&")) parameters = parameters.TrimEnd('&'); + } + } + else + { + parameters = "-" + string.Join("&", actionParameters); + } - if (parameters == "-") parameters = string.Empty; + if (parameters == "-") parameters = string.Empty; - var cachekey = string.Format("{0}{1}:{2}", key, parameters, mediaType); - return cachekey; - } + var cachekey = string.Format("{0}{1}:{2}", key, parameters, mediaType); + return cachekey; + } - private string GetJsonpCallback(HttpRequestMessage request) - { - var callback = string.Empty; - if (request.Method == HttpMethod.Get) - { - var query = request.GetQueryNameValuePairs(); + private string GetJsonpCallback(HttpRequestMessage request) + { + var callback = string.Empty; + if (request.Method == HttpMethod.Get) + { + var query = request.GetQueryNameValuePairs(); - if (query != null) - { - var queryVal = query.FirstOrDefault(x => x.Key.ToLower() == "callback"); - if (!queryVal.Equals(default(KeyValuePair))) callback = queryVal.Value; - } - } - return callback; - } + if (query != null) + { + var queryVal = query.FirstOrDefault(x => x.Key.ToLower() == "callback"); + if (!queryVal.Equals(default(KeyValuePair))) callback = queryVal.Value; + } + } + return callback; + } - private string GetValue(object val) - { - if (val is IEnumerable && !(val is string)) - { - var concatValue = string.Empty; - var paramArray = val as IEnumerable; - return paramArray.Cast().Aggregate(concatValue, (current, paramValue) => current + (paramValue + ";")); - } - return val.ToString(); - } - } + private string GetValue(object value) + { + ICacheKeyComponentGenerator generator; + + + // Use custom generator if available. + var valueType = value.GetType(); + var keyComponentAttr = valueType.GetCustomAttribute(); + if (keyComponentAttr != null) + { + generator = (ICacheKeyComponentGenerator)Activator.CreateInstance(keyComponentAttr.GeneratorType); + } + // Use a default generator. + else if (value is IEnumerable && !(value is string)) + { + generator = new EnumerableCacheKeyComponentGenerator(); + } + else + { + generator = new AnyCacheKeyComponentGenerator(); + } + + + return generator.Generate(value); + } + } } \ No newline at end of file diff --git a/src/WebApi.OutputCache.V2/WebApi.OutputCache.V2.csproj b/src/WebApi.OutputCache.V2/WebApi.OutputCache.V2.csproj index 11cfdd5..36f5f6d 100644 --- a/src/WebApi.OutputCache.V2/WebApi.OutputCache.V2.csproj +++ b/src/WebApi.OutputCache.V2/WebApi.OutputCache.V2.csproj @@ -58,9 +58,11 @@ + +