diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs index fe010f7cc4..5524d8f687 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs @@ -15,6 +15,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { // Use the record separator for delimiting components of the cache key to avoid possible collisions private static readonly char KeyDelimiter = '\x1e'; + // Use the unit separator for delimiting subcomponents of the cache key to avoid possible collisions + private static readonly char KeySubDelimiter = '\x1f'; private readonly ObjectPool _builderPool; private readonly ResponseCachingOptions _options; @@ -147,6 +149,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal for (var i = 0; i < query.Value.Count; i++) { + if (i > 0) + { + builder.Append(KeySubDelimiter); + } builder.Append(query.Value[i]); } } @@ -163,6 +169,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal for (var j = 0; j < queryKeyValues.Count; j++) { + if (j > 0) + { + builder.Append(KeySubDelimiter); + } builder.Append(queryKeyValues[j]); } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs index 1a46e23862..1c3cd0e485 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs @@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public class ResponseCachingKeyProviderTests { private static readonly char KeyDelimiter = '\x1e'; + private static readonly char KeySubDelimiter = '\x1f'; [Fact] public void ResponseCachingKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodSchemeHostPortAndPath() @@ -143,6 +144,24 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests cacheKeyProvider.CreateStorageVaryByKey(context)); } + [Fact] + public void ResponseCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesNotConsolidated() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryA=ValueB"); + context.CachedVaryByRules = new CachedVaryByRules() + { + VaryByKeyPrefix = FastGuid.NewGuid().IdString, + QueryKeys = new string[] { "*" } + }; + + // To support case insensitivity, all query keys are converted to upper case. + // Explicit query keys uses the casing specified in the setting. + Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB", + cacheKeyProvider.CreateStorageVaryByKey(context)); + } + [Fact] public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndQueryKeys() {