diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs index 51083e4..62734f8 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs @@ -13,7 +13,7 @@ public class CachedResponse : IResponseCacheEntry public int StatusCode { get; set; } - public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); + public IHeaderDictionary Headers { get; set; } public Stream Body { get; set; } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs index f2509c8..4023345 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs @@ -4,6 +4,7 @@ using System; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.ResponseCaching.Internal { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs index 3340226..4cb9fcf 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs @@ -107,13 +107,18 @@ public string CreateStorageVaryByKey(ResponseCachingContext context) builder.Append(KeyDelimiter) .Append('H'); - foreach (var header in varyByRules.Headers) + for (var i = 0; i < varyByRules.Headers.Count; i++) { + var header = varyByRules.Headers[i]; + var headerValues = context.HttpContext.Request.Headers[header]; builder.Append(KeyDelimiter) .Append(header) - .Append("=") - // TODO: Perf - iterate the string values instead? - .Append(context.HttpContext.Request.Headers[header]); + .Append("="); + + for (var j = 0; j < headerValues.Count; j++) + { + builder.Append(headerValues[j]); + } } } @@ -131,19 +136,28 @@ public string CreateStorageVaryByKey(ResponseCachingContext context) { builder.Append(KeyDelimiter) .AppendUpperInvariant(query.Key) - .Append("=") - .Append(query.Value); + .Append("="); + + for (var i = 0; i < query.Value.Count; i++) + { + builder.Append(query.Value[i]); + } } } else { - foreach (var queryKey in varyByRules.QueryKeys) + for (var i = 0; i < varyByRules.QueryKeys.Count; i++) { + var queryKey = varyByRules.QueryKeys[i]; + var queryKeyValues = context.HttpContext.Request.Query[queryKey]; builder.Append(KeyDelimiter) .Append(queryKey) - .Append("=") - // TODO: Perf - iterate the string values instead? - .Append(context.HttpContext.Request.Query[queryKey]); + .Append("="); + + for (var j = 0; j < queryKeyValues.Count; j++) + { + builder.Append(queryKeyValues[j]); + } } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index a15353a..2ac2000 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -142,18 +141,15 @@ internal async Task TryServeCachedResponseAsync(ResponseCachingContext con response.Headers.Add(header); } - response.Headers[HeaderNames.Age] = context.CachedEntryAge.Value.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture); + // Note: int64 division truncates result and errors may be up to 1 second. This reduction in + // accuracy of age calculation is considered appropriate since it is small compared to clock + // skews and the "Age" header is an estimate of the real age of cached content. + response.Headers[HeaderNames.Age] = HeaderUtilities.FormatInt64(context.CachedEntryAge.Value.Ticks / TimeSpan.TicksPerSecond); // Copy the cached response body var body = context.CachedResponse.Body; if (body.Length > 0) { - // Add a content-length if required - if (!response.ContentLength.HasValue && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) - { - response.ContentLength = body.Length; - } - try { await body.CopyToAsync(response.Body, StreamUtilities.BodySegmentSize, context.HttpContext.RequestAborted); @@ -263,7 +259,8 @@ internal async Task FinalizeCacheHeadersAsync(ResponseCachingContext context) context.CachedResponse = new CachedResponse { Created = context.ResponseDate.Value, - StatusCode = context.HttpContext.Response.StatusCode + StatusCode = context.HttpContext.Response.StatusCode, + Headers = new HeaderDictionary() }; foreach (var header in context.HttpContext.Response.Headers) @@ -288,6 +285,13 @@ internal async Task FinalizeCacheBodyAsync(ResponseCachingContext context) var bufferStream = context.ResponseCachingStream.GetBufferStream(); if (!contentLength.HasValue || contentLength == bufferStream.Length) { + var response = context.HttpContext.Response; + // Add a content-length if required + if (!response.ContentLength.HasValue && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) + { + context.CachedResponse.Headers[HeaderNames.ContentLength] = HeaderUtilities.FormatInt64(bufferStream.Length); + } + context.CachedResponse.Body = bufferStream; _logger.LogResponseCached(); await _cache.SetAsync(context.StorageVaryKey ?? context.BaseKey, context.CachedResponse, context.CachedResponseValidFor); @@ -371,8 +375,9 @@ internal static bool ContentIsNotModified(ResponseCachingContext context) && EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag], out eTag) && EntityTagHeaderValue.TryParseList(ifNoneMatchHeader, out ifNoneMatchEtags)) { - foreach (var requestETag in ifNoneMatchEtags) + for (var i = 0; i < ifNoneMatchEtags.Count; i++) { + var requestETag = ifNoneMatchEtags[i]; if (eTag.Compare(requestETag, useStrongComparison: false)) { context.Logger.LogNotModifiedIfNoneMatchMatched(requestETag); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs index bced86f..a31d7c3 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs @@ -61,6 +61,7 @@ await cache.SetAsync( "BaseKey", new CachedResponse() { + Headers = new HeaderDictionary(), Body = new SegmentReadStream(new List(0), 0) }, TimeSpan.Zero); @@ -108,6 +109,7 @@ await cache.SetAsync( "BaseKeyVaryKey2", new CachedResponse() { + Headers = new HeaderDictionary(), Body = new SegmentReadStream(new List(0), 0) }, TimeSpan.Zero); @@ -666,7 +668,10 @@ public async Task FinalizeCacheBody_Cache_IfContentLengthAbsent() await context.HttpContext.Response.WriteAsync(new string('0', 10)); context.ShouldCacheResponse = true; - context.CachedResponse = new CachedResponse(); + context.CachedResponse = new CachedResponse() + { + Headers = new HeaderDictionary() + }; context.BaseKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10);