Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
Add support for VaryByQueryKey #2894 (WIP)
Browse files Browse the repository at this point in the history
  • Loading branch information
JunTaoLuo committed Oct 7, 2016
1 parent 45337c1 commit e8ff8fe
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 18 deletions.
7 changes: 6 additions & 1 deletion src/Microsoft.AspNetCore.Mvc.Core/CacheProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class CacheProfile
/// <summary>
/// Gets or sets the duration in seconds for which the response is cached.
/// If this property is set to a non null value,
/// the "max-age" in "Cache-control" header is set in the
/// the "max-age" in "Cache-control" header is set in the
/// <see cref="Microsoft.AspNetCore.Http.HttpContext.Response" />.
/// </summary>
public int? Duration { get; set; }
Expand All @@ -36,5 +36,10 @@ public class CacheProfile
/// Gets or sets the value for the Vary header in <see cref="Microsoft.AspNetCore.Http.HttpContext.Response" />.
/// </summary>
public string VaryByHeader { get; set; }

/// <summary>
/// TODO:
/// </summary>
public string[] VaryByQueryKeys { get; set; }
}
}
21 changes: 21 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.Core/Internal/ResponseCacheFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.ResponseCaching;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNetCore.Mvc.Internal
Expand All @@ -21,6 +22,7 @@ public class ResponseCacheFilter : IActionFilter, IResponseCacheFilter
private ResponseCacheLocation? _cacheLocation;
private bool? _cacheNoStore;
private string _cacheVaryByHeader;
private string[] _cacheVaryByQueryKey;

/// <summary>
/// Creates a new instance of <see cref="ResponseCacheFilter"/>
Expand Down Expand Up @@ -73,6 +75,15 @@ public string VaryByHeader
set { _cacheVaryByHeader = value; }
}

/// <summary>
/// TODO:
/// </summary>
public string[] VaryByQueryKeys
{
get { return _cacheVaryByQueryKey ?? _cacheProfile.VaryByQueryKeys; }
set { _cacheVaryByQueryKey = value; }
}

/// <inheritdoc />
public void OnActionExecuting(ActionExecutingContext context)
{
Expand Down Expand Up @@ -110,6 +121,16 @@ public void OnActionExecuting(ActionExecutingContext context)
headers[HeaderNames.Vary] = VaryByHeader;
}

if (VaryByQueryKeys != null)
{
var responseCacheFeature = context.HttpContext.GetResponseCacheFeature(); // TODO: replace this call
if (responseCacheFeature == null)
{
throw new InvalidOperationException($"The response cache middleware must be added when using the {nameof(VaryByQueryKeys)} feature.");
}
responseCacheFeature.VaryByQueryKeys = VaryByQueryKeys;
}

if (NoStore)
{
headers[HeaderNames.CacheControl] = "no-store";
Expand Down
9 changes: 8 additions & 1 deletion src/Microsoft.AspNetCore.Mvc.Core/ResponseCacheAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ public bool NoStore
/// </summary>
public string VaryByHeader { get; set; }

/// <summary>
/// TODO:
/// </summary>
public string[] VaryByQueryKeys { get; set; }

/// <summary>
/// Gets or sets the value of the cache profile name.
/// </summary>
Expand Down Expand Up @@ -117,6 +122,7 @@ public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
_noStore = _noStore ?? selectedProfile?.NoStore;
_location = _location ?? selectedProfile?.Location;
VaryByHeader = VaryByHeader ?? selectedProfile?.VaryByHeader;
VaryByQueryKeys = VaryByQueryKeys ?? selectedProfile?.VaryByQueryKeys;

// ResponseCacheFilter cannot take any null values. Hence, if there are any null values,
// the properties convert them to their defaults and are passed on.
Expand All @@ -125,7 +131,8 @@ public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
Duration = _duration,
Location = _location,
NoStore = _noStore,
VaryByHeader = VaryByHeader
VaryByHeader = VaryByHeader,
VaryByQueryKeys = VaryByQueryKeys
});
}
}
Expand Down
1 change: 1 addition & 0 deletions src/Microsoft.AspNetCore.Mvc.Core/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"Microsoft.AspNetCore.Hosting.Abstractions": "1.1.0-*",
"Microsoft.AspNetCore.Http": "1.1.0-*",
"Microsoft.AspNetCore.Mvc.Abstractions": "1.1.0-*",
"Microsoft.AspNetCore.ResponseCaching": "0.1.0-*",
"Microsoft.AspNetCore.Routing": "1.1.0-*",
"Microsoft.AspNetCore.Routing.DecisionTree.Sources": {
"type": "build",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.ResponseCaching;
using Microsoft.AspNetCore.Routing;
using Xunit;
using System.Linq;

namespace Microsoft.AspNetCore.Mvc.Internal
{
Expand Down Expand Up @@ -210,7 +212,7 @@ public void OnActionExecuting_DoesNotSetLocationOrDuration_IfNoStoreIsSet(
Assert.Equal(output, context.HttpContext.Response.Headers["Cache-control"]);
}

public static IEnumerable<object[]> VaryData
public static IEnumerable<object[]> VaryByHeaderData
{
get
{
Expand Down Expand Up @@ -267,8 +269,8 @@ public static IEnumerable<object[]> VaryData
}

[Theory]
[MemberData(nameof(VaryData))]
public void ResponseCacheCanSetVary(ResponseCacheFilter cache, string varyOutput, string cacheControlOutput)
[MemberData(nameof(VaryByHeaderData))]
public void ResponseCacheCanSetVaryByHeader(ResponseCacheFilter cache, string varyOutput, string cacheControlOutput)
{
// Arrange
var context = GetActionExecutingContext(new List<IFilterMetadata> { cache });
Expand All @@ -281,14 +283,136 @@ public void ResponseCacheCanSetVary(ResponseCacheFilter cache, string varyOutput
Assert.Equal(cacheControlOutput, context.HttpContext.Response.Headers["Cache-control"]);
}

public static IEnumerable<object[]> VaryByQueryKeyData
{
get
{
yield return new object[] {
new ResponseCacheFilter(
new CacheProfile
{
Duration = 10, Location = ResponseCacheLocation.Any,
NoStore = false, VaryByQueryKeys = new string[0]
}),
new string[0],
"public,max-age=10" };
yield return new object[] {
new ResponseCacheFilter(
new CacheProfile
{
Duration = 10, Location = ResponseCacheLocation.Any,
NoStore = false, VaryByQueryKeys = new string[] { null }
}),
new string[] { null },
"public,max-age=10" };
yield return new object[] {
new ResponseCacheFilter(
new CacheProfile
{
Duration = 10, Location = ResponseCacheLocation.Any,
NoStore = false, VaryByQueryKeys = new[] { "" }
}),
new[] { "" },
"public,max-age=10" };
yield return new object[] {
new ResponseCacheFilter(
new CacheProfile
{
Duration = 10, Location = ResponseCacheLocation.Any,
NoStore = false, VaryByQueryKeys = new[] { "Accept" }
}),
new[] { "Accept" },
"public,max-age=10" };
yield return new object[] {
new ResponseCacheFilter(
new CacheProfile
{
Duration = 0, Location= ResponseCacheLocation.Any,
NoStore = true, VaryByQueryKeys = new[] { "Accept" }
}),
new[] { "Accept" },
"no-store"
};
yield return new object[] {
new ResponseCacheFilter(
new CacheProfile
{
Duration = 10, Location = ResponseCacheLocation.Client,
NoStore = false, VaryByQueryKeys = new[] { "Accept" }
}),
new[] { "Accept" },
"private,max-age=10"
};
yield return new object[] {
new ResponseCacheFilter(
new CacheProfile
{
Duration = 10, Location = ResponseCacheLocation.Client,
NoStore = false, VaryByQueryKeys = new[] { "Accept", "Test" }
}),
new[] { "Accept", "Test" },
"private,max-age=10"
};
yield return new object[] {
new ResponseCacheFilter(
new CacheProfile
{
Duration = 31536000, Location = ResponseCacheLocation.Any,
NoStore = false, VaryByQueryKeys = new[] { "Accept", "Test" }
}),
new[] { "Accept", "Test" },
"public,max-age=31536000"
};
}
}

[Theory]
[MemberData(nameof(VaryByQueryKeyData))]
public void ResponseCacheCanSetVaryByQueryKeys(ResponseCacheFilter cache, string[] varyOutput, string cacheControlOutput)
{
// Arrange
var context = GetActionExecutingContext(new List<IFilterMetadata> { cache });
context.HttpContext.Features.Set(new ResponseCacheFeature());

// Act
cache.OnActionExecuting(context);

// Assert
Assert.True(context.HttpContext.Features.Get<ResponseCacheFeature>().VaryByQueryKeys.SequenceEqual(varyOutput));
Assert.Equal(cacheControlOutput, context.HttpContext.Response.Headers["Cache-control"]);
}

[Fact]
public void NonEmptyVaryByQueryKeys_WithoutConfiguringMiddleware_Throws()
{
// Arrange
var cache = new ResponseCacheFilter(
new CacheProfile
{
Duration = 0,
Location = ResponseCacheLocation.None,
NoStore = true,
VaryByHeader = null,
VaryByQueryKeys = new[] { "Test" }
});
var context = GetActionExecutingContext(new List<IFilterMetadata> { cache });

// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => cache.OnActionExecuting(context));
Assert.Equal($"The response cache middleware must be added when using the VaryByQueryKeys feature.", exception.Message);
}

[Fact]
public void SetsPragmaOnNoCache()
{
// Arrange
var cache = new ResponseCacheFilter(
new CacheProfile
{
Duration = 0, Location = ResponseCacheLocation.None, NoStore = true, VaryByHeader = null
Duration = 0,
Location = ResponseCacheLocation.None,
NoStore = true,
VaryByHeader = null
});
var context = GetActionExecutingContext(new List<IFilterMetadata> { cache });

Expand Down
Loading

0 comments on commit e8ff8fe

Please sign in to comment.