From d72ef128dd5b066428aa5fa6e4603961d50560bc Mon Sep 17 00:00:00 2001 From: John Luo Date: Wed, 31 Aug 2016 17:33:01 -0700 Subject: [PATCH] Adding options to specify maximum response body size --- .../Internal/ResponseCacheStream.cs | 33 +- .../ResponseCachingContext.cs | 36 +- .../ResponseCachingExtensions.cs | 15 + .../ResponseCachingMiddleware.cs | 11 +- .../ResponseCachingOptions.cs | 22 + .../ResponseCachingContextTests.cs | 3 +- .../ResponseCachingTests.cs | 516 ++++++------------ 7 files changed, 255 insertions(+), 381 deletions(-) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheStream.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheStream.cs index bf9d90ee21..b8921b85ba 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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public void DisableBuffering() { BufferingEnabled = false; + BufferedStream.SetLength(0); + BufferedStream.Capacity = 0; BufferedStream.Dispose(); } @@ -77,7 +81,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal if (BufferingEnabled) { - BufferedStream.Write(buffer, offset, count); + if (BufferedStream.Length + count > _maxBufferSize) + { + DisableBuffering(); + } + else + { + BufferedStream.Write(buffer, offset, count); + } } } @@ -95,7 +106,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal 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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal 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 17ebe49caf..2733172960 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -7,6 +7,7 @@ using System.Globalization; 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 @@ namespace Microsoft.AspNetCore.ResponseCaching 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 @@ namespace Microsoft.AspNetCore.ResponseCaching 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 @@ namespace Microsoft.AspNetCore.ResponseCaching { 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 @@ namespace Microsoft.AspNetCore.ResponseCaching 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 @@ namespace Microsoft.AspNetCore.ResponseCaching 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 @@ namespace Microsoft.AspNetCore.ResponseCaching if (!ResponseStarted) { ResponseStarted = true; - _responseTime = _clock.UtcNow; + _responseTime = _options.SystemClock.UtcNow; FinalizeCachingHeaders(); } @@ -619,7 +615,7 @@ namespace Microsoft.AspNetCore.ResponseCaching // 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 45d905cea6..037863e4cf 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 @@ namespace Microsoft.AspNetCore.Builder 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 5fe8718134..96c1a713a3 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 @@ namespace Microsoft.AspNetCore.ResponseCaching 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 @@ namespace Microsoft.AspNetCore.ResponseCaching { 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 @@ namespace Microsoft.AspNetCore.ResponseCaching _next = next; _cache = cache; + _options = options.Value; _builderPool = poolProvider.CreateStringBuilderPool(); _cacheabilityValidator = cacheabilityValidator; _cacheKeyModifier = cacheKeyModifier; @@ -64,6 +72,7 @@ namespace Microsoft.AspNetCore.ResponseCaching 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 0000000000..753b9b6b72 --- /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 e66a33af06..2662c56b78 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; 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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests 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 400e5af249..783dfd8f86 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs @@ -21,19 +21,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { 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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { 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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { 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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryByParamsExplicit_Matches_ParamNameCaseInsensitive() { - var builder = CreateBuilderWithResponseCaching( - app => + var builder = CreateBuilderWithResponseCaching(async (context) => { - app.Use(async (context, next) => - { - context.Features.Set(new DummySendFileFeature()); - await next.Invoke(); - }); - }, - 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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryByParamsStar_Matches_ParamNameCaseInsensitive() { - var builder = CreateBuilderWithResponseCaching( - app => + var builder = CreateBuilderWithResponseCaching(async (context) => { - app.Use(async (context, next) => - { - context.Features.Set(new DummySendFileFeature()); - await next.Invoke(); - }); - }, - 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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { 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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { 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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { 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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { 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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { 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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { 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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfIHttpSendFileFeature_NotUsed() { - var builder = CreateBuilderWithResponseCaching( - app => + var builder = CreateBuilderWithResponseCaching(app => + { + app.Use(async (context, next) => { - app.Use(async (context, next) => - { - context.Features.Set(new DummySendFileFeature()); - await next.Invoke(); - }); - }, - 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); + context.Features.Set(new DummySendFileFeature()); + await next.Invoke(); }); + }); using (var server = new TestServer(builder)) { @@ -527,17 +348,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }, 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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [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 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [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,6 +400,116 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } } + [Fact] + public async void Serves304_IfIfModifiedSince_Satisfied() + { + var builder = CreateBuilderWithResponseCaching(); + + using (var server = new TestServer(builder)) + { + 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); + } + } + + [Fact] + public async void ServesCachedContent_IfIfModifiedSince_NotSatisfied() + { + var builder = CreateBuilderWithResponseCaching(); + + 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_IfIfNoneMatch_Satisfied() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + 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.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E1\"")); + var subsequentResponse = await client.GetAsync(""); + + initialResponse.EnsureSuccessStatusCode(); + Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); + } + } + + [Fact] + public async void ServesCachedContent_IfIfNoneMatch_NotSatisfied() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + 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.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E2\"")); + var subsequentResponse = await client.GetAsync(""); + + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } + } + + [Fact] + public async void ServesCachedContent_IfBodySize_IsCacheable() + { + var builder = CreateBuilderWithResponseCaching(new ResponseCachingOptions() + { + MaximumCachedBodySize = 100 + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); + + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } + } + + [Fact] + public async void ServesFreshContent_IfBodySize_IsNotCacheable() + { + var builder = CreateBuilderWithResponseCaching(new ResponseCachingOptions() + { + MaximumCachedBodySize = 1 + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync("/different"); + + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + } + } + private static async Task AssertResponseCachedAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) { initialResponse.EnsureSuccessStatusCode(); @@ -636,126 +532,36 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); } - [Fact] - public async void Serves304_IfIfModifiedSince_Satisfied() + private static RequestDelegate DefaultRequestDelegate = async (context) => { - var builder = CreateBuilderWithResponseCaching(async (context) => + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() { - 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); - }); + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + await context.Response.WriteAsync(uniqueId); + }; - using (var server = new TestServer(builder)) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MaxValue; - var subsequentResponse = await client.GetAsync(""); + private static IWebHostBuilder CreateBuilderWithResponseCaching() => + CreateBuilderWithResponseCaching(app => { }, new ResponseCachingOptions(), DefaultRequestDelegate); - initialResponse.EnsureSuccessStatusCode(); - Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); - } - } - - [Fact] - public async void ServesCachedContent_IfIfModifiedSince_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); - }); - - 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_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; - headers.ETag = new EntityTagHeaderValue("\"E1\""); - await context.Response.WriteAsync(uniqueId); - }); - - 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); - } - } - - [Fact] - 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; - headers.ETag = new EntityTagHeaderValue("\"E1\""); - await context.Response.WriteAsync(uniqueId); - }); - - 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(""); - - await AssertResponseCachedAsync(initialResponse, subsequentResponse); - } - } + 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, RequestDelegate 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, ResponseCachingOptions options, RequestDelegate requestDelegate) { return new WebHostBuilder() .ConfigureServices(services => @@ -765,7 +571,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests .Configure(app => { configureDelegate(app); - app.UseResponseCaching(); + app.UseResponseCaching(options); app.Run(requestDelegate); }); }