From 792dd235f585ed407b9cebcd7a086516b41cb3db Mon Sep 17 00:00:00 2001 From: John Luo Date: Wed, 5 Oct 2016 19:12:41 -0700 Subject: [PATCH] Add support for VaryByQueryKey #2894 --- .../CacheProfile.cs | 10 +- .../Internal/ResponseCacheFilter.cs | 24 + .../Properties/Resources.Designer.cs | 16 + .../Resources.resx | 3 + .../ResponseCacheAttribute.cs | 12 +- .../project.json | 1 + .../Internal/ResponseCacheFilterTest.cs | 485 ++++++++++++------ .../ResponseCacheAttributeTest.cs | 41 +- .../project.json | 1 + 9 files changed, 422 insertions(+), 171 deletions(-) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/CacheProfile.cs b/src/Microsoft.AspNetCore.Mvc.Core/CacheProfile.cs index 92422c2ac5..15998b31ea 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/CacheProfile.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/CacheProfile.cs @@ -11,7 +11,7 @@ public class CacheProfile /// /// 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 /// . /// public int? Duration { get; set; } @@ -36,5 +36,13 @@ public class CacheProfile /// Gets or sets the value for the Vary header in . /// public string VaryByHeader { get; set; } + + /// + /// Gets or sets the query keys to vary by. + /// + /// + /// requires the response cache middleware. + /// + public string[] VaryByQueryKeys { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ResponseCacheFilter.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ResponseCacheFilter.cs index b27ec02e93..6577ad36e5 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ResponseCacheFilter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ResponseCacheFilter.cs @@ -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 @@ -21,6 +22,7 @@ public class ResponseCacheFilter : IActionFilter, IResponseCacheFilter private ResponseCacheLocation? _cacheLocation; private bool? _cacheNoStore; private string _cacheVaryByHeader; + private string[] _cacheVaryByQueryKeys; /// /// Creates a new instance of @@ -73,6 +75,18 @@ public string VaryByHeader set { _cacheVaryByHeader = value; } } + /// + /// Gets or sets the query keys to vary by. + /// + /// + /// requires the response cache middleware. + /// + public string[] VaryByQueryKeys + { + get { return _cacheVaryByQueryKeys ?? _cacheProfile.VaryByQueryKeys; } + set { _cacheVaryByQueryKeys = value; } + } + /// public void OnActionExecuting(ActionExecutingContext context) { @@ -110,6 +124,16 @@ public void OnActionExecuting(ActionExecutingContext context) headers[HeaderNames.Vary] = VaryByHeader; } + if (VaryByQueryKeys != null) + { + var responseCachingFeature = context.HttpContext.Features.Get(); + if (responseCachingFeature == null) + { + throw new InvalidOperationException(Resources.FormatVaryByQueryKeys_Requires_ResponseCachingMiddleware(nameof(VaryByQueryKeys))); + } + responseCachingFeature.VaryByQueryKeys = VaryByQueryKeys; + } + if (NoStore) { headers[HeaderNames.CacheControl] = "no-store"; diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs index 6964666fbb..ab08d318b2 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs @@ -1338,6 +1338,22 @@ internal static string FormatFormCollectionModelBinder_CannotBindToFormCollectio return string.Format(CultureInfo.CurrentCulture, GetString("FormCollectionModelBinder_CannotBindToFormCollection"), p0, p1, p2); } + /// + /// VaryByQueryKeys requires the response cache middleware. + /// + internal static string VaryByQueryKeys_Requires_ResponseCachingMiddleware + { + get { return GetString("VaryByQueryKeys_Requires_ResponseCachingMiddleware"); } + } + + /// + /// VaryByQueryKeys requires the response cache middleware. + /// + 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) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx index a054806f71..e5d07f4d0f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx @@ -376,4 +376,7 @@ The '{0}' cannot bind to a model of type '{1}'. Change the model type to '{2}' instead. + + '{0}' requires the response cache middleware. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ResponseCacheAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/ResponseCacheAttribute.cs index 9bddb6a66b..553be45b09 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ResponseCacheAttribute.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ResponseCacheAttribute.cs @@ -77,6 +77,14 @@ public bool NoStore /// public string VaryByHeader { get; set; } + /// + /// Gets or sets the query keys to vary by. + /// + /// + /// requires the response cache middleware. + /// + public string[] VaryByQueryKeys { get; set; } + /// /// Gets or sets the value of the cache profile name. /// @@ -117,6 +125,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. @@ -125,7 +134,8 @@ public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) Duration = _duration, Location = _location, NoStore = _noStore, - VaryByHeader = VaryByHeader + VaryByHeader = VaryByHeader, + VaryByQueryKeys = VaryByQueryKeys, }); } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/project.json b/src/Microsoft.AspNetCore.Mvc.Core/project.json index 64203d282e..3f463ec966 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/project.json +++ b/src/Microsoft.AspNetCore.Mvc.Core/project.json @@ -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.Abstractions": "1.0.0-*", "Microsoft.AspNetCore.Routing": "1.1.0-*", "Microsoft.AspNetCore.Routing.DecisionTree.Sources": { "type": "build", diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ResponseCacheFilterTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ResponseCacheFilterTest.cs index 4e8248266d..c04b67adec 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ResponseCacheFilterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ResponseCacheFilterTest.cs @@ -3,10 +3,13 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.ResponseCaching; using Microsoft.AspNetCore.Routing; +using Microsoft.Net.Http.Headers; using Xunit; namespace Microsoft.AspNetCore.Mvc.Internal @@ -64,86 +67,113 @@ public void OnActionExecuting_ThrowsIfDurationIsNotSet_WhenNoStoreIsFalse() ex.Message); } - public static IEnumerable CacheControlData + public static TheoryData CacheControlData { get { - yield return new object[] { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 0, Location = ResponseCacheLocation.Any, NoStore = true, VaryByHeader = null - }), - "no-store" - }; - // If no-store is set, then location is ignored. - yield return new object[] { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 0, Location = ResponseCacheLocation.Client, NoStore = true, VaryByHeader = null - }), - "no-store" - }; - yield return new object[] { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 0, Location = ResponseCacheLocation.Any, NoStore = true, VaryByHeader = null - }), - "no-store" - }; - // If no-store is set, then duration is ignored. - yield return new object[] { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 100, Location = ResponseCacheLocation.Any, NoStore = true, VaryByHeader = null - }), - "no-store" - }; - - yield return new object[] { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 10, Location = ResponseCacheLocation.Client, - NoStore = false, VaryByHeader = null - }), - "private,max-age=10" - }; - yield return new object[] { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 10, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = null - }), - "public,max-age=10" - }; - yield return new object[] { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 10, Location = ResponseCacheLocation.None, NoStore = false, VaryByHeader = null - }), - "no-cache,max-age=10" - }; - yield return new object[] { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 31536000, Location = ResponseCacheLocation.Any, - NoStore = false, VaryByHeader = null - }), - "public,max-age=31536000" - }; - yield return new object[] { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 20, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = null - }), - "public,max-age=20" + return new TheoryData + { + { + new ResponseCacheFilter( + new CacheProfile + { + Duration = 0, + Location = ResponseCacheLocation.Any, + NoStore = true, + VaryByHeader = null + }), + "no-store" + }, + // If no-store is set, then location is ignored. + { + new ResponseCacheFilter( + new CacheProfile + { + Duration = 0, + Location = ResponseCacheLocation.Client, + NoStore = true, + VaryByHeader = null + }), + "no-store" + }, + { + new ResponseCacheFilter( + new CacheProfile + { + Duration = 0, + Location = ResponseCacheLocation.Any, + NoStore = true, + VaryByHeader = null + }), + "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" + }, + { + new ResponseCacheFilter( + new CacheProfile + { + Duration = 10, + Location = ResponseCacheLocation.Client, + NoStore = false, + VaryByHeader = null + }), + "private,max-age=10" + }, + { + new ResponseCacheFilter( + new CacheProfile + { + Duration = 10, + Location = ResponseCacheLocation.Any, + NoStore = false, + VaryByHeader = null + }), + "public,max-age=10" + }, + { + new ResponseCacheFilter( + new CacheProfile + { + Duration = 10, + 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 @@ public void OnActionExecuting_CanSetCacheControlHeaders(ResponseCacheFilter cach Assert.Equal(output, context.HttpContext.Response.Headers["Cache-control"]); } - public static IEnumerable NoStoreData + public static TheoryData NoStoreData { get { - // If no-store is set, then location is ignored. - yield return new object[] { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 0, Location = ResponseCacheLocation.Client, NoStore = true, VaryByHeader = null - }), - "no-store" - }; - yield return new object[] { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 0, Location = ResponseCacheLocation.Any, NoStore = true, VaryByHeader = null - }), - "no-store" - }; - // If no-store is set, then duration is ignored. - yield return new object[] { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 100, Location = ResponseCacheLocation.Any, NoStore = true, VaryByHeader = null - }), - "no-store" + return new TheoryData + { + // If no-store is set, then location is ignored. + { + new ResponseCacheFilter( + new CacheProfile + { + Duration = 0, + Location = ResponseCacheLocation.Client, + NoStore = true, + VaryByHeader = null + }), + "no-store" + }, + { + new ResponseCacheFilter( + new CacheProfile + { + Duration = 0, + Location = ResponseCacheLocation.Any, + NoStore = true, + VaryByHeader = null + }), + "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 @@ public void OnActionExecuting_DoesNotSetLocationOrDuration_IfNoStoreIsSet( Assert.Equal(output, context.HttpContext.Response.Headers["Cache-control"]); } - public static IEnumerable VaryData + public static TheoryData VaryByHeaderData { get { - yield return new object[] { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 10, Location = ResponseCacheLocation.Any, - NoStore = false, VaryByHeader = "Accept" - }), - "Accept", - "public,max-age=10" }; - yield return new object[] { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 0, Location= ResponseCacheLocation.Any, - NoStore = true, VaryByHeader = "Accept" - }), - "Accept", - "no-store" - }; - yield return new object[] { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 10, Location = ResponseCacheLocation.Client, - NoStore = false, VaryByHeader = "Accept" - }), - "Accept", - "private,max-age=10" - }; - yield return new object[] { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 10, Location = ResponseCacheLocation.Client, - NoStore = false, VaryByHeader = "Test" - }), - "Test", - "private,max-age=10" - }; - yield return new object[] { - new ResponseCacheFilter( - new CacheProfile - { - Duration = 31536000, Location = ResponseCacheLocation.Any, - NoStore = false, VaryByHeader = "Test" - }), - "Test", - "public,max-age=31536000" + return new TheoryData + { + { + new ResponseCacheFilter( + new CacheProfile + { + Duration = 10, + Location = ResponseCacheLocation.Any, + NoStore = false, + VaryByHeader = "Accept" + }), + "Accept", + "public,max-age=10" + }, + { + new ResponseCacheFilter( + new CacheProfile + { + Duration = 0, + Location = ResponseCacheLocation.Any, + NoStore = true, + VaryByHeader = "Accept" + }), + "Accept", + "no-store" + }, + { + new ResponseCacheFilter( + new CacheProfile + { + Duration = 10, + Location = ResponseCacheLocation.Client, + NoStore = false, + VaryByHeader = "Accept" + }), + "Accept", + "private,max-age=10" + }, + { + new ResponseCacheFilter( + new CacheProfile + { + Duration = 10, + Location = ResponseCacheLocation.Client, + NoStore = false, + VaryByHeader = "Test" + }), + "Test", + "private,max-age=10" + }, + { + new ResponseCacheFilter( + new CacheProfile + { + Duration = 31536000, + Location = ResponseCacheLocation.Any, + NoStore = false, + VaryByHeader = "Test" + }), + "Test", + "public,max-age=31536000" + } }; } } [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 { cache }); @@ -281,6 +337,112 @@ public void ResponseCacheCanSetVary(ResponseCacheFilter cache, string varyOutput Assert.Equal(cacheControlOutput, context.HttpContext.Response.Headers["Cache-control"]); } + public static TheoryData VaryByQueryKeyData + { + get + { + return new TheoryData + { + { + 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 { cache }); + context.HttpContext.Features.Set(new ResponseCachingFeature()); + + // Act + cache.OnActionExecuting(context); + + // Assert + Assert.Equal(varyOutput, context.HttpContext.Features.Get().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 { cache }); + + // Act & Assert + var exception = Assert.Throws(() => cache.OnActionExecuting(context)); + Assert.Equal("'VaryByQueryKeys' requires the response cache middleware.", exception.Message); + } + [Fact] public void SetsPragmaOnNoCache() { @@ -288,7 +450,10 @@ public void SetsPragmaOnNoCache() 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 { cache }); @@ -308,12 +473,18 @@ public void IsOverridden_ReturnsTrueForAllButLastFilter() caches.Add(new ResponseCacheFilter( new CacheProfile { - Duration = 0, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = null + Duration = 0, + Location = ResponseCacheLocation.Any, + NoStore = false, + VaryByHeader = null })); caches.Add(new ResponseCacheFilter( new CacheProfile { - Duration = 0, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = null + Duration = 0, + Location = ResponseCacheLocation.Any, + NoStore = false, + VaryByHeader = null })); var context = GetActionExecutingContext(caches); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ResponseCacheAttributeTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ResponseCacheAttributeTest.cs index 4994c304d6..1b32eac679 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ResponseCacheAttributeTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ResponseCacheAttributeTest.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.ResponseCaching; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -66,18 +67,18 @@ public static IEnumerable OverrideData // When there are no cache profiles then the passed in data is returned unchanged yield return new object[] { new ResponseCacheAttribute() - { Duration = 20, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = "Accept" }, + { Duration = 20, Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = "Accept", VaryByQueryKeys = new[] { "QueryKey" } }, null, 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[] { new ResponseCacheAttribute() - { Duration = 0, Location = ResponseCacheLocation.None, NoStore = true, VaryByHeader = null }, + { Duration = 0, Location = ResponseCacheLocation.None, NoStore = true, VaryByHeader = null, VaryByQueryKeys = null }, null, 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, @@ -89,6 +90,7 @@ public static IEnumerable OverrideData Location = ResponseCacheLocation.Any, NoStore = false, VaryByHeader = "Accept", + VaryByQueryKeys = new[] { "QueryKey" }, CacheProfileName = "TestCacheProfile" }, new Dictionary { { "TestCacheProfile", new CacheProfile @@ -96,10 +98,11 @@ public static IEnumerable OverrideData Duration = 10, Location = ResponseCacheLocation.Client, NoStore = true, - VaryByHeader = "Test" + VaryByHeader = "Test", + VaryByQueryKeys = new[] { "ProfileQueryKey" } } } }, 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. @@ -114,10 +117,11 @@ public static IEnumerable OverrideData Duration = 10, Location = ResponseCacheLocation.Client, NoStore = false, - VaryByHeader = "Test" + VaryByHeader = "Test", + VaryByQueryKeys = new[] { "ProfileQueryKey" } } } }, 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. @@ -131,10 +135,11 @@ public static IEnumerable OverrideData { Location = ResponseCacheLocation.Client, NoStore = false, - VaryByHeader = "Test" + VaryByHeader = "Test", + VaryByQueryKeys = new[] { "ProfileQueryKey" } } } }, 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 @@ -146,7 +151,7 @@ public static IEnumerable OverrideData }, new Dictionary() { { "TestCacheProfile", 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 @@ public void CreateInstance_HonorsOverrides( Assert.Equal(expectedProfile.Location, responseCacheFilter.Location); Assert.Equal(expectedProfile.NoStore, responseCacheFilter.NoStore); Assert.Equal(expectedProfile.VaryByHeader, responseCacheFilter.VaryByHeader); + if (expectedProfile.VaryByQueryKeys == null) + { + Assert.Null(responseCacheFilter.VaryByQueryKeys); + } + else + { + Assert.Equal(expectedProfile.VaryByQueryKeys, responseCacheFilter.VaryByQueryKeys); + } } [Fact] @@ -191,14 +204,17 @@ public void CreateInstance_DoesNotThrowWhenTheDurationIsNotSet_WithNoStoreFalse( public void ResponseCache_SetsAllHeaders() { // Arrange + var varyByQueryKeys = new[] { "QueryKey" }; var responseCache = new ResponseCacheAttribute() { Duration = 100, Location = ResponseCacheLocation.Any, - VaryByHeader = "Accept" + VaryByHeader = "Accept", + VaryByQueryKeys = varyByQueryKeys }; var filter = (ResponseCacheFilter)responseCache.CreateInstance(GetServiceProvider(cacheProfiles: null)); var context = GetActionExecutingContext(filter); + context.HttpContext.Features.Set(new ResponseCachingFeature()); // Act filter.OnActionExecuting(context); @@ -212,6 +228,7 @@ public void ResponseCache_SetsAllHeaders() Assert.True(response.Headers.TryGetValue("Vary", out values)); data = Assert.Single(values); Assert.Equal("Accept", data); + Assert.Equal(varyByQueryKeys, context.HttpContext.Features.Get().VaryByQueryKeys); } public static TheoryData CacheControlData diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/project.json b/test/Microsoft.AspNetCore.Mvc.Core.Test/project.json index 9e2e43d6d4..c337791b14 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/project.json +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/project.json @@ -17,6 +17,7 @@ "version": "1.1.0-*", "type": "build" }, + "Microsoft.AspNetCore.ResponseCaching": "1.0.0-*", "Microsoft.AspNetCore.Testing": "1.1.0-*", "Microsoft.DotNet.InternalAbstractions": "1.0.0", "Microsoft.Extensions.DependencyInjection": "1.1.0-*",