Skip to content

Commit

Permalink
Merge pull request #236 from filipw/dev
Browse files Browse the repository at this point in the history
Sync master with latest dev
  • Loading branch information
filipw authored Apr 3, 2018
2 parents 62fa226 + 9ba890d commit 4a8f908
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 12 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ public interface IApiOutputCache
bool Contains(string key);
void Add(string key, object o, DateTimeOffset expiration, string dependsOnKey = null);
}
``
```

Suppose you have a custom implementation:

Expand Down
1 change: 1 addition & 0 deletions src/WebApi.OutputCache.Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ public sealed class Constants
{
public const string ContentTypeKey = ":response-ct";
public const string EtagKey = ":response-etag";
public const string GenerationTimestampKey = ":response-generationtimestamp";
}
}
21 changes: 16 additions & 5 deletions src/WebApi.OutputCache.V2/CacheOutputAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ public override void OnActionExecuting(HttpActionContext actionContext)
if (val == null) return;

var contenttype = _webApiCache.Get<MediaTypeHeaderValue>(cachekey + Constants.ContentTypeKey) ?? responseMediaType;
var contentGenerationTimestamp = DateTimeOffset.Parse(_webApiCache.Get<string>(cachekey + Constants.GenerationTimestampKey));

actionContext.Response = actionContext.Request.CreateResponse();
actionContext.Response.Content = new ByteArrayContent(val);
Expand All @@ -214,7 +215,7 @@ public override void OnActionExecuting(HttpActionContext actionContext)
if (responseEtag != null) SetEtag(actionContext.Response, responseEtag);

var cacheTime = CacheTimeQuery.Execute(DateTime.Now);
ApplyCacheHeaders(actionContext.Response, cacheTime);
ApplyCacheHeaders(actionContext.Response, cacheTime, contentGenerationTimestamp);
}

public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
Expand All @@ -223,8 +224,9 @@ public override async Task OnActionExecutedAsync(HttpActionExecutedContext actio

if (!IsCachingAllowed(actionExecutedContext.ActionContext, AnonymousOnly)) return;

var cacheTime = CacheTimeQuery.Execute(DateTime.Now);
if (cacheTime.AbsoluteExpiration > DateTime.Now)
var actionExecutionTimestamp = DateTimeOffset.Now;
var cacheTime = CacheTimeQuery.Execute(actionExecutionTimestamp.DateTime);
if (cacheTime.AbsoluteExpiration > actionExecutionTimestamp)
{
var httpConfig = actionExecutedContext.Request.GetConfiguration();
var config = httpConfig.CacheOutputConfiguration();
Expand Down Expand Up @@ -261,14 +263,19 @@ public override async Task OnActionExecutedAsync(HttpActionExecutedContext actio
_webApiCache.Add(cachekey + Constants.EtagKey,
etag,
cacheTime.AbsoluteExpiration, baseKey);


_webApiCache.Add(cachekey + Constants.GenerationTimestampKey,
actionExecutionTimestamp.ToString(),
cacheTime.AbsoluteExpiration, baseKey);
}
}
}

ApplyCacheHeaders(actionExecutedContext.ActionContext.Response, cacheTime);
ApplyCacheHeaders(actionExecutedContext.ActionContext.Response, cacheTime, actionExecutionTimestamp);
}

protected virtual void ApplyCacheHeaders(HttpResponseMessage response, CacheTime cacheTime)
protected virtual void ApplyCacheHeaders(HttpResponseMessage response, CacheTime cacheTime, DateTimeOffset? contentGenerationTimestamp = null)
{
if (cacheTime.ClientTimeSpan > TimeSpan.Zero || MustRevalidate || Private)
{
Expand All @@ -287,6 +294,10 @@ protected virtual void ApplyCacheHeaders(HttpResponseMessage response, CacheTime
response.Headers.CacheControl = new CacheControlHeaderValue { NoCache = true };
response.Headers.Add("Pragma", "no-cache");
}
if ((response.Content != null) && contentGenerationTimestamp.HasValue)
{
response.Content.Headers.LastModified = contentGenerationTimestamp.Value;
}
}

protected virtual string CreateEtag(HttpActionExecutedContext actionExecutedContext, string cachekey, CacheTime cacheTime)
Expand Down
20 changes: 14 additions & 6 deletions src/WebApi.OutputCache.V2/DefaultCacheKeyGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
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 key = MakeBaseKey(context);
var parameters = FormatParameters(context, excludeQueryString);

return string.Format("{0}{1}:{2}", key, parameters, mediaType);
}

protected virtual string MakeBaseKey(HttpActionContext context)
{
var controller = context.ControllerContext.ControllerDescriptor.ControllerType.FullName;
var action = context.ActionDescriptor.ActionName;
var key = context.Request.GetConfiguration().CacheOutputConfiguration().MakeBaseCachekey(controller, action);
return context.Request.GetConfiguration().CacheOutputConfiguration().MakeBaseCachekey(controller, action);
}

protected virtual string FormatParameters(HttpActionContext context, bool excludeQueryString)
{
var actionParameters = context.ActionArguments.Where(x => x.Value != null).Select(x => x.Key + "=" + GetValue(x.Value));

string parameters;
Expand Down Expand Up @@ -45,9 +55,7 @@ public virtual string MakeCacheKey(HttpActionContext context, MediaTypeHeaderVal
}

if (parameters == "-") parameters = string.Empty;

var cachekey = string.Format("{0}{1}:{2}", key, parameters, mediaType);
return cachekey;
return parameters;
}

private string GetJsonpCallback(HttpRequestMessage request)
Expand Down
22 changes: 22 additions & 0 deletions src/WebApi.OutputCache.V2/PerUserCacheKeyGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Net.Http.Headers;
using System.Web.Http.Controllers;

namespace WebApi.OutputCache.V2
{
public class PerUserCacheKeyGenerator : DefaultCacheKeyGenerator
{
public override string MakeCacheKey(HttpActionContext context, MediaTypeHeaderValue mediaType, bool excludeQueryString = false)
{
var baseKey = MakeBaseKey(context);
var parameters = FormatParameters(context, excludeQueryString);
var userIdentity = FormatUserIdentity(context);

return string.Format("{0}{1}:{2}:{3}", baseKey, parameters, userIdentity, mediaType);
}

protected virtual string FormatUserIdentity(HttpActionContext context)
{
return context.RequestContext.Principal.Identity.Name.ToLower();
}
}
}
1 change: 1 addition & 0 deletions src/WebApi.OutputCache.V2/WebApi.OutputCache.V2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
<Compile Include="ICacheKeyGenerator.cs" />
<Compile Include="IgnoreCacheOutputAttribute.cs" />
<Compile Include="InvalidateCacheOutputAttribute.cs" />
<Compile Include="PerUserCacheKeyGenerator.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="CacheOutputAttribute.cs" />
<Compile Include="TimeAttributes\CacheOutputUntilCacheAttribute.cs" />
Expand Down
60 changes: 60 additions & 0 deletions test/WebApi.OutputCache.V2.Tests/CacheKeyGenerationTestsBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using NUnit.Framework;
using System;
using System.Net.Http.Headers;
using System.Web.Http.Controllers;

namespace WebApi.OutputCache.V2.Tests
{
/// <summary>
/// Base class for implementing tests for the generation of cache keys (meaning: implementations of the <see cref="ICacheKeyGenerator"/>
/// </summary>
public abstract class CacheKeyGenerationTestsBase<TCacheKeyGenerator> where TCacheKeyGenerator : ICacheKeyGenerator
{
private const string ArgumentKey = "filterExpression";
private const string ArgumentValue = "val";
protected HttpActionContext context;
protected MediaTypeHeaderValue mediaType;
protected Uri requestUri;
protected TCacheKeyGenerator cacheKeyGenerator;
protected string BaseCacheKey;

[SetUp]
public virtual void Setup()
{
requestUri = new Uri("http://localhost:8080/cacheKeyGeneration?filter=val");
var controllerType = typeof(TestControllers.CacheKeyGenerationController);
var actionMethodInfo = controllerType.GetMethod("Get");
var controllerDescriptor = new HttpControllerDescriptor() { ControllerType = controllerType };
var actionDescriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, actionMethodInfo);
var request = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, requestUri.AbsoluteUri);

context = new HttpActionContext(
new HttpControllerContext() { ControllerDescriptor = controllerDescriptor, Request = request },
actionDescriptor
);
mediaType = new MediaTypeHeaderValue("application/json");

BaseCacheKey = new CacheOutputConfiguration(null).MakeBaseCachekey((TestControllers.CacheKeyGenerationController c) => c.Get(String.Empty));
cacheKeyGenerator = BuildCacheKeyGenerator();
}

protected abstract TCacheKeyGenerator BuildCacheKeyGenerator();

protected virtual void AssertCacheKeysBasicFormat(string cacheKey)
{
Assert.IsNotNull(cacheKey);
StringAssert.StartsWith(BaseCacheKey, cacheKey, "Key does not start with BaseKey");
StringAssert.EndsWith(mediaType.ToString(), cacheKey, "Key does not end with MediaType");
}

protected void AddActionArgumentsToContext()
{
context.ActionArguments.Add(ArgumentKey, ArgumentValue);
}

protected string FormatActionArgumentsForKeyAssertion()
{
return String.Format("{0}={1}", ArgumentKey, ArgumentValue);
}
}
}
56 changes: 56 additions & 0 deletions test/WebApi.OutputCache.V2.Tests/DefaultCacheKeyGeneratorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using NUnit.Framework;
using System;

namespace WebApi.OutputCache.V2.Tests
{
[TestFixture]
public class DefaultCacheKeyGeneratorTests : CacheKeyGenerationTestsBase<DefaultCacheKeyGenerator>
{
protected override DefaultCacheKeyGenerator BuildCacheKeyGenerator()
{
return new DefaultCacheKeyGenerator();
}

[Test]
public void NoParametersIncludeQueryString_ShouldReturnBaseKeyAndQueryStringAndMediaTypeConcatenated()
{
var cacheKey = cacheKeyGenerator.MakeCacheKey(context, mediaType, false);

AssertCacheKeysBasicFormat(cacheKey);
Assert.AreEqual(String.Format("{0}-{1}:{2}", BaseCacheKey, requestUri.Query.Substring(1), mediaType), cacheKey,
"Key does not match expected <BaseKey>-<QueryString>:<MediaType>");
}

[Test]
public void NoParametersExcludeQueryString_ShouldReturnBaseKeyAndMediaTypeConcatenated()
{
var cacheKey = cacheKeyGenerator.MakeCacheKey(context, mediaType, true);

AssertCacheKeysBasicFormat(cacheKey);
Assert.AreEqual(String.Format("{0}:{1}", BaseCacheKey, mediaType), cacheKey,
"Key does not match expected <BaseKey>:<MediaType>");
}

[Test]
public void WithParametersIncludeQueryString_ShouldReturnBaseKeyAndArgumentsAndQueryStringAndMediaTypeConcatenated()
{
AddActionArgumentsToContext();
var cacheKey = cacheKeyGenerator.MakeCacheKey(context, mediaType, false);

AssertCacheKeysBasicFormat(cacheKey);
Assert.AreEqual(String.Format("{0}-{1}&{2}:{3}", BaseCacheKey, FormatActionArgumentsForKeyAssertion(), requestUri.Query.Substring(1), mediaType), cacheKey,
"Key does not match expected <BaseKey>-<Arguments>&<QueryString>:<MediaType>");
}

[Test]
public void WithParametersExcludeQueryString_ShouldReturnBaseKeyAndArgumentsAndMediaTypeConcatenated()
{
AddActionArgumentsToContext();
var cacheKey = cacheKeyGenerator.MakeCacheKey(context, mediaType, true);

AssertCacheKeysBasicFormat(cacheKey);
Assert.AreEqual(String.Format("{0}-{1}:{2}", BaseCacheKey, FormatActionArgumentsForKeyAssertion(), mediaType), cacheKey,
"Key does not match expected <BaseKey>-<Arguments>:<MediaType>");
}
}
}
71 changes: 71 additions & 0 deletions test/WebApi.OutputCache.V2.Tests/PerUserCacheKeyGeneratorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using NUnit.Framework;
using System;
using System.Security.Principal;

namespace WebApi.OutputCache.V2.Tests
{
[TestFixture]
public class PerUserCacheKeyGeneratorTests : CacheKeyGenerationTestsBase<PerUserCacheKeyGenerator>
{
private const string UserIdentityName = "SomeUserIDon'tMind";

[SetUp]
public override void Setup()
{
base.Setup();
context.RequestContext.Principal = new GenericPrincipal(new GenericIdentity(UserIdentityName), new string[0]);
}

protected override PerUserCacheKeyGenerator BuildCacheKeyGenerator()
{
return new PerUserCacheKeyGenerator();
}

private string FormatUserIdentityForAssertion()
{
return UserIdentityName.ToLower();
}

[Test]
public void NoParametersIncludeQueryString_ShouldReturnBaseKeyAndQueryStringAndUserIdentityAndMediaTypeConcatenated()
{
var cacheKey = cacheKeyGenerator.MakeCacheKey(context, mediaType, false);

AssertCacheKeysBasicFormat(cacheKey);
Assert.AreEqual(String.Format("{0}-{1}:{2}:{3}", BaseCacheKey, requestUri.Query.Substring(1), FormatUserIdentityForAssertion(), mediaType), cacheKey,
"Key does not match expected <BaseKey>-<QueryString>:<UserIdentity>:<MediaType>");
}

[Test]
public void NoParametersExcludeQueryString_ShouldReturnBaseKeyAndUserIdentityAndMediaTypeConcatenated()
{
var cacheKey = cacheKeyGenerator.MakeCacheKey(context, mediaType, true);

AssertCacheKeysBasicFormat(cacheKey);
Assert.AreEqual(String.Format("{0}:{1}:{2}", BaseCacheKey, FormatUserIdentityForAssertion(), mediaType), cacheKey,
"Key does not match expected <BaseKey>:<UserIdentity>:<MediaType>");
}

[Test]
public void WithParametersIncludeQueryString_ShouldReturnBaseKeyAndArgumentsAndQueryStringAndUserIdentityAndMediaTypeConcatenated()
{
AddActionArgumentsToContext();
var cacheKey = cacheKeyGenerator.MakeCacheKey(context, mediaType, false);

AssertCacheKeysBasicFormat(cacheKey);
Assert.AreEqual(String.Format("{0}-{1}&{2}:{3}:{4}", BaseCacheKey, FormatActionArgumentsForKeyAssertion(), requestUri.Query.Substring(1), FormatUserIdentityForAssertion(), mediaType), cacheKey,
"Key does not match expected <BaseKey>-<Arguments>&<QueryString>:<UserIdentity>:<MediaType>");
}

[Test]
public void WithParametersExcludeQueryString_ShouldReturnBaseKeyAndArgumentsAndUserIdentityAndMediaTypeConcatenated()
{
AddActionArgumentsToContext();
var cacheKey = cacheKeyGenerator.MakeCacheKey(context, mediaType, true);

AssertCacheKeysBasicFormat(cacheKey);
Assert.AreEqual(String.Format("{0}-{1}:{2}:{3}", BaseCacheKey, FormatActionArgumentsForKeyAssertion(), FormatUserIdentityForAssertion(), mediaType), cacheKey,
"Key does not match expected <BaseKey>-<Arguments>:<UserIdentity>:<MediaType>");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;

namespace WebApi.OutputCache.V2.Tests.TestControllers
{
/// <summary>
/// Controller needed for generating the <see cref="System.Web.Http.Controllers.HttpActionContext" /> needed for testing the <see cref="ICacheKeyGenerator"/> implementations
/// </summary>
[RoutePrefix("cacheKeyGeneration")]
public class CacheKeyGenerationController : ApiController
{
private readonly string[] Values = new string[] { "first", "second", "third" };

[Route("")]
public IEnumerable<string> Get([FromUri(Name="filter")]string filterExpression)
{
return String.IsNullOrWhiteSpace(filterExpression) ? Values : Values.Where(x => x.Contains(filterExpression));
}

[Route("{index}")]
public string GetByIndex(int index)
{
return Values[index];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
<ItemGroup>
<Compile Include="CacheKeyGeneratorRegistrationTests.cs" />
<Compile Include="CacheKeyGeneratorTests.cs" />
<Compile Include="CacheKeyGenerationTestsBase.cs" />
<Compile Include="ClientSideTests.cs">
<SubType>Code</SubType>
</Compile>
Expand All @@ -83,6 +84,8 @@
<Compile Include="ConnegTests.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="PerUserCacheKeyGeneratorTests.cs" />
<Compile Include="DefaultCacheKeyGeneratorTests.cs" />
<Compile Include="InlineInvalidateTests.cs">
<SubType>Code</SubType>
</Compile>
Expand All @@ -95,6 +98,7 @@
<Compile Include="TestControllers\AutoInvalidateController.cs" />
<Compile Include="TestControllers\AutoInvalidateWithTypeController.cs" />
<Compile Include="TestControllers\CacheKeyController.cs" />
<Compile Include="TestControllers\CacheKeyGenerationController.cs" />
<Compile Include="TestControllers\IgnoreController.cs" />
<Compile Include="TestControllers\InlineInvalidateController.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
Expand Down

0 comments on commit 4a8f908

Please sign in to comment.