diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheStream.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheStream.cs index bf9d90e..b8921b8 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheStream.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheStream.cs @@ -11,10 +11,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal internal class ResponseCacheStream : Stream { private readonly Stream _innerStream; + private readonly long _maxBufferSize; - public ResponseCacheStream(Stream innerStream) + public ResponseCacheStream(Stream innerStream, long maxBufferSize) { _innerStream = innerStream; + _maxBufferSize = maxBufferSize; } public MemoryStream BufferedStream { get; } = new MemoryStream(); @@ -38,6 +40,8 @@ public override long Position public void DisableBuffering() { BufferingEnabled = false; + BufferedStream.SetLength(0); + BufferedStream.Capacity = 0; BufferedStream.Dispose(); } @@ -77,7 +81,14 @@ public override void Write(byte[] buffer, int offset, int count) if (BufferingEnabled) { - BufferedStream.Write(buffer, offset, count); + if (BufferedStream.Length + count > _maxBufferSize) + { + DisableBuffering(); + } + else + { + BufferedStream.Write(buffer, offset, count); + } } } @@ -95,7 +106,14 @@ public override async Task WriteAsync(byte[] buffer, int offset, int count, Canc if (BufferingEnabled) { - await BufferedStream.WriteAsync(buffer, offset, count, cancellationToken); + if (BufferedStream.Length + count > _maxBufferSize) + { + DisableBuffering(); + } + else + { + await BufferedStream.WriteAsync(buffer, offset, count, cancellationToken); + } } } @@ -113,7 +131,14 @@ public override void WriteByte(byte value) if (BufferingEnabled) { - BufferedStream.WriteByte(value); + if (BufferedStream.Length + 1 > _maxBufferSize) + { + DisableBuffering(); + } + else + { + BufferedStream.WriteByte(value); + } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index 17ebe49..2733172 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Headers; @@ -25,7 +26,7 @@ internal class ResponseCachingContext private readonly HttpContext _httpContext; private readonly IResponseCache _cache; - private readonly ISystemClock _clock; + private readonly ResponseCachingOptions _options; private readonly ObjectPool _builderPool; private readonly IResponseCachingCacheabilityValidator _cacheabilityValidator; private readonly IResponseCachingCacheKeyModifier _cacheKeyModifier; @@ -40,29 +41,19 @@ internal class ResponseCachingContext private CachedResponse _cachedResponse; private TimeSpan _cachedResponseValidFor; internal DateTimeOffset _responseTime; - - internal ResponseCachingContext( - HttpContext httpContext, - IResponseCache cache, - ObjectPool builderPool, - IResponseCachingCacheabilityValidator cacheabilityValidator, - IResponseCachingCacheKeyModifier cacheKeyModifier) - : this(httpContext, cache, new SystemClock(), builderPool, cacheabilityValidator, cacheKeyModifier) - { - } // Internal for testing internal ResponseCachingContext( HttpContext httpContext, IResponseCache cache, - ISystemClock clock, + ResponseCachingOptions options, ObjectPool builderPool, IResponseCachingCacheabilityValidator cacheabilityValidator, IResponseCachingCacheKeyModifier cacheKeyModifier) { _httpContext = httpContext; _cache = cache; - _clock = clock; + _options = options; _builderPool = builderPool; _cacheabilityValidator = cacheabilityValidator; _cacheKeyModifier = cacheKeyModifier; @@ -74,10 +65,7 @@ internal bool CacheResponse { if (_cacheResponse == null) { - // TODO: apparent age vs corrected age value - var responseAge = _responseTime - ResponseHeaders.Date ?? TimeSpan.Zero; - - _cacheResponse = ResponseIsCacheable() && EntryIsFresh(ResponseHeaders, responseAge, verifyAgainstRequest: false); + _cacheResponse = ResponseIsCacheable(); } return _cacheResponse.Value; } @@ -363,6 +351,14 @@ internal bool ResponseIsCacheable() return false; } + // Check response freshness + // TODO: apparent age vs corrected age value + var responseAge = _responseTime - ResponseHeaders.Date ?? TimeSpan.Zero; + if (!EntryIsFresh(ResponseHeaders, responseAge, verifyAgainstRequest: false)) + { + return false; + } + return true; } @@ -433,7 +429,7 @@ internal async Task TryServeFromCacheAsync() var cachedResponse = cacheEntry as CachedResponse; var cachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers); - _responseTime = _clock.UtcNow; + _responseTime = _options.SystemClock.UtcNow; var age = _responseTime - cachedResponse.Created; age = age > TimeSpan.Zero ? age : TimeSpan.Zero; @@ -607,7 +603,7 @@ internal void OnResponseStarting() if (!ResponseStarted) { ResponseStarted = true; - _responseTime = _clock.UtcNow; + _responseTime = _options.SystemClock.UtcNow; FinalizeCachingHeaders(); } @@ -619,7 +615,7 @@ internal void ShimResponseStream() // Shim response stream OriginalResponseStream = _httpContext.Response.Body; - ResponseCacheStream = new ResponseCacheStream(OriginalResponseStream); + ResponseCacheStream = new ResponseCacheStream(OriginalResponseStream, _options.MaximumCachedBodySize); _httpContext.Response.Body = ResponseCacheStream; // Shim IHttpSendFileFeature diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs index 45d905c..037863e 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.ResponseCaching; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder { @@ -17,5 +18,19 @@ public static IApplicationBuilder UseResponseCaching(this IApplicationBuilder ap return app.UseMiddleware(); } + + public static IApplicationBuilder UseResponseCaching(this IApplicationBuilder app, ResponseCachingOptions options) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + return app.UseMiddleware(Options.Create(options)); + } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index 5fe8718..96c1a71 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -4,6 +4,7 @@ using System; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; @@ -20,13 +21,15 @@ public class ResponseCachingMiddleware private readonly RequestDelegate _next; private readonly IResponseCache _cache; + private readonly ResponseCachingOptions _options; private readonly ObjectPool _builderPool; private readonly IResponseCachingCacheabilityValidator _cacheabilityValidator; private readonly IResponseCachingCacheKeyModifier _cacheKeyModifier; public ResponseCachingMiddleware( - RequestDelegate next, + RequestDelegate next, IResponseCache cache, + IOptions options, ObjectPoolProvider poolProvider, IResponseCachingCacheabilityValidator cacheabilityValidator, IResponseCachingCacheKeyModifier cacheKeyModifier) @@ -39,6 +42,10 @@ public ResponseCachingMiddleware( { throw new ArgumentNullException(nameof(cache)); } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } if (poolProvider == null) { throw new ArgumentNullException(nameof(poolProvider)); @@ -54,6 +61,7 @@ public ResponseCachingMiddleware( _next = next; _cache = cache; + _options = options.Value; _builderPool = poolProvider.CreateStringBuilderPool(); _cacheabilityValidator = cacheabilityValidator; _cacheKeyModifier = cacheKeyModifier; @@ -64,6 +72,7 @@ public async Task Invoke(HttpContext context) var cachingContext = new ResponseCachingContext( context, _cache, + _options, _builderPool, _cacheabilityValidator, _cacheKeyModifier); diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs new file mode 100644 index 0000000..753b9b6 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs @@ -0,0 +1,22 @@ +// 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.ComponentModel; +using Microsoft.AspNetCore.ResponseCaching.Internal; + +namespace Microsoft.AspNetCore.Builder +{ + public class ResponseCachingOptions + { + /// + /// The largest cacheable size for the response body in bytes. + /// + public long MaximumCachedBodySize { get; set; } = 1024 * 1024; + + /// + /// For testing purposes only. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal ISystemClock SystemClock { get; set; } = new SystemClock(); + } +} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs index e66a33a..2662c56 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Headers; @@ -859,7 +860,7 @@ private static ResponseCachingContext CreateTestContext( return new ResponseCachingContext( httpContext, new TestResponseCache(), - new SystemClock(), + new ResponseCachingOptions(), new DefaultObjectPool(new StringBuilderPooledObjectPolicy()), cacheabilityValidator, cacheKeyModifier); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs index 400e5af..783dfd8 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs @@ -21,19 +21,7 @@ public class ResponseCachingTests [Fact] public async void ServesCachedContent_IfAvailable() { - var builder = CreateBuilderWithResponseCaching(async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); - }); + var builder = CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -48,19 +36,7 @@ public async void ServesCachedContent_IfAvailable() [Fact] public async void ServesFreshContent_IfNotAvailable() { - var builder = CreateBuilderWithResponseCaching(async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); - }); + var builder = CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -77,17 +53,8 @@ public async void ServesCachedContent_IfVaryByHeader_Matches() { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -106,17 +73,8 @@ public async void ServesFreshContent_IfVaryByHeader_Mismatches() { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -136,17 +94,8 @@ public async void ServesCachedContent_IfVaryByParams_Matches() { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.GetResponseCachingFeature().VaryByParams = "param"; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -162,28 +111,10 @@ public async void ServesCachedContent_IfVaryByParams_Matches() [Fact] public async void ServesCachedContent_IfVaryByParamsExplicit_Matches_ParamNameCaseInsensitive() { - var builder = CreateBuilderWithResponseCaching( - app => - { - app.Use(async (context, next) => - { - context.Features.Set(new DummySendFileFeature()); - await next.Invoke(); - }); - }, - async (context) => + var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.GetResponseCachingFeature().VaryByParams = new[] { "ParamA", "paramb" }; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -199,28 +130,10 @@ public async void ServesCachedContent_IfVaryByParamsExplicit_Matches_ParamNameCa [Fact] public async void ServesCachedContent_IfVaryByParamsStar_Matches_ParamNameCaseInsensitive() { - var builder = CreateBuilderWithResponseCaching( - app => - { - app.Use(async (context, next) => - { - context.Features.Set(new DummySendFileFeature()); - await next.Invoke(); - }); - }, - async (context) => + var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.GetResponseCachingFeature().VaryByParams = new[] { "*" }; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -238,17 +151,8 @@ public async void ServesCachedContent_IfVaryByParamsExplicit_Matches_OrderInsens { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.GetResponseCachingFeature().VaryByParams = new[] { "ParamB", "ParamA" }; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -266,17 +170,8 @@ public async void ServesCachedContent_IfVaryByParamsStar_Matches_OrderInsensitiv { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.GetResponseCachingFeature().VaryByParams = new[] { "*" }; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -294,17 +189,8 @@ public async void ServesFreshContent_IfVaryByParams_Mismatches() { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.GetResponseCachingFeature().VaryByParams = "param"; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -322,17 +208,8 @@ public async void ServesFreshContent_IfVaryByParamsExplicit_Mismatch_ParamValueC { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.GetResponseCachingFeature().VaryByParams = new[] { "ParamA", "ParamB" }; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -350,17 +227,8 @@ public async void ServesFreshContent_IfVaryByParamsStar_Mismatch_ParamValueCaseS { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.GetResponseCachingFeature().VaryByParams = new[] { "*" }; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -376,19 +244,7 @@ public async void ServesFreshContent_IfVaryByParamsStar_Mismatch_ParamValueCaseS [Fact] public async void ServesFreshContent_IfRequestRequirements_NotMet() { - var builder = CreateBuilderWithResponseCaching(async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); - }); + var builder = CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -407,19 +263,7 @@ public async void ServesFreshContent_IfRequestRequirements_NotMet() [Fact] public async void Serves504_IfOnlyIfCachedHeader_IsSpecified() { - var builder = CreateBuilderWithResponseCaching(async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); - }); + var builder = CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -441,17 +285,8 @@ public async void ServesCachedContent_WithoutSetCookie() { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - headers.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; - await context.Response.WriteAsync(uniqueId); + var headers = context.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -480,28 +315,14 @@ public async void ServesCachedContent_WithoutSetCookie() [Fact] public async void ServesCachedContent_IfIHttpSendFileFeature_NotUsed() { - var builder = CreateBuilderWithResponseCaching( - app => - { - app.Use(async (context, next) => - { - context.Features.Set(new DummySendFileFeature()); - await next.Invoke(); - }); - }, - async (context) => + var builder = CreateBuilderWithResponseCaching(app => + { + app.Use(async (context, next) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); + context.Features.Set(new DummySendFileFeature()); + await next.Invoke(); }); + }); using (var server = new TestServer(builder)) { @@ -527,17 +348,8 @@ public async void ServesFreshContent_IfIHttpSendFileFeature_Used() }, async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; await context.Features.Get().SendFileAsync("dummy", 0, 0, CancellationToken.None); - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -553,20 +365,7 @@ public async void ServesFreshContent_IfIHttpSendFileFeature_Used() [Fact] public async void ServesCachedContent_IfSubsequentRequest_ContainsNoStore() { - var builder = CreateBuilderWithResponseCaching( - async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); - }); + var builder = CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -585,20 +384,7 @@ public async void ServesCachedContent_IfSubsequentRequest_ContainsNoStore() [Fact] public async void ServesFreshContent_IfInitialRequestContains_NoStore() { - var builder = CreateBuilderWithResponseCaching( - async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); - }); + var builder = CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -614,50 +400,53 @@ public async void ServesFreshContent_IfInitialRequestContains_NoStore() } } - private static async Task AssertResponseCachedAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) + [Fact] + public async void Serves304_IfIfModifiedSince_Satisfied() { - initialResponse.EnsureSuccessStatusCode(); - subsequentResponse.EnsureSuccessStatusCode(); + var builder = CreateBuilderWithResponseCaching(); - foreach (var header in initialResponse.Headers) + using (var server = new TestServer(builder)) { - Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key)); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MaxValue; + var subsequentResponse = await client.GetAsync(""); + + initialResponse.EnsureSuccessStatusCode(); + Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); } - Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age)); - Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); } - private static async Task AssertResponseNotCachedAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) + [Fact] + public async void ServesCachedContent_IfIfModifiedSince_NotSatisfied() { - initialResponse.EnsureSuccessStatusCode(); - subsequentResponse.EnsureSuccessStatusCode(); + var builder = CreateBuilderWithResponseCaching(); - Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); - Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue; + var subsequentResponse = await client.GetAsync(""); + + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } } [Fact] - public async void Serves304_IfIfModifiedSince_Satisfied() + public async void Serves304_IfIfNoneMatch_Satisfied() { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); + var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) { var client = server.CreateClient(); var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MaxValue; + client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E1\"")); var subsequentResponse = await client.GetAsync(""); initialResponse.EnsureSuccessStatusCode(); @@ -666,27 +455,19 @@ public async void Serves304_IfIfModifiedSince_Satisfied() } [Fact] - public async void ServesCachedContent_IfIfModifiedSince_NotSatisfied() + public async void ServesCachedContent_IfIfNoneMatch_NotSatisfied() { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); + var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) { var client = server.CreateClient(); var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue; + client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E2\"")); var subsequentResponse = await client.GetAsync(""); await AssertResponseCachedAsync(initialResponse, subsequentResponse); @@ -694,68 +475,93 @@ public async void ServesCachedContent_IfIfModifiedSince_NotSatisfied() } [Fact] - public async void Serves304_IfIfNoneMatch_Satisfied() + public async void ServesCachedContent_IfBodySize_IsCacheable() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = CreateBuilderWithResponseCaching(new ResponseCachingOptions() { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - headers.ETag = new EntityTagHeaderValue("\"E1\""); - await context.Response.WriteAsync(uniqueId); + MaximumCachedBodySize = 100 }); using (var server = new TestServer(builder)) { var client = server.CreateClient(); var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E1\"")); var subsequentResponse = await client.GetAsync(""); - initialResponse.EnsureSuccessStatusCode(); - Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); + await AssertResponseCachedAsync(initialResponse, subsequentResponse); } } [Fact] - public async void ServesCachedContent_IfIfNoneMatch_NotSatisfied() + public async void ServesFreshContent_IfBodySize_IsNotCacheable() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = CreateBuilderWithResponseCaching(new ResponseCachingOptions() { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - headers.ETag = new EntityTagHeaderValue("\"E1\""); - await context.Response.WriteAsync(uniqueId); + MaximumCachedBodySize = 1 }); using (var server = new TestServer(builder)) { var client = server.CreateClient(); var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E2\"")); - var subsequentResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync("/different"); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + } + } + + private static async Task AssertResponseCachedAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) + { + initialResponse.EnsureSuccessStatusCode(); + subsequentResponse.EnsureSuccessStatusCode(); + + foreach (var header in initialResponse.Headers) + { + Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key)); } + Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age)); + Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + } + + private static async Task AssertResponseNotCachedAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) + { + initialResponse.EnsureSuccessStatusCode(); + subsequentResponse.EnsureSuccessStatusCode(); + + Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); + Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); } + private static RequestDelegate DefaultRequestDelegate = async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + await context.Response.WriteAsync(uniqueId); + }; + + private static IWebHostBuilder CreateBuilderWithResponseCaching() => + CreateBuilderWithResponseCaching(app => { }, new ResponseCachingOptions(), DefaultRequestDelegate); + + private static IWebHostBuilder CreateBuilderWithResponseCaching(ResponseCachingOptions options) => + CreateBuilderWithResponseCaching(app => { }, options, DefaultRequestDelegate); + private static IWebHostBuilder CreateBuilderWithResponseCaching(RequestDelegate requestDelegate) => - CreateBuilderWithResponseCaching(app => { }, requestDelegate); + CreateBuilderWithResponseCaching(app => { }, new ResponseCachingOptions(), requestDelegate); + + private static IWebHostBuilder CreateBuilderWithResponseCaching(Action configureDelegate) => + CreateBuilderWithResponseCaching(configureDelegate, new ResponseCachingOptions(), DefaultRequestDelegate); + + private static IWebHostBuilder CreateBuilderWithResponseCaching(Action configureDelegate, RequestDelegate requestDelegate) => + CreateBuilderWithResponseCaching(configureDelegate, new ResponseCachingOptions(), requestDelegate); - private static IWebHostBuilder CreateBuilderWithResponseCaching(Action configureDelegate, RequestDelegate requestDelegate) + private static IWebHostBuilder CreateBuilderWithResponseCaching(Action configureDelegate, ResponseCachingOptions options, RequestDelegate requestDelegate) { return new WebHostBuilder() .ConfigureServices(services => @@ -765,7 +571,7 @@ private static IWebHostBuilder CreateBuilderWithResponseCaching(Action { configureDelegate(app); - app.UseResponseCaching(); + app.UseResponseCaching(options); app.Run(requestDelegate); }); }