From 9c94a7764bdb1f7194709ec7d5bc011f21f01a37 Mon Sep 17 00:00:00 2001 From: BrennanConroy Date: Mon, 3 Oct 2016 10:25:06 -0700 Subject: [PATCH] Improve header parsing performance --- .../CacheControlValues.cs | 21 ++ .../Internal/HttpHeaderParsingHelpers.cs | 118 +++++++++ .../Internal/ResponseCachingContext.cs | 107 ++++---- .../Internal/ResponseCachingPolicyProvider.cs | 90 ++++--- .../ResponseCachingMiddleware.cs | 54 ++-- .../ParsingHelpersTests.cs | 39 +++ .../ResponseCachingMiddlewareTests.cs | 96 ++++--- .../ResponseCachingPolicyProviderTests.cs | 248 ++++++++---------- 8 files changed, 470 insertions(+), 303 deletions(-) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs create mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs new file mode 100644 index 0000000000..1dd0db8167 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs @@ -0,0 +1,21 @@ +// 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 new file mode 100644 index 0000000000..94d452198b --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs @@ -0,0 +1,118 @@ +// 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/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs index 4fc7292894..4d03434f6a 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs @@ -5,7 +5,6 @@ using System; using System.IO; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Http.Headers; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; @@ -13,16 +12,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { public class ResponseCachingContext { - private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); - - private RequestHeaders _requestHeaders; - private ResponseHeaders _responseHeaders; - private CacheControlHeaderValue _requestCacheControl; - private CacheControlHeaderValue _responseCacheControl; private DateTimeOffset? _responseDate; private bool _parsedResponseDate; private DateTimeOffset? _responseExpires; private bool _parsedResponseExpires; + private TimeSpan? _responseSharedMaxAge; + private bool _parsedResponseSharedMaxAge; + private TimeSpan? _responseMaxAge; + private bool _parsedResponseMaxAge; internal ResponseCachingContext(HttpContext httpContext, ILogger logger) { @@ -58,55 +55,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal internal IHttpSendFileFeature OriginalSendFileFeature { get; set; } - internal ResponseHeaders CachedResponseHeaders { get; set; } - - internal RequestHeaders TypedRequestHeaders - { - get - { - if (_requestHeaders == null) - { - _requestHeaders = HttpContext.Request.GetTypedHeaders(); - } - return _requestHeaders; - } - } - - internal ResponseHeaders TypedResponseHeaders - { - get - { - if (_responseHeaders == null) - { - _responseHeaders = HttpContext.Response.GetTypedHeaders(); - } - return _responseHeaders; - } - } - - internal CacheControlHeaderValue RequestCacheControlHeaderValue - { - get - { - if (_requestCacheControl == null) - { - _requestCacheControl = TypedRequestHeaders.CacheControl ?? EmptyCacheControl; - } - return _requestCacheControl; - } - } - - internal CacheControlHeaderValue ResponseCacheControlHeaderValue - { - get - { - if (_responseCacheControl == null) - { - _responseCacheControl = TypedResponseHeaders.CacheControl ?? EmptyCacheControl; - } - return _responseCacheControl; - } - } + internal IHeaderDictionary CachedResponseHeaders { get; set; } internal DateTimeOffset? ResponseDate { @@ -115,7 +64,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal if (!_parsedResponseDate) { _parsedResponseDate = true; - _responseDate = TypedResponseHeaders.Date; + DateTimeOffset date; + if (HttpHeaderParsingHelpers.TryParseHeaderDate(HttpContext.Response.Headers[HeaderNames.Date], out date)) + { + _responseDate = date; + } + else + { + _responseDate = null; + } } return _responseDate; } @@ -134,10 +91,44 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal if (!_parsedResponseExpires) { _parsedResponseExpires = true; - _responseExpires = TypedResponseHeaders.Expires; + DateTimeOffset expires; + if (HttpHeaderParsingHelpers.TryParseHeaderDate(HttpContext.Response.Headers[HeaderNames.Expires], out expires)) + { + _responseExpires = expires; + } + else + { + _responseExpires = null; + } } return _responseExpires; } } + + internal TimeSpan? ResponseSharedMaxAge + { + get + { + if (!_parsedResponseSharedMaxAge) + { + _parsedResponseSharedMaxAge = true; + HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlValues.SharedMaxAgeString, out _responseSharedMaxAge); + } + return _responseSharedMaxAge; + } + } + + internal TimeSpan? ResponseMaxAge + { + get + { + if (!_parsedResponseMaxAge) + { + _parsedResponseMaxAge = true; + HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlValues.MaxAgeString, out _responseMaxAge); + } + return _responseMaxAge; + } + } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs index 0072546ef7..87e37915ab 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs @@ -10,8 +10,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { public class ResponseCachingPolicyProvider : IResponseCachingPolicyProvider { - private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); - public virtual bool IsRequestCacheable(ResponseCachingContext context) { // Verify the method @@ -32,7 +30,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Verify request cache-control parameters if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl])) { - if (context.RequestCacheControlHeaderValue.NoCache) + if (HttpHeaderParsingHelpers.HeaderContains(request.Headers[HeaderNames.CacheControl], CacheControlValues.NoCacheString)) { context.Logger.LogRequestWithNoCacheNotCacheable(); return false; @@ -42,13 +40,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { // Support for legacy HTTP 1.0 cache directive var pragmaHeaderValues = request.Headers[HeaderNames.Pragma]; - foreach (var directive in pragmaHeaderValues) + if (HttpHeaderParsingHelpers.HeaderContains(request.Headers[HeaderNames.Pragma], CacheControlValues.NoCacheString)) { - if (string.Equals("no-cache", directive, StringComparison.OrdinalIgnoreCase)) - { - context.Logger.LogRequestWithPragmaNoCacheNotCacheable(); - return false; - } + context.Logger.LogRequestWithPragmaNoCacheNotCacheable(); + return false; } } @@ -57,22 +52,30 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public virtual bool IsResponseCacheable(ResponseCachingContext context) { + var responseCacheControlHeader = context.HttpContext.Response.Headers[HeaderNames.CacheControl]; + // Only cache pages explicitly marked with public - if (!context.ResponseCacheControlHeaderValue.Public) + if (!HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.PublicString)) { context.Logger.LogResponseWithoutPublicNotCacheable(); return false; } // Check no-store - if (context.RequestCacheControlHeaderValue.NoStore || context.ResponseCacheControlHeaderValue.NoStore) + if (HttpHeaderParsingHelpers.HeaderContains(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlValues.NoStoreString)) + { + context.Logger.LogResponseWithNoStoreNotCacheable(); + return false; + } + + if (HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.NoStoreString)) { context.Logger.LogResponseWithNoStoreNotCacheable(); return false; } // Check no-cache - if (context.ResponseCacheControlHeaderValue.NoCache) + if (HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.NoCacheString)) { context.Logger.LogResponseWithNoCacheNotCacheable(); return false; @@ -96,7 +99,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } // Check private - if (context.ResponseCacheControlHeaderValue.Private) + if (HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.PrivateString)) { context.Logger.LogResponseWithPrivateNotCacheable(); return false; @@ -112,8 +115,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Check response freshness if (!context.ResponseDate.HasValue) { - if (!context.ResponseCacheControlHeaderValue.SharedMaxAge.HasValue && - !context.ResponseCacheControlHeaderValue.MaxAge.HasValue && + if (!context.ResponseSharedMaxAge.HasValue && + !context.ResponseMaxAge.HasValue && context.ResponseTime.Value >= context.ResponseExpires) { context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, context.ResponseExpires.Value); @@ -125,22 +128,20 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal var age = context.ResponseTime.Value - context.ResponseDate.Value; // Validate shared max age - var sharedMaxAge = context.ResponseCacheControlHeaderValue.SharedMaxAge; - if (age >= sharedMaxAge) + if (age >= context.ResponseSharedMaxAge) { - context.Logger.LogExpirationSharedMaxAgeExceeded(age, sharedMaxAge.Value); + context.Logger.LogExpirationSharedMaxAgeExceeded(age, context.ResponseSharedMaxAge.Value); return false; } - else if (!sharedMaxAge.HasValue) + else if (!context.ResponseSharedMaxAge.HasValue) { // Validate max age - var maxAge = context.ResponseCacheControlHeaderValue.MaxAge; - if (age >= maxAge) + if (age >= context.ResponseMaxAge) { - context.Logger.LogExpirationMaxAgeExceeded(age, maxAge.Value); + context.Logger.LogExpirationMaxAgeExceeded(age, context.ResponseMaxAge.Value); return false; } - else if (!maxAge.HasValue) + else if (!context.ResponseMaxAge.HasValue) { // Validate expiration if (context.ResponseTime.Value >= context.ResponseExpires) @@ -158,44 +159,53 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public virtual bool IsCachedEntryFresh(ResponseCachingContext context) { var age = context.CachedEntryAge.Value; - var cachedControlHeaders = context.CachedResponseHeaders.CacheControl ?? EmptyCacheControl; + var cachedControlHeaders = context.CachedResponseHeaders[HeaderNames.CacheControl]; + var requestCacheControlHeaders = context.HttpContext.Request.Headers[HeaderNames.CacheControl]; // Add min-fresh requirements - var minFresh = context.RequestCacheControlHeaderValue.MinFresh; - if (minFresh.HasValue) + TimeSpan? minFresh; + if (HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(requestCacheControlHeaders, CacheControlValues.MinFreshString, out minFresh)) { age += minFresh.Value; context.Logger.LogExpirationMinFreshAdded(minFresh.Value); } // Validate shared max age, this overrides any max age settings for shared caches - var sharedMaxAge = cachedControlHeaders.SharedMaxAge; - if (age >= sharedMaxAge) + TimeSpan? cachedSharedMaxAge; + HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(cachedControlHeaders, CacheControlValues.SharedMaxAgeString, out cachedSharedMaxAge); + + if (age >= cachedSharedMaxAge) { // shared max age implies must revalidate - context.Logger.LogExpirationSharedMaxAgeExceeded(age, sharedMaxAge.Value); + context.Logger.LogExpirationSharedMaxAgeExceeded(age, cachedSharedMaxAge.Value); return false; } - else if (!sharedMaxAge.HasValue) + else if (!cachedSharedMaxAge.HasValue) { - var cachedMaxAge = cachedControlHeaders.MaxAge; - var requestMaxAge = context.RequestCacheControlHeaderValue.MaxAge; + TimeSpan? requestMaxAge; + HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(requestCacheControlHeaders, CacheControlValues.MaxAgeString, out requestMaxAge); + + TimeSpan? cachedMaxAge; + HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(cachedControlHeaders, CacheControlValues.MaxAgeString, out cachedMaxAge); + var lowestMaxAge = cachedMaxAge < requestMaxAge ? cachedMaxAge : requestMaxAge ?? cachedMaxAge; // Validate max age if (age >= lowestMaxAge) { // Must revalidate - if (cachedControlHeaders.MustRevalidate) + if (HttpHeaderParsingHelpers.HeaderContains(cachedControlHeaders, CacheControlValues.MustRevalidateString)) { context.Logger.LogExpirationMustRevalidate(age, lowestMaxAge.Value); return false; } + TimeSpan? requestMaxStale; + HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(requestCacheControlHeaders, CacheControlValues.MaxStaleString, out requestMaxStale); + // Request allows stale values - var maxStaleLimit = context.RequestCacheControlHeaderValue.MaxStaleLimit; - if (maxStaleLimit.HasValue && age - lowestMaxAge < maxStaleLimit) + if (requestMaxStale.HasValue && age - lowestMaxAge < requestMaxStale) { - context.Logger.LogExpirationMaxStaleSatisfied(age, lowestMaxAge.Value, maxStaleLimit.Value); + context.Logger.LogExpirationMaxStaleSatisfied(age, lowestMaxAge.Value, requestMaxStale.Value); return true; } @@ -205,11 +215,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal else if (!cachedMaxAge.HasValue && !requestMaxAge.HasValue) { // Validate expiration - var responseTime = context.ResponseTime.Value; - var expires = context.CachedResponseHeaders.Expires; - if (responseTime >= expires) + DateTimeOffset expires; + if (HttpHeaderParsingHelpers.TryParseHeaderDate(context.CachedResponseHeaders[HeaderNames.Expires], out expires) && + context.ResponseTime.Value >= expires) { - context.Logger.LogExpirationExpiresExceeded(responseTime, expires.Value); + context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, expires); return false; } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index dc01df4fba..a87f2deee9 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -119,7 +119,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } context.CachedResponse = cachedResponse; - context.CachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers); + context.CachedResponseHeaders = cachedResponse.Headers; context.ResponseTime = _options.SystemClock.UtcNow; var cachedEntryAge = context.ResponseTime.Value - context.CachedResponse.Created; context.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero; @@ -198,7 +198,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } } - if (context.RequestCacheControlHeaderValue.OnlyIfCached) + if (HttpHeaderParsingHelpers.HeaderContains(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlValues.OnlyIfCachedString)) { _logger.LogGatewayTimeoutServed(); context.HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; @@ -219,8 +219,8 @@ namespace Microsoft.AspNetCore.ResponseCaching var response = context.HttpContext.Response; var varyHeaders = new StringValues(response.Headers.GetCommaSeparatedValues(HeaderNames.Vary)); var varyQueryKeys = new StringValues(context.HttpContext.Features.Get()?.VaryByQueryKeys); - context.CachedResponseValidFor = context.ResponseCacheControlHeaderValue.SharedMaxAge ?? - context.ResponseCacheControlHeaderValue.MaxAge ?? + context.CachedResponseValidFor = context.ResponseSharedMaxAge ?? + context.ResponseMaxAge ?? (context.ResponseExpires - context.ResponseTime.Value) ?? DefaultExpirationTimeSpan; @@ -256,7 +256,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { context.ResponseDate = context.ResponseTime.Value; // Setting the date on the raw response headers. - context.TypedResponseHeaders.Date = context.ResponseDate; + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(context.ResponseDate.Value); } // Store the response on the state @@ -266,7 +266,7 @@ namespace Microsoft.AspNetCore.ResponseCaching StatusCode = context.HttpContext.Response.StatusCode }; - foreach (var header in context.TypedResponseHeaders.Headers) + foreach (var header in context.HttpContext.Response.Headers) { if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase)) { @@ -282,7 +282,7 @@ namespace Microsoft.AspNetCore.ResponseCaching internal async Task FinalizeCacheBodyAsync(ResponseCachingContext context) { - var contentLength = context.TypedResponseHeaders.ContentLength; + var contentLength = context.HttpContext.Response.ContentLength; if (context.ShouldCacheResponse && context.ResponseCachingStream.BufferingEnabled) { var bufferStream = context.ResponseCachingStream.GetBufferStream(); @@ -355,37 +355,51 @@ namespace Microsoft.AspNetCore.ResponseCaching internal static bool ContentIsNotModified(ResponseCachingContext context) { var cachedResponseHeaders = context.CachedResponseHeaders; - var ifNoneMatchHeader = context.TypedRequestHeaders.IfNoneMatch; + var ifNoneMatchHeader = context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch]; - if (ifNoneMatchHeader != null) + if (!StringValues.IsNullOrEmpty(ifNoneMatchHeader)) { - if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any)) + if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any.Tag)) { context.Logger.LogNotModifiedIfNoneMatchStar(); return true; } - if (cachedResponseHeaders.ETag != null) + if (!StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag])) { - foreach (var tag in ifNoneMatchHeader) + EntityTagHeaderValue eTag; + if (EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag], out eTag)) { - if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: false)) + foreach (var tag in ifNoneMatchHeader) { - context.Logger.LogNotModifiedIfNoneMatchMatched(tag); - return true; + EntityTagHeaderValue requestETag; + if (EntityTagHeaderValue.TryParse(tag, out requestETag) && + eTag.Compare(requestETag, useStrongComparison: false)) + { + context.Logger.LogNotModifiedIfNoneMatchMatched(requestETag); + return true; + } } } } } else { - var ifUnmodifiedSince = context.TypedRequestHeaders.IfUnmodifiedSince; - if (ifUnmodifiedSince != null) + var ifUnmodifiedSince = context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince]; + if (!StringValues.IsNullOrEmpty(ifUnmodifiedSince)) { - var lastModified = cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date; - if (lastModified <= ifUnmodifiedSince) + DateTimeOffset modified; + if (!HttpHeaderParsingHelpers.TryParseHeaderDate(cachedResponseHeaders[HeaderNames.LastModified], out modified) && + !HttpHeaderParsingHelpers.TryParseHeaderDate(cachedResponseHeaders[HeaderNames.Date], out modified)) { - context.Logger.LogNotModifiedIfUnmodifiedSinceSatisfied(lastModified.Value, ifUnmodifiedSince.Value); + return false; + } + + DateTimeOffset unmodifiedSince; + if (HttpHeaderParsingHelpers.TryParseHeaderDate(ifUnmodifiedSince, out unmodifiedSince) && + modified <= unmodifiedSince) + { + context.Logger.LogNotModifiedIfUnmodifiedSinceSatisfied(modified, unmodifiedSince); return true; } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs new file mode 100644 index 0000000000..fb495bb535 --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs @@ -0,0 +1,39 @@ +// 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 fb742416b1..ad607e9a1c 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs @@ -23,10 +23,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider()); var context = TestUtils.CreateTestContext(); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { OnlyIfCached = true - }; + }.ToString(); Assert.True(await middleware.TryServeFromCacheAsync(context)); Assert.Equal(StatusCodes.Status504GatewayTimeout, context.HttpContext.Response.StatusCode); @@ -149,7 +149,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); @@ -161,22 +161,22 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); - context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; + context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); // Verify modifications in the past succeeds - context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10); + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Equal(1, sink.Writes.Count); // Verify modifications at present succeeds - context.CachedResponseHeaders.Date = utcNow; + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Equal(2, sink.Writes.Count); // Verify modifications in the future fails - context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); // Verify logging @@ -192,25 +192,25 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); - context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; + context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); // Verify modifications in the past succeeds - context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); - context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Equal(1, sink.Writes.Count); // Verify modifications at present - context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); - context.CachedResponseHeaders.LastModified = utcNow; + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Equal(2, sink.Writes.Count); // Verify modifications in the future fails - context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10); - context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); // Verify logging @@ -226,13 +226,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); // This would fail the IfUnmodifiedSince checks - context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; - context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); + context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); - context.TypedRequestHeaders.IfNoneMatch = new List(new[] { EntityTagHeaderValue.Any }); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = EntityTagHeaderValue.Any.ToString(); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); TestUtils.AssertLoggedMessages( sink.Writes, @@ -245,13 +245,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); // This would pass the IfUnmodifiedSince checks - context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; - context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); + context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); - context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new List(new[] { new EntityTagHeaderValue("\"E1\"") }).ToString(); Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } @@ -261,9 +261,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); - context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new List(new[] { new EntityTagHeaderValue("\"E1\"") }).ToString(); Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); @@ -289,12 +289,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) - { - ETag = responseETag - }; + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.ETag] = responseETag.ToString(); - context.TypedRequestHeaders.IfNoneMatch = new List(new[] { requestETag }); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = requestETag.ToString(); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); TestUtils.AssertLoggedMessages( @@ -307,12 +305,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) - { - ETag = new EntityTagHeaderValue("\"E2\"") - }; + context.CachedResponseHeaders = new HeaderDictionary(); + context.HttpContext.Response.Headers[HeaderNames.ETag] = new EntityTagHeaderValue("\"E2\"").ToString(); - context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new List(new[] { new EntityTagHeaderValue("\"E1\"") }).ToString(); Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); @@ -340,10 +336,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider()); var context = TestUtils.CreateTestContext(); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); Assert.False(context.ShouldCacheResponse); @@ -375,7 +371,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(); context.ResponseTime = utcNow; - context.TypedResponseHeaders.Expires = utcNow + TimeSpan.FromSeconds(11); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(11)); await middleware.FinalizeCacheHeadersAsync(context); @@ -389,12 +385,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(12) - }; + }.ToString(); - context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(context.ResponseTime.Value + TimeSpan.FromSeconds(11)); await middleware.FinalizeCacheHeadersAsync(context); @@ -408,13 +404,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(12), SharedMaxAge = TimeSpan.FromSeconds(13) - }; + }.ToString(); - context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(context.ResponseTime.Value + TimeSpan.FromSeconds(11)); await middleware.FinalizeCacheHeadersAsync(context); @@ -538,11 +534,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(); context.ResponseTime = utcNow; - Assert.Null(context.TypedResponseHeaders.Date); + Assert.True(StringValues.IsNullOrEmpty(context.HttpContext.Response.Headers[HeaderNames.Date])); await middleware.FinalizeCacheHeadersAsync(context); - Assert.Equal(utcNow, context.TypedResponseHeaders.Date); + Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers[HeaderNames.Date]); Assert.Empty(sink.Writes); } @@ -553,14 +549,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); - context.TypedResponseHeaders.Date = utcNow; + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); - Assert.Equal(utcNow, context.TypedResponseHeaders.Date); + Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers[HeaderNames.Date]); await middleware.FinalizeCacheHeadersAsync(context); - Assert.Equal(utcNow, context.TypedResponseHeaders.Date); + Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers[HeaderNames.Date]); Assert.Empty(sink.Writes); } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs index 36fbb6a2d6..02a068e6e4 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs @@ -88,10 +88,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Request.Method = HttpMethods.Get; - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { NoCache = true - }; + }.ToString(); Assert.False(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); TestUtils.AssertLoggedMessages( @@ -105,10 +105,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Request.Method = HttpMethods.Get; - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { NoStore = true - }; + }.ToString(); Assert.True(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); Assert.Empty(sink.Writes); @@ -158,10 +158,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); Assert.Empty(sink.Writes); @@ -172,11 +172,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, NoCache = true - }; + }.ToString(); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( @@ -189,14 +189,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { NoStore = true - }; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( @@ -209,11 +209,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, NoStore = true - }; + }.ToString(); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( @@ -226,10 +226,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); context.HttpContext.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -243,10 +243,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); context.HttpContext.Response.Headers[HeaderNames.Vary] = "*"; Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -260,11 +260,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, Private = true - }; + }.ToString(); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( @@ -279,10 +279,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = statusCode; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); Assert.Empty(sink.Writes); @@ -342,10 +342,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = statusCode; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( @@ -359,13 +359,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); var utcNow = DateTimeOffset.UtcNow; - context.TypedResponseHeaders.Date = utcNow; + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = DateTimeOffset.MaxValue; Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -378,14 +378,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); var utcNow = DateTimeOffset.UtcNow; - context.TypedResponseHeaders.Expires = utcNow; + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); - context.TypedResponseHeaders.Date = utcNow; + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow; Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -401,13 +401,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10) - }; - context.TypedResponseHeaders.Expires = utcNow; - context.TypedResponseHeaders.Date = utcNow; + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(9); Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -421,13 +421,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10) - }; - context.TypedResponseHeaders.Expires = utcNow; - context.TypedResponseHeaders.Date = utcNow; + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -443,13 +443,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10), SharedMaxAge = TimeSpan.FromSeconds(15) - }; - context.TypedResponseHeaders.Date = utcNow; + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -463,13 +463,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10), SharedMaxAge = TimeSpan.FromSeconds(5) - }; - context.TypedResponseHeaders.Date = utcNow; + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(5); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -486,7 +486,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(sink); context.ResponseTime = DateTimeOffset.MaxValue; context.CachedEntryAge = TimeSpan.MaxValue; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); Assert.Empty(sink.Writes); @@ -500,13 +500,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(sink); context.ResponseTime = DateTimeOffset.MaxValue; context.CachedEntryAge = TimeSpan.MaxValue; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - Public = true - } - }; + Public = true + }.ToString(); Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); Assert.Empty(sink.Writes); @@ -520,14 +518,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(sink); context.ResponseTime = utcNow; context.CachedEntryAge = TimeSpan.Zero; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - Public = true - }, - Expires = utcNow - }; + Public = true + }.ToString(); + context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); TestUtils.AssertLoggedMessages( @@ -543,15 +539,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(sink); context.CachedEntryAge = TimeSpan.FromSeconds(9); context.ResponseTime = utcNow + context.CachedEntryAge; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }, - Expires = utcNow - }; + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); Assert.Empty(sink.Writes); @@ -565,15 +559,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(sink); context.CachedEntryAge = TimeSpan.FromSeconds(10); context.ResponseTime = utcNow + context.CachedEntryAge; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }, - Expires = utcNow - }; + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); TestUtils.AssertLoggedMessages( @@ -589,16 +581,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(sink); context.CachedEntryAge = TimeSpan.FromSeconds(11); context.ResponseTime = utcNow + context.CachedEntryAge; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10), - SharedMaxAge = TimeSpan.FromSeconds(15) - }, - Expires = utcNow - }; + Public = true, + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(15) + }.ToString(); + context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); Assert.Empty(sink.Writes); @@ -612,16 +602,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(sink); context.CachedEntryAge = TimeSpan.FromSeconds(5); context.ResponseTime = utcNow + context.CachedEntryAge; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10), - SharedMaxAge = TimeSpan.FromSeconds(5) - }, - Expires = utcNow - }; + Public = true, + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(5) + }.ToString(); + context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); TestUtils.AssertLoggedMessages( @@ -634,18 +622,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MinFresh = TimeSpan.FromSeconds(2) - }; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + }.ToString(); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(10), - SharedMaxAge = TimeSpan.FromSeconds(5) - } - }; + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(5) + }.ToString(); context.CachedEntryAge = TimeSpan.FromSeconds(3); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); @@ -660,17 +646,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5) - }; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + }.ToString(); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(10), - } - }; + MaxAge = TimeSpan.FromSeconds(10), + }.ToString(); context.CachedEntryAge = TimeSpan.FromSeconds(5); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); @@ -684,19 +668,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5), MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit MaxStaleLimit = TimeSpan.FromSeconds(2) - }; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + }.ToString(); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(5), - } - }; + MaxAge = TimeSpan.FromSeconds(5), + }.ToString(); context.CachedEntryAge = TimeSpan.FromSeconds(6); Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); @@ -710,19 +692,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5), MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit MaxStaleLimit = TimeSpan.FromSeconds(1) - }; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + }.ToString(); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(5), - } - }; + MaxAge = TimeSpan.FromSeconds(5), + }.ToString(); context.CachedEntryAge = TimeSpan.FromSeconds(6); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); @@ -736,20 +716,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5), MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit MaxStaleLimit = TimeSpan.FromSeconds(2) - }; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + }.ToString(); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(5), - MustRevalidate = true - } - }; + MaxAge = TimeSpan.FromSeconds(5), + MustRevalidate = true + }.ToString(); context.CachedEntryAge = TimeSpan.FromSeconds(6); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));