diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs deleted file mode 100644 index 1dd0db8167..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.ResponseCaching.Internal -{ - internal class CacheControlValues - { - public const string MaxAgeString = "max-age"; - public const string MaxStaleString = "max-stale"; - public const string MinFreshString = "min-fresh"; - public const string MustRevalidateString = "must-revalidate"; - public const string NoCacheString = "no-cache"; - public const string NoStoreString = "no-store"; - public const string NoTransformString = "no-transform"; - public const string OnlyIfCachedString = "only-if-cached"; - public const string PrivateString = "private"; - public const string ProxyRevalidateString = "proxy-revalidate"; - public const string PublicString = "public"; - public const string SharedMaxAgeString = "s-maxage"; - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs deleted file mode 100644 index 94d452198b..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Globalization; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNetCore.ResponseCaching.Internal -{ - internal static class HttpHeaderParsingHelpers - { - private static readonly string[] DateFormats = new string[] { - // "r", // RFC 1123, required output format but too strict for input - "ddd, d MMM yyyy H:m:s 'GMT'", // RFC 1123 (r, except it allows both 1 and 01 for date and time) - "ddd, d MMM yyyy H:m:s", // RFC 1123, no zone - assume GMT - "d MMM yyyy H:m:s 'GMT'", // RFC 1123, no day-of-week - "d MMM yyyy H:m:s", // RFC 1123, no day-of-week, no zone - "ddd, d MMM yy H:m:s 'GMT'", // RFC 1123, short year - "ddd, d MMM yy H:m:s", // RFC 1123, short year, no zone - "d MMM yy H:m:s 'GMT'", // RFC 1123, no day-of-week, short year - "d MMM yy H:m:s", // RFC 1123, no day-of-week, short year, no zone - - "dddd, d'-'MMM'-'yy H:m:s 'GMT'", // RFC 850 - "dddd, d'-'MMM'-'yy H:m:s", // RFC 850 no zone - "ddd MMM d H:m:s yyyy", // ANSI C's asctime() format - - "ddd, d MMM yyyy H:m:s zzz", // RFC 5322 - "ddd, d MMM yyyy H:m:s", // RFC 5322 no zone - "d MMM yyyy H:m:s zzz", // RFC 5322 no day-of-week - "d MMM yyyy H:m:s", // RFC 5322 no day-of-week, no zone - }; - - // Try the various date formats in the order listed above. - // We should accept a wide verity of common formats, but only output RFC 1123 style dates. - internal static bool TryParseHeaderDate(string input, out DateTimeOffset result) => DateTimeOffset.TryParseExact(input, DateFormats, DateTimeFormatInfo.InvariantInfo, - DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal, out result); - - // Try to get the value of a specific header from a list of headers - // e.g. "header1=10, header2=30" - internal static bool TryParseHeaderTimeSpan(StringValues headers, string headerName, out TimeSpan? value) - { - foreach (var header in headers) - { - var index = header.IndexOf(headerName, StringComparison.OrdinalIgnoreCase); - if (index != -1) - { - index += headerName.Length; - int seconds; - if (!TryParseHeaderInt(index, header, out seconds)) - { - break; - } - value = TimeSpan.FromSeconds(seconds); - return true; - } - } - value = null; - return false; - } - - internal static bool HeaderContains(StringValues headers, string headerName) - { - foreach (var header in headers) - { - var index = header.IndexOf(headerName, StringComparison.OrdinalIgnoreCase); - if (index != -1) - { - return true; - } - } - - return false; - } - - private static bool TryParseHeaderInt(int startIndex, string header, out int value) - { - var found = false; - while (startIndex != header.Length) - { - var c = header[startIndex]; - if (c == '=') - { - found = true; - } - else if (c != ' ') - { - --startIndex; - break; - } - ++startIndex; - } - if (found && startIndex != header.Length) - { - var endIndex = startIndex + 1; - while (endIndex < header.Length) - { - var c = header[endIndex]; - if ((c >= '0') && (c <= '9')) - { - endIndex++; - } - else - { - break; - } - } - var length = endIndex - (startIndex + 1); - if (length > 0) - { - value = int.Parse(header.Substring(startIndex + 1, length), NumberStyles.None, NumberFormatInfo.InvariantInfo); - return true; - } - } - value = 0; - return false; - } - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs index 4a855e2d24..f2509c8ce3 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public Task GetAsync(string key) { var entry = _cache.Get(key); - + var memoryCachedResponse = entry as MemoryCachedResponse; if (memoryCachedResponse != null) { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs index 4d03434f6a..eeed0d9a09 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs @@ -65,7 +65,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { _parsedResponseDate = true; DateTimeOffset date; - if (HttpHeaderParsingHelpers.TryParseHeaderDate(HttpContext.Response.Headers[HeaderNames.Date], out date)) + if (HeaderUtilities.TryParseDate(HttpContext.Response.Headers[HeaderNames.Date], out date)) { _responseDate = date; } @@ -92,7 +92,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { _parsedResponseExpires = true; DateTimeOffset expires; - if (HttpHeaderParsingHelpers.TryParseHeaderDate(HttpContext.Response.Headers[HeaderNames.Expires], out expires)) + if (HeaderUtilities.TryParseDate(HttpContext.Response.Headers[HeaderNames.Expires], out expires)) { _responseExpires = expires; } @@ -112,7 +112,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal if (!_parsedResponseSharedMaxAge) { _parsedResponseSharedMaxAge = true; - HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlValues.SharedMaxAgeString, out _responseSharedMaxAge); + HeaderUtilities.TryParseSeconds(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.SharedMaxAgeString, out _responseSharedMaxAge); } return _responseSharedMaxAge; } @@ -125,7 +125,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal if (!_parsedResponseMaxAge) { _parsedResponseMaxAge = true; - HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlValues.MaxAgeString, out _responseMaxAge); + HeaderUtilities.TryParseSeconds(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.MaxAgeString, out _responseMaxAge); } return _responseMaxAge; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs index 87e37915ab..c37fcacc70 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Verify request cache-control parameters if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl])) { - if (HttpHeaderParsingHelpers.HeaderContains(request.Headers[HeaderNames.CacheControl], CacheControlValues.NoCacheString)) + if (HeaderUtilities.ContainsCacheDirective(request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.NoCacheString)) { context.Logger.LogRequestWithNoCacheNotCacheable(); return false; @@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { // Support for legacy HTTP 1.0 cache directive var pragmaHeaderValues = request.Headers[HeaderNames.Pragma]; - if (HttpHeaderParsingHelpers.HeaderContains(request.Headers[HeaderNames.Pragma], CacheControlValues.NoCacheString)) + if (HeaderUtilities.ContainsCacheDirective(request.Headers[HeaderNames.Pragma], CacheControlHeaderValue.NoCacheString)) { context.Logger.LogRequestWithPragmaNoCacheNotCacheable(); return false; @@ -55,27 +55,22 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal var responseCacheControlHeader = context.HttpContext.Response.Headers[HeaderNames.CacheControl]; // Only cache pages explicitly marked with public - if (!HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.PublicString)) + if (!HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.PublicString)) { context.Logger.LogResponseWithoutPublicNotCacheable(); return false; } // Check no-store - if (HttpHeaderParsingHelpers.HeaderContains(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlValues.NoStoreString)) - { - context.Logger.LogResponseWithNoStoreNotCacheable(); - return false; - } - - if (HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.NoStoreString)) + if (HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.NoStoreString) + || HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoStoreString)) { context.Logger.LogResponseWithNoStoreNotCacheable(); return false; } // Check no-cache - if (HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.NoCacheString)) + if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoCacheString)) { context.Logger.LogResponseWithNoCacheNotCacheable(); return false; @@ -99,7 +94,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } // Check private - if (HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.PrivateString)) + if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.PrivateString)) { context.Logger.LogResponseWithPrivateNotCacheable(); return false; @@ -159,12 +154,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public virtual bool IsCachedEntryFresh(ResponseCachingContext context) { var age = context.CachedEntryAge.Value; - var cachedControlHeaders = context.CachedResponseHeaders[HeaderNames.CacheControl]; + var cachedCacheControlHeaders = context.CachedResponseHeaders[HeaderNames.CacheControl]; var requestCacheControlHeaders = context.HttpContext.Request.Headers[HeaderNames.CacheControl]; // Add min-fresh requirements TimeSpan? minFresh; - if (HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(requestCacheControlHeaders, CacheControlValues.MinFreshString, out minFresh)) + if (HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MinFreshString, out minFresh)) { age += minFresh.Value; context.Logger.LogExpirationMinFreshAdded(minFresh.Value); @@ -172,7 +167,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Validate shared max age, this overrides any max age settings for shared caches TimeSpan? cachedSharedMaxAge; - HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(cachedControlHeaders, CacheControlValues.SharedMaxAgeString, out cachedSharedMaxAge); + HeaderUtilities.TryParseSeconds(cachedCacheControlHeaders, CacheControlHeaderValue.SharedMaxAgeString, out cachedSharedMaxAge); if (age >= cachedSharedMaxAge) { @@ -183,24 +178,24 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal else if (!cachedSharedMaxAge.HasValue) { TimeSpan? requestMaxAge; - HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(requestCacheControlHeaders, CacheControlValues.MaxAgeString, out requestMaxAge); + HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxAgeString, out requestMaxAge); TimeSpan? cachedMaxAge; - HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(cachedControlHeaders, CacheControlValues.MaxAgeString, out cachedMaxAge); + HeaderUtilities.TryParseSeconds(cachedCacheControlHeaders, CacheControlHeaderValue.MaxAgeString, out cachedMaxAge); var lowestMaxAge = cachedMaxAge < requestMaxAge ? cachedMaxAge : requestMaxAge ?? cachedMaxAge; // Validate max age if (age >= lowestMaxAge) { // Must revalidate - if (HttpHeaderParsingHelpers.HeaderContains(cachedControlHeaders, CacheControlValues.MustRevalidateString)) + if (HeaderUtilities.ContainsCacheDirective(cachedCacheControlHeaders, CacheControlHeaderValue.MustRevalidateString)) { context.Logger.LogExpirationMustRevalidate(age, lowestMaxAge.Value); return false; } TimeSpan? requestMaxStale; - HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(requestCacheControlHeaders, CacheControlValues.MaxStaleString, out requestMaxStale); + HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxStaleString, out requestMaxStale); // Request allows stale values if (requestMaxStale.HasValue && age - lowestMaxAge < requestMaxStale) @@ -216,7 +211,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { // Validate expiration DateTimeOffset expires; - if (HttpHeaderParsingHelpers.TryParseHeaderDate(context.CachedResponseHeaders[HeaderNames.Expires], out expires) && + if (HeaderUtilities.TryParseDate(context.CachedResponseHeaders[HeaderNames.Expires], out expires) && context.ResponseTime.Value >= expires) { context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, expires); diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index a87f2deee9..a15353a9d9 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -2,11 +2,11 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; @@ -198,7 +198,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } } - if (HttpHeaderParsingHelpers.HeaderContains(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlValues.OnlyIfCachedString)) + if (HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.OnlyIfCachedString)) { _logger.LogGatewayTimeoutServed(); context.HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; @@ -359,26 +359,24 @@ namespace Microsoft.AspNetCore.ResponseCaching if (!StringValues.IsNullOrEmpty(ifNoneMatchHeader)) { - if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any.Tag)) + if (ifNoneMatchHeader.Count == 1 && string.Equals(ifNoneMatchHeader[0], EntityTagHeaderValue.Any.Tag, StringComparison.OrdinalIgnoreCase)) { context.Logger.LogNotModifiedIfNoneMatchStar(); return true; } - if (!StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag])) + EntityTagHeaderValue eTag; + IList ifNoneMatchEtags; + if (!StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag]) + && EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag], out eTag) + && EntityTagHeaderValue.TryParseList(ifNoneMatchHeader, out ifNoneMatchEtags)) { - EntityTagHeaderValue eTag; - if (EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag], out eTag)) + foreach (var requestETag in ifNoneMatchEtags) { - foreach (var tag in ifNoneMatchHeader) + if (eTag.Compare(requestETag, useStrongComparison: false)) { - EntityTagHeaderValue requestETag; - if (EntityTagHeaderValue.TryParse(tag, out requestETag) && - eTag.Compare(requestETag, useStrongComparison: false)) - { - context.Logger.LogNotModifiedIfNoneMatchMatched(requestETag); - return true; - } + context.Logger.LogNotModifiedIfNoneMatchMatched(requestETag); + return true; } } } @@ -389,14 +387,14 @@ namespace Microsoft.AspNetCore.ResponseCaching if (!StringValues.IsNullOrEmpty(ifUnmodifiedSince)) { DateTimeOffset modified; - if (!HttpHeaderParsingHelpers.TryParseHeaderDate(cachedResponseHeaders[HeaderNames.LastModified], out modified) && - !HttpHeaderParsingHelpers.TryParseHeaderDate(cachedResponseHeaders[HeaderNames.Date], out modified)) + if (!HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.LastModified], out modified) && + !HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.Date], out modified)) { return false; } DateTimeOffset unmodifiedSince; - if (HttpHeaderParsingHelpers.TryParseHeaderDate(ifUnmodifiedSince, out unmodifiedSince) && + if (HeaderUtilities.TryParseDate(ifUnmodifiedSince, out unmodifiedSince) && modified <= unmodifiedSince) { context.Logger.LogNotModifiedIfUnmodifiedSinceSatisfied(modified, unmodifiedSince); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs deleted file mode 100644 index fb495bb535..0000000000 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.ResponseCaching.Internal; -using Microsoft.Extensions.Primitives; -using Xunit; - -namespace Microsoft.AspNetCore.ResponseCaching.Tests -{ - public class ParsingHelpersTests - { - [Theory] - [InlineData("h=1", "h", 1)] - [InlineData("header1=3, header2=10", "header1", 3)] - [InlineData("header1 =45, header2=80", "header1", 45)] - [InlineData("header1= 89 , header2=22", "header1", 89)] - [InlineData("header1= 89 , header2= 42", "header2", 42)] - void TryGetHeaderValue_Succeeds(string headerValue, string headerName, int expectedValue) - { - TimeSpan? value; - Assert.True(HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(new StringValues(headerValue), headerName, out value)); - Assert.Equal(TimeSpan.FromSeconds(expectedValue), value); - } - - [Theory] - [InlineData("h=", "h")] - [InlineData("header1=, header2=10", "header1")] - [InlineData("header1 , header2=80", "header1")] - [InlineData("h=10", "header")] - [InlineData("", "")] - [InlineData(null, null)] - void TryGetHeaderValue_Fails(string headerValue, string headerName) - { - TimeSpan? value; - Assert.False(HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(new StringValues(headerValue), headerName, out value)); - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs index ad607e9a1c..bced86fbc8 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Primitives; @@ -251,7 +250,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); - context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new List(new[] { new EntityTagHeaderValue("\"E1\"") }).ToString(); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "\"E1\""; Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } @@ -262,8 +261,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new HeaderDictionary(); - - context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new List(new[] { new EntityTagHeaderValue("\"E1\"") }).ToString(); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "\"E1\""; Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); @@ -291,7 +289,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new HeaderDictionary(); context.CachedResponseHeaders[HeaderNames.ETag] = responseETag.ToString(); - context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = requestETag.ToString(); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); @@ -306,14 +303,28 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new HeaderDictionary(); - context.HttpContext.Response.Headers[HeaderNames.ETag] = new EntityTagHeaderValue("\"E2\"").ToString(); - - context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new List(new[] { new EntityTagHeaderValue("\"E1\"") }).ToString(); + context.CachedResponseHeaders[HeaderNames.ETag] = "\"E2\""; + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "\"E1\""; Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } + [Fact] + public void ContentIsNotModified_IfNoneMatch_MatchesAtLeastOneValue_True() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.ETag] = "\"E2\""; + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new string[] { "\"E0\", \"E1\"", "\"E1\", \"E2\"" }; + + Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NotModifiedIfNoneMatchMatched); + } + [Fact] public async Task FinalizeCacheHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() {