Add support for VaryByQueryKey #2894

This commit is contained in:
John Luo 2016-10-05 19:12:41 -07:00
parent 3004fb822b
commit d8c6c4ab34
9 changed files with 422 additions and 171 deletions

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Mvc
/// <summary> /// <summary>
/// Gets or sets the duration in seconds for which the response is cached. /// Gets or sets the duration in seconds for which the response is cached.
/// If this property is set to a non null value, /// 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" />. /// <see cref="Microsoft.AspNetCore.Http.HttpContext.Response" />.
/// </summary> /// </summary>
public int? Duration { get; set; } public int? Duration { get; set; }
@ -36,5 +36,13 @@ namespace Microsoft.AspNetCore.Mvc
/// Gets or sets the value for the Vary header in <see cref="Microsoft.AspNetCore.Http.HttpContext.Response" />. /// Gets or sets the value for the Vary header in <see cref="Microsoft.AspNetCore.Http.HttpContext.Response" />.
/// </summary> /// </summary>
public string VaryByHeader { get; set; } public string VaryByHeader { get; set; }
/// <summary>
/// Gets or sets the query keys to vary by.
/// </summary>
/// <remarks>
/// <see cref="VaryByQueryKeys"/> requires the response cache middleware.
/// </remarks>
public string[] VaryByQueryKeys { get; set; }
} }
} }

View File

@ -7,6 +7,7 @@ using System.Linq;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.ResponseCaching;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc.Internal namespace Microsoft.AspNetCore.Mvc.Internal
@ -21,6 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private ResponseCacheLocation? _cacheLocation; private ResponseCacheLocation? _cacheLocation;
private bool? _cacheNoStore; private bool? _cacheNoStore;
private string _cacheVaryByHeader; private string _cacheVaryByHeader;
private string[] _cacheVaryByQueryKeys;
/// <summary> /// <summary>
/// Creates a new instance of <see cref="ResponseCacheFilter"/> /// Creates a new instance of <see cref="ResponseCacheFilter"/>
@ -73,6 +75,18 @@ namespace Microsoft.AspNetCore.Mvc.Internal
set { _cacheVaryByHeader = value; } set { _cacheVaryByHeader = value; }
} }
/// <summary>
/// Gets or sets the query keys to vary by.
/// </summary>
/// <remarks>
/// <see cref="VaryByQueryKeys"/> requires the response cache middleware.
/// </remarks>
public string[] VaryByQueryKeys
{
get { return _cacheVaryByQueryKeys ?? _cacheProfile.VaryByQueryKeys; }
set { _cacheVaryByQueryKeys = value; }
}
/// <inheritdoc /> /// <inheritdoc />
public void OnActionExecuting(ActionExecutingContext context) public void OnActionExecuting(ActionExecutingContext context)
{ {
@ -110,6 +124,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal
headers[HeaderNames.Vary] = VaryByHeader; headers[HeaderNames.Vary] = VaryByHeader;
} }
if (VaryByQueryKeys != null)
{
var responseCachingFeature = context.HttpContext.Features.Get<IResponseCachingFeature>();
if (responseCachingFeature == null)
{
throw new InvalidOperationException(Resources.FormatVaryByQueryKeys_Requires_ResponseCachingMiddleware(nameof(VaryByQueryKeys)));
}
responseCachingFeature.VaryByQueryKeys = VaryByQueryKeys;
}
if (NoStore) if (NoStore)
{ {
headers[HeaderNames.CacheControl] = "no-store"; headers[HeaderNames.CacheControl] = "no-store";

View File

@ -1338,6 +1338,22 @@ namespace Microsoft.AspNetCore.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("FormCollectionModelBinder_CannotBindToFormCollection"), p0, p1, p2); return string.Format(CultureInfo.CurrentCulture, GetString("FormCollectionModelBinder_CannotBindToFormCollection"), p0, p1, p2);
} }
/// <summary>
/// VaryByQueryKeys requires the response cache middleware.
/// </summary>
internal static string VaryByQueryKeys_Requires_ResponseCachingMiddleware
{
get { return GetString("VaryByQueryKeys_Requires_ResponseCachingMiddleware"); }
}
/// <summary>
/// VaryByQueryKeys requires the response cache middleware.
/// </summary>
internal static string FormatVaryByQueryKeys_Requires_ResponseCachingMiddleware(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("VaryByQueryKeys_Requires_ResponseCachingMiddleware"), p0);
}
private static string GetString(string name, params string[] formatterNames) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -376,4 +376,7 @@
<data name="FormCollectionModelBinder_CannotBindToFormCollection" xml:space="preserve"> <data name="FormCollectionModelBinder_CannotBindToFormCollection" xml:space="preserve">
<value>The '{0}' cannot bind to a model of type '{1}'. Change the model type to '{2}' instead.</value> <value>The '{0}' cannot bind to a model of type '{1}'. Change the model type to '{2}' instead.</value>
</data> </data>
<data name="VaryByQueryKeys_Requires_ResponseCachingMiddleware" xml:space="preserve">
<value>'{0}' requires the response cache middleware.</value>
</data>
</root> </root>

View File

@ -77,6 +77,14 @@ namespace Microsoft.AspNetCore.Mvc
/// </summary> /// </summary>
public string VaryByHeader { get; set; } public string VaryByHeader { get; set; }
/// <summary>
/// Gets or sets the query keys to vary by.
/// </summary>
/// <remarks>
/// <see cref="VaryByQueryKeys"/> requires the response cache middleware.
/// </remarks>
public string[] VaryByQueryKeys { get; set; }
/// <summary> /// <summary>
/// Gets or sets the value of the cache profile name. /// Gets or sets the value of the cache profile name.
/// </summary> /// </summary>
@ -117,6 +125,7 @@ namespace Microsoft.AspNetCore.Mvc
_noStore = _noStore ?? selectedProfile?.NoStore; _noStore = _noStore ?? selectedProfile?.NoStore;
_location = _location ?? selectedProfile?.Location; _location = _location ?? selectedProfile?.Location;
VaryByHeader = VaryByHeader ?? selectedProfile?.VaryByHeader; VaryByHeader = VaryByHeader ?? selectedProfile?.VaryByHeader;
VaryByQueryKeys = VaryByQueryKeys ?? selectedProfile?.VaryByQueryKeys;
// ResponseCacheFilter cannot take any null values. Hence, if there are any null values, // ResponseCacheFilter cannot take any null values. Hence, if there are any null values,
// the properties convert them to their defaults and are passed on. // the properties convert them to their defaults and are passed on.
@ -125,7 +134,8 @@ namespace Microsoft.AspNetCore.Mvc
Duration = _duration, Duration = _duration,
Location = _location, Location = _location,
NoStore = _noStore, NoStore = _noStore,
VaryByHeader = VaryByHeader VaryByHeader = VaryByHeader,
VaryByQueryKeys = VaryByQueryKeys,
}); });
} }
} }

View File

@ -24,6 +24,7 @@
"Microsoft.AspNetCore.Hosting.Abstractions": "1.1.0-*", "Microsoft.AspNetCore.Hosting.Abstractions": "1.1.0-*",
"Microsoft.AspNetCore.Http": "1.1.0-*", "Microsoft.AspNetCore.Http": "1.1.0-*",
"Microsoft.AspNetCore.Mvc.Abstractions": "1.1.0-*", "Microsoft.AspNetCore.Mvc.Abstractions": "1.1.0-*",
"Microsoft.AspNetCore.ResponseCaching.Abstractions": "1.0.0-*",
"Microsoft.AspNetCore.Routing": "1.1.0-*", "Microsoft.AspNetCore.Routing": "1.1.0-*",
"Microsoft.AspNetCore.Routing.DecisionTree.Sources": { "Microsoft.AspNetCore.Routing.DecisionTree.Sources": {
"type": "build", "type": "build",

View File

@ -3,10 +3,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.ResponseCaching;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.Net.Http.Headers;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.Internal namespace Microsoft.AspNetCore.Mvc.Internal
@ -64,86 +67,113 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ex.Message); ex.Message);
} }
public static IEnumerable<object[]> CacheControlData public static TheoryData<ResponseCacheFilter, string> CacheControlData
{ {
get get
{ {
yield return new object[] { return new TheoryData<ResponseCacheFilter, string>
new ResponseCacheFilter( {
new CacheProfile {
{ new ResponseCacheFilter(
Duration = 0, Location = ResponseCacheLocation.Any, NoStore = true, VaryByHeader = null new CacheProfile
}), {
"no-store" Duration = 0,
}; Location = ResponseCacheLocation.Any,
// If no-store is set, then location is ignored. NoStore = true,
yield return new object[] { VaryByHeader = null
new ResponseCacheFilter( }),
new CacheProfile "no-store"
{ },
Duration = 0, Location = ResponseCacheLocation.Client, NoStore = true, VaryByHeader = null // If no-store is set, then location is ignored.
}), {
"no-store" new ResponseCacheFilter(
}; new CacheProfile
yield return new object[] { {
new ResponseCacheFilter( Duration = 0,
new CacheProfile Location = ResponseCacheLocation.Client,
{ NoStore = true,
Duration = 0, Location = ResponseCacheLocation.Any, NoStore = true, VaryByHeader = null VaryByHeader = null
}), }),
"no-store" "no-store"
}; },
// If no-store is set, then duration is ignored. {
yield return new object[] { new ResponseCacheFilter(
new ResponseCacheFilter( new CacheProfile
new CacheProfile {
{ Duration = 0,
Duration = 100, Location = ResponseCacheLocation.Any, NoStore = true, VaryByHeader = null Location = ResponseCacheLocation.Any,
}), NoStore = true,
"no-store" VaryByHeader = null
}; }),
"no-store"
yield return new object[] { },
new ResponseCacheFilter( // If no-store is set, then duration is ignored.
new CacheProfile {
{ new ResponseCacheFilter(
Duration = 10, Location = ResponseCacheLocation.Client, new CacheProfile
NoStore = false, VaryByHeader = null {
}), Duration = 100,
"private,max-age=10" Location = ResponseCacheLocation.Any,
}; NoStore = true,
yield return new object[] { VaryByHeader = null
new ResponseCacheFilter( }),
new CacheProfile "no-store"
{ },
Duration = 10, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = null {
}), new ResponseCacheFilter(
"public,max-age=10" new CacheProfile
}; {
yield return new object[] { Duration = 10,
new ResponseCacheFilter( Location = ResponseCacheLocation.Client,
new CacheProfile NoStore = false,
{ VaryByHeader = null
Duration = 10, Location = ResponseCacheLocation.None, NoStore = false, VaryByHeader = null }),
}), "private,max-age=10"
"no-cache,max-age=10" },
}; {
yield return new object[] { new ResponseCacheFilter(
new ResponseCacheFilter( new CacheProfile
new CacheProfile {
{ Duration = 10,
Duration = 31536000, Location = ResponseCacheLocation.Any, Location = ResponseCacheLocation.Any,
NoStore = false, VaryByHeader = null NoStore = false,
}), VaryByHeader = null
"public,max-age=31536000" }),
}; "public,max-age=10"
yield return new object[] { },
new ResponseCacheFilter( {
new CacheProfile new ResponseCacheFilter(
{ new CacheProfile
Duration = 20, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = null {
}), Duration = 10,
"public,max-age=20" Location = ResponseCacheLocation.None,
NoStore = false,
VaryByHeader = null
}),
"no-cache,max-age=10"
},
{
new ResponseCacheFilter(
new CacheProfile
{
Duration = 31536000,
Location = ResponseCacheLocation.Any,
NoStore = false,
VaryByHeader = null
}),
"public,max-age=31536000"
},
{
new ResponseCacheFilter(
new CacheProfile
{
Duration = 20,
Location = ResponseCacheLocation.Any,
NoStore = false,
VaryByHeader = null
}),
"public,max-age=20"
}
}; };
} }
} }
@ -162,35 +192,47 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.Equal(output, context.HttpContext.Response.Headers["Cache-control"]); Assert.Equal(output, context.HttpContext.Response.Headers["Cache-control"]);
} }
public static IEnumerable<object[]> NoStoreData public static TheoryData<ResponseCacheFilter, string> NoStoreData
{ {
get get
{ {
// If no-store is set, then location is ignored. return new TheoryData<ResponseCacheFilter, string>
yield return new object[] { {
new ResponseCacheFilter( // If no-store is set, then location is ignored.
new CacheProfile {
{ new ResponseCacheFilter(
Duration = 0, Location = ResponseCacheLocation.Client, NoStore = true, VaryByHeader = null new CacheProfile
}), {
"no-store" Duration = 0,
}; Location = ResponseCacheLocation.Client,
yield return new object[] { NoStore = true,
new ResponseCacheFilter( VaryByHeader = null
new CacheProfile }),
{ "no-store"
Duration = 0, Location = ResponseCacheLocation.Any, NoStore = true, VaryByHeader = null },
}), {
"no-store" new ResponseCacheFilter(
}; new CacheProfile
// If no-store is set, then duration is ignored. {
yield return new object[] { Duration = 0,
new ResponseCacheFilter( Location = ResponseCacheLocation.Any,
new CacheProfile NoStore = true,
{ VaryByHeader = null
Duration = 100, Location = ResponseCacheLocation.Any, NoStore = true, VaryByHeader = null }),
}), "no-store"
"no-store" },
// If no-store is set, then duration is ignored.
{
new ResponseCacheFilter(
new CacheProfile
{
Duration = 100,
Location = ResponseCacheLocation.Any,
NoStore = true,
VaryByHeader = null
}),
"no-store"
}
}; };
} }
} }
@ -210,65 +252,79 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.Equal(output, context.HttpContext.Response.Headers["Cache-control"]); Assert.Equal(output, context.HttpContext.Response.Headers["Cache-control"]);
} }
public static IEnumerable<object[]> VaryData public static TheoryData<ResponseCacheFilter, string, string> VaryByHeaderData
{ {
get get
{ {
yield return new object[] { return new TheoryData<ResponseCacheFilter, string, string>
new ResponseCacheFilter( {
new CacheProfile {
{ new ResponseCacheFilter(
Duration = 10, Location = ResponseCacheLocation.Any, new CacheProfile
NoStore = false, VaryByHeader = "Accept" {
}), Duration = 10,
"Accept", Location = ResponseCacheLocation.Any,
"public,max-age=10" }; NoStore = false,
yield return new object[] { VaryByHeader = "Accept"
new ResponseCacheFilter( }),
new CacheProfile "Accept",
{ "public,max-age=10"
Duration = 0, Location= ResponseCacheLocation.Any, },
NoStore = true, VaryByHeader = "Accept" {
}), new ResponseCacheFilter(
"Accept", new CacheProfile
"no-store" {
}; Duration = 0,
yield return new object[] { Location = ResponseCacheLocation.Any,
new ResponseCacheFilter( NoStore = true,
new CacheProfile VaryByHeader = "Accept"
{ }),
Duration = 10, Location = ResponseCacheLocation.Client, "Accept",
NoStore = false, VaryByHeader = "Accept" "no-store"
}), },
"Accept", {
"private,max-age=10" new ResponseCacheFilter(
}; new CacheProfile
yield return new object[] { {
new ResponseCacheFilter( Duration = 10,
new CacheProfile Location = ResponseCacheLocation.Client,
{ NoStore = false,
Duration = 10, Location = ResponseCacheLocation.Client, VaryByHeader = "Accept"
NoStore = false, VaryByHeader = "Test" }),
}), "Accept",
"Test", "private,max-age=10"
"private,max-age=10" },
}; {
yield return new object[] { new ResponseCacheFilter(
new ResponseCacheFilter( new CacheProfile
new CacheProfile {
{ Duration = 10,
Duration = 31536000, Location = ResponseCacheLocation.Any, Location = ResponseCacheLocation.Client,
NoStore = false, VaryByHeader = "Test" NoStore = false,
}), VaryByHeader = "Test"
"Test", }),
"public,max-age=31536000" "Test",
"private,max-age=10"
},
{
new ResponseCacheFilter(
new CacheProfile
{
Duration = 31536000,
Location = ResponseCacheLocation.Any,
NoStore = false,
VaryByHeader = "Test"
}),
"Test",
"public,max-age=31536000"
}
}; };
} }
} }
[Theory] [Theory]
[MemberData(nameof(VaryData))] [MemberData(nameof(VaryByHeaderData))]
public void ResponseCacheCanSetVary(ResponseCacheFilter cache, string varyOutput, string cacheControlOutput) public void ResponseCacheCanSetVaryByHeader(ResponseCacheFilter cache, string varyOutput, string cacheControlOutput)
{ {
// Arrange // Arrange
var context = GetActionExecutingContext(new List<IFilterMetadata> { cache }); var context = GetActionExecutingContext(new List<IFilterMetadata> { cache });
@ -281,6 +337,112 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.Equal(cacheControlOutput, context.HttpContext.Response.Headers["Cache-control"]); Assert.Equal(cacheControlOutput, context.HttpContext.Response.Headers["Cache-control"]);
} }
public static TheoryData<ResponseCacheFilter, string[], string> VaryByQueryKeyData
{
get
{
return new TheoryData<ResponseCacheFilter, string[], string>
{
{
new ResponseCacheFilter(
new CacheProfile
{
Duration = 10,
Location = ResponseCacheLocation.Any,
NoStore = false,
VaryByQueryKeys = new[] { "Accept" }
}),
new[] { "Accept" },
"public,max-age=10"
},
{
new ResponseCacheFilter(
new CacheProfile
{
Duration = 0,
Location = ResponseCacheLocation.Any,
NoStore = true,
VaryByQueryKeys = new[] { "Accept" }
}),
new[] { "Accept" },
"no-store"
},
{
new ResponseCacheFilter(
new CacheProfile
{
Duration = 10,
Location = ResponseCacheLocation.Client,
NoStore = false,
VaryByQueryKeys = new[] { "Accept" }
}),
new[] { "Accept" },
"private,max-age=10"
},
{
new ResponseCacheFilter(
new CacheProfile
{
Duration = 10,
Location = ResponseCacheLocation.Client,
NoStore = false,
VaryByQueryKeys = new[] { "Accept", "Test" }
}),
new[] { "Accept", "Test" },
"private,max-age=10"
},
{
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<IResponseCachingFeature>(new ResponseCachingFeature());
// Act
cache.OnActionExecuting(context);
// Assert
Assert.Equal(varyOutput, context.HttpContext.Features.Get<IResponseCachingFeature>().VaryByQueryKeys);
Assert.Equal(cacheControlOutput, context.HttpContext.Response.Headers[HeaderNames.CacheControl]);
}
[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("'VaryByQueryKeys' requires the response cache middleware.", exception.Message);
}
[Fact] [Fact]
public void SetsPragmaOnNoCache() public void SetsPragmaOnNoCache()
{ {
@ -288,7 +450,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var cache = new ResponseCacheFilter( var cache = new ResponseCacheFilter(
new CacheProfile 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 }); var context = GetActionExecutingContext(new List<IFilterMetadata> { cache });
@ -308,12 +473,18 @@ namespace Microsoft.AspNetCore.Mvc.Internal
caches.Add(new ResponseCacheFilter( caches.Add(new ResponseCacheFilter(
new CacheProfile new CacheProfile
{ {
Duration = 0, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = null Duration = 0,
Location = ResponseCacheLocation.Any,
NoStore = false,
VaryByHeader = null
})); }));
caches.Add(new ResponseCacheFilter( caches.Add(new ResponseCacheFilter(
new CacheProfile new CacheProfile
{ {
Duration = 0, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = null Duration = 0,
Location = ResponseCacheLocation.Any,
NoStore = false,
VaryByHeader = null
})); }));
var context = GetActionExecutingContext(caches); var context = GetActionExecutingContext(caches);

View File

@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.ResponseCaching;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
@ -66,18 +67,18 @@ namespace Microsoft.AspNetCore.Mvc
// When there are no cache profiles then the passed in data is returned unchanged // When there are no cache profiles then the passed in data is returned unchanged
yield return new object[] { yield return new object[] {
new ResponseCacheAttribute() new ResponseCacheAttribute()
{ Duration = 20, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = "Accept" }, { Duration = 20, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = "Accept", VaryByQueryKeys = new[] { "QueryKey" } },
null, null,
new CacheProfile new CacheProfile
{ Duration = 20, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = "Accept" } { Duration = 20, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = "Accept", VaryByQueryKeys = new[] { "QueryKey" } }
}; };
yield return new object[] { yield return new object[] {
new ResponseCacheAttribute() new ResponseCacheAttribute()
{ Duration = 0, Location = ResponseCacheLocation.None, NoStore = true, VaryByHeader = null }, { Duration = 0, Location = ResponseCacheLocation.None, NoStore = true, VaryByHeader = null, VaryByQueryKeys = null },
null, null,
new CacheProfile new CacheProfile
{ Duration = 0, Location = ResponseCacheLocation.None, NoStore = true, VaryByHeader = null } { Duration = 0, Location = ResponseCacheLocation.None, NoStore = true, VaryByHeader = null, VaryByQueryKeys = null }
}; };
// Everything gets overriden if attribute parameters are present, // Everything gets overriden if attribute parameters are present,
@ -89,6 +90,7 @@ namespace Microsoft.AspNetCore.Mvc
Location = ResponseCacheLocation.Any, Location = ResponseCacheLocation.Any,
NoStore = false, NoStore = false,
VaryByHeader = "Accept", VaryByHeader = "Accept",
VaryByQueryKeys = new[] { "QueryKey" },
CacheProfileName = "TestCacheProfile" CacheProfileName = "TestCacheProfile"
}, },
new Dictionary<string, CacheProfile> { { "TestCacheProfile", new CacheProfile new Dictionary<string, CacheProfile> { { "TestCacheProfile", new CacheProfile
@ -96,10 +98,11 @@ namespace Microsoft.AspNetCore.Mvc
Duration = 10, Duration = 10,
Location = ResponseCacheLocation.Client, Location = ResponseCacheLocation.Client,
NoStore = true, NoStore = true,
VaryByHeader = "Test" VaryByHeader = "Test",
VaryByQueryKeys = new[] { "ProfileQueryKey" }
} } }, } } },
new CacheProfile new CacheProfile
{ Duration = 20, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = "Accept" } { Duration = 20, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = "Accept", VaryByQueryKeys = new[] { "QueryKey" } }
}; };
// Select parameters override the selected profile. // Select parameters override the selected profile.
@ -114,10 +117,11 @@ namespace Microsoft.AspNetCore.Mvc
Duration = 10, Duration = 10,
Location = ResponseCacheLocation.Client, Location = ResponseCacheLocation.Client,
NoStore = false, NoStore = false,
VaryByHeader = "Test" VaryByHeader = "Test",
VaryByQueryKeys = new[] { "ProfileQueryKey" }
} } }, } } },
new CacheProfile new CacheProfile
{ Duration = 534, Location = ResponseCacheLocation.Client, NoStore = false, VaryByHeader = "Test" } { Duration = 534, Location = ResponseCacheLocation.Client, NoStore = false, VaryByHeader = "Test", VaryByQueryKeys = new[] { "ProfileQueryKey" } }
}; };
// Duration parameter gets added to the selected profile. // Duration parameter gets added to the selected profile.
@ -131,10 +135,11 @@ namespace Microsoft.AspNetCore.Mvc
{ {
Location = ResponseCacheLocation.Client, Location = ResponseCacheLocation.Client,
NoStore = false, NoStore = false,
VaryByHeader = "Test" VaryByHeader = "Test",
VaryByQueryKeys = new[] { "ProfileQueryKey" }
} } }, } } },
new CacheProfile new CacheProfile
{ Duration = 534, Location = ResponseCacheLocation.Client, NoStore = false, VaryByHeader = "Test" } { Duration = 534, Location = ResponseCacheLocation.Client, NoStore = false, VaryByHeader = "Test", VaryByQueryKeys = new[] { "ProfileQueryKey" } }
}; };
// Default values gets added for parameters which are absent // Default values gets added for parameters which are absent
@ -146,7 +151,7 @@ namespace Microsoft.AspNetCore.Mvc
}, },
new Dictionary<string, CacheProfile>() { { "TestCacheProfile", new CacheProfile() } }, new Dictionary<string, CacheProfile>() { { "TestCacheProfile", new CacheProfile() } },
new CacheProfile new CacheProfile
{ Duration = 5234, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = null } { Duration = 5234, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = null, VaryByQueryKeys = null }
}; };
} }
} }
@ -167,6 +172,14 @@ namespace Microsoft.AspNetCore.Mvc
Assert.Equal(expectedProfile.Location, responseCacheFilter.Location); Assert.Equal(expectedProfile.Location, responseCacheFilter.Location);
Assert.Equal(expectedProfile.NoStore, responseCacheFilter.NoStore); Assert.Equal(expectedProfile.NoStore, responseCacheFilter.NoStore);
Assert.Equal(expectedProfile.VaryByHeader, responseCacheFilter.VaryByHeader); Assert.Equal(expectedProfile.VaryByHeader, responseCacheFilter.VaryByHeader);
if (expectedProfile.VaryByQueryKeys == null)
{
Assert.Null(responseCacheFilter.VaryByQueryKeys);
}
else
{
Assert.Equal(expectedProfile.VaryByQueryKeys, responseCacheFilter.VaryByQueryKeys);
}
} }
[Fact] [Fact]
@ -191,14 +204,17 @@ namespace Microsoft.AspNetCore.Mvc
public void ResponseCache_SetsAllHeaders() public void ResponseCache_SetsAllHeaders()
{ {
// Arrange // Arrange
var varyByQueryKeys = new[] { "QueryKey" };
var responseCache = new ResponseCacheAttribute() var responseCache = new ResponseCacheAttribute()
{ {
Duration = 100, Duration = 100,
Location = ResponseCacheLocation.Any, Location = ResponseCacheLocation.Any,
VaryByHeader = "Accept" VaryByHeader = "Accept",
VaryByQueryKeys = varyByQueryKeys
}; };
var filter = (ResponseCacheFilter)responseCache.CreateInstance(GetServiceProvider(cacheProfiles: null)); var filter = (ResponseCacheFilter)responseCache.CreateInstance(GetServiceProvider(cacheProfiles: null));
var context = GetActionExecutingContext(filter); var context = GetActionExecutingContext(filter);
context.HttpContext.Features.Set<IResponseCachingFeature>(new ResponseCachingFeature());
// Act // Act
filter.OnActionExecuting(context); filter.OnActionExecuting(context);
@ -212,6 +228,7 @@ namespace Microsoft.AspNetCore.Mvc
Assert.True(response.Headers.TryGetValue("Vary", out values)); Assert.True(response.Headers.TryGetValue("Vary", out values));
data = Assert.Single(values); data = Assert.Single(values);
Assert.Equal("Accept", data); Assert.Equal("Accept", data);
Assert.Equal(varyByQueryKeys, context.HttpContext.Features.Get<IResponseCachingFeature>().VaryByQueryKeys);
} }
public static TheoryData<ResponseCacheAttribute, string> CacheControlData public static TheoryData<ResponseCacheAttribute, string> CacheControlData

View File

@ -17,6 +17,7 @@
"version": "1.1.0-*", "version": "1.1.0-*",
"type": "build" "type": "build"
}, },
"Microsoft.AspNetCore.ResponseCaching": "1.0.0-*",
"Microsoft.AspNetCore.Testing": "1.1.0-*", "Microsoft.AspNetCore.Testing": "1.1.0-*",
"Microsoft.DotNet.InternalAbstractions": "1.0.0", "Microsoft.DotNet.InternalAbstractions": "1.0.0",
"Microsoft.Extensions.DependencyInjection": "1.1.0-*", "Microsoft.Extensions.DependencyInjection": "1.1.0-*",