Sort header and query values

This commit is contained in:
John Luo 2018-02-23 15:09:50 -08:00
parent 1437f00e84
commit d9778252d0
2 changed files with 73 additions and 10 deletions

View File

@ -82,7 +82,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
} }
} }
// BaseKey<delimiter>H<delimiter>HeaderName=HeaderValue<delimiter>Q<delimiter>QueryName=QueryValue // BaseKey<delimiter>H<delimiter>HeaderName=HeaderValue<delimiter>Q<delimiter>QueryName=QueryValue1<subdelimiter>QueryValue2
public string CreateStorageVaryByKey(ResponseCachingContext context) public string CreateStorageVaryByKey(ResponseCachingContext context)
{ {
if (context == null) if (context == null)
@ -124,9 +124,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
.Append(header) .Append(header)
.Append("="); .Append("=");
for (var j = 0; j < headerValues.Count; j++) var headerValuesArray = headerValues.ToArray();
Array.Sort(headerValuesArray, StringComparer.Ordinal);
for (var j = 0; j < headerValuesArray.Length; j++)
{ {
builder.Append(headerValues[j]); builder.Append(headerValuesArray[j]);
} }
} }
} }
@ -141,19 +144,27 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
if (varyByRules.QueryKeys.Count == 1 && string.Equals(varyByRules.QueryKeys[0], "*", StringComparison.Ordinal)) if (varyByRules.QueryKeys.Count == 1 && string.Equals(varyByRules.QueryKeys[0], "*", StringComparison.Ordinal))
{ {
// Vary by all available query keys // Vary by all available query keys
foreach (var query in context.HttpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase)) var queryArray = context.HttpContext.Request.Query.ToArray();
// Query keys are aggregated case-insensitively whereas the query values are compared ordinally.
Array.Sort(queryArray, QueryKeyComparer.OrdinalIgnoreCase);
for (var i = 0; i < queryArray.Length; i++)
{ {
builder.Append(KeyDelimiter) builder.Append(KeyDelimiter)
.AppendUpperInvariant(query.Key) .AppendUpperInvariant(queryArray[i].Key)
.Append("="); .Append("=");
for (var i = 0; i < query.Value.Count; i++) var queryValueArray = queryArray[i].Value.ToArray();
Array.Sort(queryValueArray, StringComparer.Ordinal);
for (var j = 0; j < queryValueArray.Length; j++)
{ {
if (i > 0) if (j > 0)
{ {
builder.Append(KeySubDelimiter); builder.Append(KeySubDelimiter);
} }
builder.Append(query.Value[i]);
builder.Append(queryValueArray[j]);
} }
} }
} }
@ -167,13 +178,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
.Append(queryKey) .Append(queryKey)
.Append("="); .Append("=");
for (var j = 0; j < queryKeyValues.Count; j++) var queryValueArray = queryKeyValues.ToArray();
Array.Sort(queryValueArray, StringComparer.Ordinal);
for (var j = 0; j < queryValueArray.Length; j++)
{ {
if (j > 0) if (j > 0)
{ {
builder.Append(KeySubDelimiter); builder.Append(KeySubDelimiter);
} }
builder.Append(queryKeyValues[j]);
builder.Append(queryValueArray[j]);
} }
} }
} }
@ -186,5 +201,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
_builderPool.Return(builder); _builderPool.Return(builder);
} }
} }
private class QueryKeyComparer : IComparer<KeyValuePair<string, StringValues>>
{
private StringComparer _stringComparer;
public static QueryKeyComparer OrdinalIgnoreCase { get; } = new QueryKeyComparer(StringComparer.OrdinalIgnoreCase);
public QueryKeyComparer(StringComparer stringComparer)
{
_stringComparer = stringComparer;
}
public int Compare(KeyValuePair<string, StringValues> x, KeyValuePair<string, StringValues> y) => _stringComparer.Compare(x.Key, y.Key);
}
} }
} }

View File

@ -94,6 +94,22 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
cacheKeyProvider.CreateStorageVaryByKey(context)); cacheKeyProvider.CreateStorageVaryByKey(context));
} }
[Fact]
public void ResponseCachingKeyProvider_CreateStorageVaryKey_HeaderValuesAreSorted()
{
var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
var context = TestUtils.CreateTestContext();
context.HttpContext.Request.Headers["HeaderA"] = "ValueB";
context.HttpContext.Request.Headers.Append("HeaderA", "ValueA");
context.CachedVaryByRules = new CachedVaryByRules()
{
Headers = new string[] { "HeaderA", "HeaderC" }
};
Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueAValueB{KeyDelimiter}HeaderC=",
cacheKeyProvider.CreateStorageVaryByKey(context));
}
[Fact] [Fact]
public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedQueryKeysOnly() public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedQueryKeysOnly()
{ {
@ -162,6 +178,24 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
cacheKeyProvider.CreateStorageVaryByKey(context)); cacheKeyProvider.CreateStorageVaryByKey(context));
} }
[Fact]
public void ResponseCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesAreSorted()
{
var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
var context = TestUtils.CreateTestContext();
context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueB&QueryA=ValueA");
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] [Fact]
public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndQueryKeys() public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndQueryKeys()
{ {