From 3d0201bb567681c1eb361cda16ce8e0a512c72ac Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Fri, 5 Aug 2022 14:37:17 -0700 Subject: [PATCH 01/12] Sync output cache with API review --- .../OutputCaching/src/OutputCacheContext.cs | 2 +- .../src/OutputCachePolicyBuilder.cs | 20 ++++++++++++++++--- .../OutputCaching/src/PublicAPI.Unshipped.txt | 3 ++- .../OutputCaching/test/TestUtils.cs | 6 +++--- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/Middleware/OutputCaching/src/OutputCacheContext.cs b/src/Middleware/OutputCaching/src/OutputCacheContext.cs index 667ca5dcce94..0b03649de2bf 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheContext.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheContext.cs @@ -52,7 +52,7 @@ internal OutputCacheContext(HttpContext httpContext, IOutputCacheStore store, Ou /// /// Gets the instance. /// - public CacheVaryByRules CacheVaryByRules { get; set; } = new(); + public CacheVaryByRules CacheVaryByRules { get; internal set; } = new(); /// /// Gets the tags of the cached response. diff --git a/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs index e2c2e59403bb..dd09c2360605 100644 --- a/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs +++ b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs @@ -17,7 +17,7 @@ public sealed class OutputCachePolicyBuilder private IOutputCachePolicy? _builtPolicy; private readonly List _policies = new(); - private List>>? _requirements; + private List>>? _requirements; /// /// Creates a new instance. @@ -57,7 +57,7 @@ public OutputCachePolicyBuilder AddPolicy([DynamicallyAccessedMembers(Dynamicall /// Adds a requirement to the current policy. /// /// The predicate applied to the policy. - public OutputCachePolicyBuilder With(Func> predicate) + public OutputCachePolicyBuilder With(Func> predicate) { ArgumentNullException.ThrowIfNull(predicate); @@ -77,7 +77,7 @@ public OutputCachePolicyBuilder With(Func predicate) _builtPolicy = null; _requirements ??= new(); - _requirements.Add((c, t) => Task.FromResult(predicate(c))); + _requirements.Add((c, t) => ValueTask.FromResult(predicate(c))); return this; } @@ -206,6 +206,20 @@ public OutputCachePolicyBuilder NoCache() return AddPolicy(EnableCachePolicy.Disabled); } + /// + /// Enables caching for the current request if not already enabled. + /// + public OutputCachePolicyBuilder Enable() + { + // If no custom policy is added, the DefaultPolicy is already "enabled". + if (_policies.Count != 1 || _policies[0] != DefaultPolicy.Instance) + { + AddPolicy(EnableCachePolicy.Enabled); + } + + return this; + } + /// /// Creates the . /// diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index 779e48f3530a..e7e01319a00f 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -42,6 +42,7 @@ Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.AddPolicy(System.Typ Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.AddPolicy() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.AllowLocking(bool lockResponse = true) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Clear() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Enable() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Expire(System.TimeSpan expiration) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.NoCache() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.OutputCachePolicyBuilder() -> void @@ -52,7 +53,6 @@ Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.F Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func>>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.With(System.Func!>! predicate) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCacheContext Microsoft.AspNetCore.OutputCaching.OutputCacheContext.AllowCacheLookup.get -> bool Microsoft.AspNetCore.OutputCaching.OutputCacheContext.AllowCacheLookup.set -> void @@ -75,6 +75,7 @@ Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.SizeLimit.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.UseCaseSensitivePaths.get -> bool Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.UseCaseSensitivePaths.set -> void Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.With(System.Func! predicate) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.With(System.Func>! predicate) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.Extensions.DependencyInjection.OutputCacheConventionBuilderExtensions Microsoft.Extensions.DependencyInjection.OutputCacheServiceCollectionExtensions static Microsoft.AspNetCore.Builder.OutputCacheApplicationBuilderExtensions.UseOutputCache(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs index 33ac6f56c869..149b26f1ebd6 100644 --- a/src/Middleware/OutputCaching/test/TestUtils.cs +++ b/src/Middleware/OutputCaching/test/TestUtils.cs @@ -319,12 +319,12 @@ internal class TestOutputCache : IOutputCacheStore public int GetCount { get; private set; } public int SetCount { get; private set; } - public ValueTask EvictByTagAsync(string tag, CancellationToken token) + public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public ValueTask GetAsync(string? key, CancellationToken token) + public ValueTask GetAsync(string? key, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(key); @@ -339,7 +339,7 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken token) } } - public ValueTask SetAsync(string key, byte[] entry, string[]? tags, TimeSpan validFor, CancellationToken token) + public ValueTask SetAsync(string key, byte[] entry, string[]? tags, TimeSpan validFor, CancellationToken cancellationToken) { SetCount++; _storage[key] = entry; From dd692b51a2f5a853c24aaa7ec9cfc7e594c84ee9 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Fri, 5 Aug 2022 15:35:21 -0700 Subject: [PATCH 02/12] Fix public API --- src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index e7e01319a00f..fdee93dbf51a 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -86,6 +86,5 @@ static Microsoft.Extensions.DependencyInjection.OutputCacheConventionBuilderExte static Microsoft.Extensions.DependencyInjection.OutputCacheServiceCollectionExtensions.AddOutputCache(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.OutputCacheServiceCollectionExtensions.AddOutputCache(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! Microsoft.AspNetCore.OutputCaching.OutputCacheContext.CacheVaryByRules.get -> Microsoft.AspNetCore.OutputCaching.CacheVaryByRules! -Microsoft.AspNetCore.OutputCaching.OutputCacheContext.CacheVaryByRules.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext! Microsoft.AspNetCore.OutputCaching.OutputCacheContext.Tags.get -> System.Collections.Generic.HashSet! From 08d83ade8e12ce7576109534bd67b631a0688505 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 8 Aug 2022 16:15:34 -0700 Subject: [PATCH 03/12] Rename Headers to HeaderNames --- .../OutputCaching/src/CacheVaryByRules.cs | 2 +- .../OutputCaching/src/LoggerExtensions.cs | 4 ++-- .../OutputCaching/src/OutputCacheAttribute.cs | 2 +- .../OutputCaching/src/OutputCacheKeyProvider.cs | 4 ++-- .../OutputCaching/src/OutputCacheMiddleware.cs | 4 ++-- .../src/OutputCachePolicyBuilder.cs | 8 ++++---- .../src/Policies/VaryByHeaderPolicy.cs | 16 ++++++++-------- .../OutputCaching/src/PublicAPI.Unshipped.txt | 10 +++++----- .../test/OutputCacheKeyProviderTests.cs | 7 +++---- .../test/OutputCachePoliciesTests.cs | 6 +++--- 10 files changed, 31 insertions(+), 32 deletions(-) diff --git a/src/Middleware/OutputCaching/src/CacheVaryByRules.cs b/src/Middleware/OutputCaching/src/CacheVaryByRules.cs index 28caea7acea2..696b46de7d07 100644 --- a/src/Middleware/OutputCaching/src/CacheVaryByRules.cs +++ b/src/Middleware/OutputCaching/src/CacheVaryByRules.cs @@ -23,7 +23,7 @@ public sealed class CacheVaryByRules /// /// Gets or sets the list of headers to vary by. /// - public StringValues Headers { get; set; } + public StringValues HeaderNames { get; set; } /// /// Gets or sets the list of query string keys to vary by. diff --git a/src/Middleware/OutputCaching/src/LoggerExtensions.cs b/src/Middleware/OutputCaching/src/LoggerExtensions.cs index 642f7252a623..4875f319c619 100644 --- a/src/Middleware/OutputCaching/src/LoggerExtensions.cs +++ b/src/Middleware/OutputCaching/src/LoggerExtensions.cs @@ -50,8 +50,8 @@ internal static partial class LoggerExtensions [LoggerMessage(11, LogLevel.Information, "No cached response available for this request.", EventName = "NoResponseServed")] internal static partial void NoResponseServed(this ILogger logger); - [LoggerMessage(12, LogLevel.Debug, "Vary by rules were updated. Headers: {Headers}, Query keys: {QueryKeys}", EventName = "VaryByRulesUpdated")] - internal static partial void VaryByRulesUpdated(this ILogger logger, string headers, string queryKeys); + [LoggerMessage(12, LogLevel.Debug, "Vary by rules were updated. Headers: {HeaderNames}, Query keys: {QueryKeys}", EventName = "VaryByRulesUpdated")] + internal static partial void VaryByRulesUpdated(this ILogger logger, string headerNames, string queryKeys); [LoggerMessage(13, LogLevel.Information, "The response has been cached.", EventName = "ResponseCached")] internal static partial void ResponseCached(this ILogger logger); diff --git a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs index ab48a0355609..b4a40c21e013 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs @@ -47,7 +47,7 @@ public bool NoStore /// /// Gets or sets the headers to vary by. /// - public string[]? VaryByHeaders { get; init; } + public string[]? VaryByHeaderNames { get; init; } /// /// Gets or sets the value of the cache policy name. diff --git a/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs b/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs index dc8cce4afc15..66e0f148fdcf 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs @@ -80,7 +80,7 @@ public string CreateStorageKey(OutputCacheContext context) } // Vary by headers - var headersCount = varyByRules?.Headers.Count ?? 0; + var headersCount = varyByRules?.HeaderNames.Count ?? 0; if (headersCount > 0) { // Append a group separator for the header segment of the cache key @@ -90,7 +90,7 @@ public string CreateStorageKey(OutputCacheContext context) var requestHeaders = context.HttpContext.Request.Headers; for (var i = 0; i < headersCount; i++) { - var header = varyByRules!.Headers[i] ?? string.Empty; + var header = varyByRules!.HeaderNames[i] ?? string.Empty; var headerValues = requestHeaders[header]; builder.Append(KeyDelimiter) .Append(header) diff --git a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs index a526358e114b..66562f1865c3 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs @@ -341,7 +341,7 @@ internal void CreateCacheKey(OutputCacheContext context) return; } - var varyHeaders = context.CacheVaryByRules.Headers; + var varyHeaders = context.CacheVaryByRules.HeaderNames; var varyQueryKeys = context.CacheVaryByRules.QueryKeys; var varyByCustomKeys = context.CacheVaryByRules.VaryByCustom; var varyByPrefix = context.CacheVaryByRules.VaryByPrefix; @@ -358,7 +358,7 @@ internal void CreateCacheKey(OutputCacheContext context) context.CacheVaryByRules = new CacheVaryByRules { VaryByPrefix = varyByPrefix + normalizedVaryByCustom, - Headers = normalizedVaryHeaders, + HeaderNames = normalizedVaryHeaders, QueryKeys = normalizedVaryQueryKeys }; diff --git a/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs index dd09c2360605..80eea55a6f33 100644 --- a/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs +++ b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs @@ -98,12 +98,12 @@ public OutputCachePolicyBuilder VaryByQuery(params string[] queryKeys) /// /// Adds a policy to vary the cached responses by header. /// - /// The headers to vary the cached responses by. - public OutputCachePolicyBuilder VaryByHeader(params string[] headers) + /// The headers to vary the cached responses by. + public OutputCachePolicyBuilder VaryByHeader(params string[] headerNames) { - ArgumentNullException.ThrowIfNull(headers); + ArgumentNullException.ThrowIfNull(headerNames); - return AddPolicy(new VaryByHeaderPolicy(headers)); + return AddPolicy(new VaryByHeaderPolicy(headerNames)); } /// diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs index 35ed3ca16a26..e02696dc0e40 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// internal sealed class VaryByHeaderPolicy : IOutputCachePolicy { - private readonly StringValues _headers; + private readonly StringValues _headerNames; /// /// Creates a policy that doesn't vary the cached content based on headers. @@ -26,30 +26,30 @@ public VaryByHeaderPolicy(string header) { ArgumentNullException.ThrowIfNull(header); - _headers = header; + _headerNames = header; } /// /// Creates a policy that varies the cached content based on the specified query string keys. /// - public VaryByHeaderPolicy(params string[] headers) + public VaryByHeaderPolicy(params string[] headerNames) { - ArgumentNullException.ThrowIfNull(headers); + ArgumentNullException.ThrowIfNull(headerNames); - _headers = headers; + _headerNames = headerNames; } /// ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) { // No vary by header? - if (_headers.Count == 0) + if (_headerNames.Count == 0) { - context.CacheVaryByRules.Headers = _headers; + context.CacheVaryByRules.HeaderNames = _headerNames; return ValueTask.CompletedTask; } - context.CacheVaryByRules.Headers = StringValues.Concat(context.CacheVaryByRules.Headers, _headers); + context.CacheVaryByRules.HeaderNames = StringValues.Concat(context.CacheVaryByRules.HeaderNames, _headerNames); return ValueTask.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index fdee93dbf51a..4868d30e1963 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -2,8 +2,8 @@ Microsoft.AspNetCore.Builder.OutputCacheApplicationBuilderExtensions Microsoft.AspNetCore.OutputCaching.CacheVaryByRules Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.CacheVaryByRules() -> void -Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.Headers.get -> Microsoft.Extensions.Primitives.StringValues -Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.Headers.set -> void +Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.HeaderNames.get -> Microsoft.Extensions.Primitives.StringValues +Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.HeaderNames.set -> void Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.QueryKeys.get -> Microsoft.Extensions.Primitives.StringValues Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.QueryKeys.set -> void Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.VaryByCustom.get -> System.Collections.Generic.IDictionary! @@ -27,8 +27,8 @@ Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.NoStore.init -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.OutputCacheAttribute() -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.PolicyName.get -> string? Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.PolicyName.init -> void -Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaders.get -> string![]? -Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaders.init -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaderNames.get -> string![]? +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaderNames.init -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.get -> string![]? Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.init -> void Microsoft.AspNetCore.OutputCaching.OutputCacheContext.EnableOutputCaching.get -> bool @@ -47,7 +47,7 @@ Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Expire(System.TimeSp Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.NoCache() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.OutputCachePolicyBuilder() -> void Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Tag(params string![]! tags) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByHeader(params string![]! headers) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByHeader(params string![]! headerNames) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByQuery(params string![]! queryKeys) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! diff --git a/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs index ddeb485d73a9..a31a18a70a52 100644 --- a/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs @@ -56,7 +56,6 @@ public void OutputCachingKeyProvider_CreateStorageKey_CaseSensitivePath_Preserve [Fact] public void OutputCachingKeyProvider_CreateStorageKey_VaryByRulesIsotNull() { - var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); Assert.NotNull(context.CacheVaryByRules); @@ -84,7 +83,7 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersO context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; context.CacheVaryByRules = new CacheVaryByRules() { - Headers = new string[] { "HeaderA", "HeaderC" } + HeaderNames = new string[] { "HeaderA", "HeaderC" } }; Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=", @@ -100,7 +99,7 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_HeaderValuesAreSorted( context.HttpContext.Request.Headers.Append("HeaderA", "ValueA"); context.CacheVaryByRules = new CacheVaryByRules() { - Headers = new string[] { "HeaderA", "HeaderC" } + HeaderNames = new string[] { "HeaderA", "HeaderC" } }; Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueAValueB{KeyDelimiter}HeaderC=", @@ -204,7 +203,7 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersA context.CacheVaryByRules = new CacheVaryByRules() { VaryByPrefix = Guid.NewGuid().ToString("n"), - Headers = new string[] { "HeaderA", "HeaderC" }, + HeaderNames = new string[] { "HeaderA", "HeaderC" }, QueryKeys = new string[] { "QueryA", "QueryC" } }; diff --git a/src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs b/src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs index 53acf44c2acc..64b4a7846f31 100644 --- a/src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs @@ -165,7 +165,7 @@ public async Task VaryByHeadersPolicy_IsEmpty() await policy.CacheRequestAsync(context, default); - Assert.Empty(context.CacheVaryByRules.Headers); + Assert.Empty(context.CacheVaryByRules.HeaderNames); } [Fact] @@ -178,7 +178,7 @@ public async Task VaryByHeadersPolicy_AddsSingleHeader() await policy.CacheRequestAsync(context, default); - Assert.Equal(header, context.CacheVaryByRules.Headers); + Assert.Equal(header, context.CacheVaryByRules.HeaderNames); } [Fact] @@ -191,7 +191,7 @@ public async Task VaryByHeadersPolicy_AddsMultipleHeaders() await policy.CacheRequestAsync(context, default); - Assert.Equal(headers, context.CacheVaryByRules.Headers); + Assert.Equal(headers, context.CacheVaryByRules.HeaderNames); } [Fact] From f860adff5639cb6749669ee509e8094d92eadd2e Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 8 Aug 2022 16:23:37 -0700 Subject: [PATCH 04/12] Rename Enable to Cache --- src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs index 80eea55a6f33..c5ec3dd24dbc 100644 --- a/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs +++ b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs @@ -209,7 +209,7 @@ public OutputCachePolicyBuilder NoCache() /// /// Enables caching for the current request if not already enabled. /// - public OutputCachePolicyBuilder Enable() + public OutputCachePolicyBuilder Cache() { // If no custom policy is added, the DefaultPolicy is already "enabled". if (_policies.Count != 1 || _policies[0] != DefaultPolicy.Instance) From ca649e491be4539698d670092ff8030e557e8635 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 9 Aug 2022 11:07:19 -0700 Subject: [PATCH 05/12] Add VaryByRouteValue --- .../OutputCaching/src/CacheVaryByRules.cs | 5 + .../OutputCaching/src/LoggerExtensions.cs | 4 +- .../OutputCaching/src/OutputCacheAttribute.cs | 17 +- .../OutputCaching/src/OutputCacheContext.cs | 3 + .../src/OutputCacheKeyProvider.cs | 26 +- .../src/OutputCacheMiddleware.cs | 15 +- .../src/OutputCachePolicyBuilder.cs | 13 +- .../src/Policies/VaryByHeaderPolicy.cs | 4 +- .../src/Policies/VaryByRouteValuePolicy.cs | 68 ++++++ .../OutputCaching/src/PublicAPI.Unshipped.txt | 7 +- .../test/OutputCacheAttributeTests.cs | 145 +++++++++++ .../test/OutputCacheKeyProviderTests.cs | 50 +++- .../test/OutputCachePolicyBuilderTests.cs | 227 ++++++++++++++++++ 13 files changed, 567 insertions(+), 17 deletions(-) create mode 100644 src/Middleware/OutputCaching/src/Policies/VaryByRouteValuePolicy.cs create mode 100644 src/Middleware/OutputCaching/test/OutputCacheAttributeTests.cs create mode 100644 src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs diff --git a/src/Middleware/OutputCaching/src/CacheVaryByRules.cs b/src/Middleware/OutputCaching/src/CacheVaryByRules.cs index 696b46de7d07..b67ac54b4111 100644 --- a/src/Middleware/OutputCaching/src/CacheVaryByRules.cs +++ b/src/Middleware/OutputCaching/src/CacheVaryByRules.cs @@ -20,6 +20,11 @@ public sealed class CacheVaryByRules /// public IDictionary VaryByCustom => _varyByCustom ??= new(); + /// + /// Gets or sets the list of route values to vary by. + /// + public StringValues RouteValues { get; set; } + /// /// Gets or sets the list of headers to vary by. /// diff --git a/src/Middleware/OutputCaching/src/LoggerExtensions.cs b/src/Middleware/OutputCaching/src/LoggerExtensions.cs index 4875f319c619..b542b8d8f767 100644 --- a/src/Middleware/OutputCaching/src/LoggerExtensions.cs +++ b/src/Middleware/OutputCaching/src/LoggerExtensions.cs @@ -50,8 +50,8 @@ internal static partial class LoggerExtensions [LoggerMessage(11, LogLevel.Information, "No cached response available for this request.", EventName = "NoResponseServed")] internal static partial void NoResponseServed(this ILogger logger); - [LoggerMessage(12, LogLevel.Debug, "Vary by rules were updated. Headers: {HeaderNames}, Query keys: {QueryKeys}", EventName = "VaryByRulesUpdated")] - internal static partial void VaryByRulesUpdated(this ILogger logger, string headerNames, string queryKeys); + [LoggerMessage(12, LogLevel.Debug, "Vary by rules were updated. Headers: {HeaderNames}, Query keys: {QueryKeys}, Route values: {RouteValues}", EventName = "VaryByRulesUpdated")] + internal static partial void VaryByRulesUpdated(this ILogger logger, string headerNames, string queryKeys, string routeValues); [LoggerMessage(13, LogLevel.Information, "The response has been cached.", EventName = "ResponseCached")] internal static partial void ResponseCached(this ILogger logger); diff --git a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs index b4a40c21e013..cf5dbd6b961f 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs @@ -45,10 +45,15 @@ public bool NoStore public string[]? VaryByQueryKeys { get; init; } /// - /// Gets or sets the headers to vary by. + /// Gets or sets the header names to vary by. /// public string[]? VaryByHeaderNames { get; init; } + /// + /// Gets or sets the route value names to vary by. + /// + public string[]? VaryByRouteValues { get; init; } + /// /// Gets or sets the value of the cache policy name. /// @@ -78,6 +83,16 @@ internal IOutputCachePolicy BuildPolicy() builder.VaryByQuery(VaryByQueryKeys); } + if (VaryByHeaderNames != null) + { + builder.VaryByHeader(VaryByHeaderNames); + } + + if (VaryByRouteValues != null) + { + builder.VaryByRouteValue(VaryByRouteValues); + } + if (_duration != null) { builder.Expire(TimeSpan.FromSeconds(_duration.Value)); diff --git a/src/Middleware/OutputCaching/src/OutputCacheContext.cs b/src/Middleware/OutputCaching/src/OutputCacheContext.cs index 0b03649de2bf..98b49f8bc52a 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheContext.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheContext.cs @@ -79,7 +79,10 @@ internal OutputCacheContext(HttpContext httpContext, IOutputCacheStore store, Ou internal Stream OriginalResponseStream { get; set; } = default!; internal OutputCacheStream OutputCacheStream { get; set; } = default!; + internal ILogger Logger { get; } + internal OutputCacheOptions Options { get; } + internal IOutputCacheStore Store { get; } } diff --git a/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs b/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs index 66e0f148fdcf..fe57daae8c8b 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Globalization; using System.Linq; using System.Text; using Microsoft.Extensions.ObjectPool; @@ -28,7 +29,7 @@ internal OutputCacheKeyProvider(ObjectPoolProvider poolProvider, IOptionsSCHEMEHOST:PORT/PATHBASE/PATHHHeaderName=HeaderValueQQueryName=QueryValue1QueryValue2 + // GETSCHEMEHOST:PORT/PATHBASE/PATHHHeaderName=HeaderValueQQueryName=QueryValue1QueryValue2RRouteName1=RouteValue1RouteName2=RouteValue2 public string CreateStorageKey(OutputCacheContext context) { ArgumentNullException.ThrowIfNull(_builderPool); @@ -166,6 +167,29 @@ public string CreateStorageKey(OutputCacheContext context) } } + // Vary by route values + var routeValuesCount = varyByRules?.RouteValues.Count ?? 0; + if (routeValuesCount > 0) + { + // Append a group separator for the query key segment of the cache key + builder.Append(KeyDelimiter) + .Append('R'); + + for (var i = 0; i < routeValuesCount; i++) + { + // The lookup key can't be null + var routeValueName = varyByRules!.RouteValues[i] ?? string.Empty; + + // RouteValues returns null if the key doesn't exist + var routeValueValue = context.HttpContext.Request.RouteValues[routeValueName]; + + builder.Append(KeyDelimiter) + .Append(routeValueName) + .Append('=') + .Append(Convert.ToString(routeValueValue, CultureInfo.InvariantCulture)); + } + } + return builder.ToString(); } finally diff --git a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs index 66562f1865c3..5fdb179155b6 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs @@ -341,16 +341,18 @@ internal void CreateCacheKey(OutputCacheContext context) return; } - var varyHeaders = context.CacheVaryByRules.HeaderNames; + var varyHeaderNames = context.CacheVaryByRules.HeaderNames; + var varyRouteValues = context.CacheVaryByRules.RouteValues; var varyQueryKeys = context.CacheVaryByRules.QueryKeys; - var varyByCustomKeys = context.CacheVaryByRules.VaryByCustom; + var varyByCustomKeys = context.CacheVaryByRules.HasVaryByCustom ? context.CacheVaryByRules.VaryByCustom : null; var varyByPrefix = context.CacheVaryByRules.VaryByPrefix; // Check if any vary rules exist - if (!StringValues.IsNullOrEmpty(varyHeaders) || !StringValues.IsNullOrEmpty(varyQueryKeys) || !StringValues.IsNullOrEmpty(varyByPrefix) || varyByCustomKeys?.Count > 0) + if (!StringValues.IsNullOrEmpty(varyHeaderNames) || !StringValues.IsNullOrEmpty(varyRouteValues) || !StringValues.IsNullOrEmpty(varyQueryKeys) || !StringValues.IsNullOrEmpty(varyByPrefix) || varyByCustomKeys?.Count > 0) { // Normalize order and casing of vary by rules - var normalizedVaryHeaders = GetOrderCasingNormalizedStringValues(varyHeaders); + var normalizedVaryHeaderNames = GetOrderCasingNormalizedStringValues(varyHeaderNames); + var normalizedVaryRouteValues = GetOrderCasingNormalizedStringValues(varyRouteValues); var normalizedVaryQueryKeys = GetOrderCasingNormalizedStringValues(varyQueryKeys); var normalizedVaryByCustom = GetOrderCasingNormalizedDictionary(varyByCustomKeys); @@ -358,7 +360,8 @@ internal void CreateCacheKey(OutputCacheContext context) context.CacheVaryByRules = new CacheVaryByRules { VaryByPrefix = varyByPrefix + normalizedVaryByCustom, - HeaderNames = normalizedVaryHeaders, + HeaderNames = normalizedVaryHeaderNames, + RouteValues = normalizedVaryRouteValues, QueryKeys = normalizedVaryQueryKeys }; @@ -366,7 +369,7 @@ internal void CreateCacheKey(OutputCacheContext context) // Always overwrite the CachedVaryByRules to update the expiry information if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.VaryByRulesUpdated(normalizedVaryHeaders.ToString(), normalizedVaryQueryKeys.ToString()); + _logger.VaryByRulesUpdated(normalizedVaryHeaderNames.ToString(), normalizedVaryQueryKeys.ToString(), normalizedVaryRouteValues.ToString()); } } diff --git a/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs index c5ec3dd24dbc..b386083e912b 100644 --- a/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs +++ b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs @@ -98,7 +98,7 @@ public OutputCachePolicyBuilder VaryByQuery(params string[] queryKeys) /// /// Adds a policy to vary the cached responses by header. /// - /// The headers to vary the cached responses by. + /// The header names to vary the cached responses by. public OutputCachePolicyBuilder VaryByHeader(params string[] headerNames) { ArgumentNullException.ThrowIfNull(headerNames); @@ -106,6 +106,17 @@ public OutputCachePolicyBuilder VaryByHeader(params string[] headerNames) return AddPolicy(new VaryByHeaderPolicy(headerNames)); } + /// + /// Adds a policy to vary the cached responses by route value. + /// + /// The route value names to vary the cached responses by. + public OutputCachePolicyBuilder VaryByRouteValue(params string[] routeValueNames) + { + ArgumentNullException.ThrowIfNull(routeValueNames); + + return AddPolicy(new VaryByRouteValuePolicy(routeValueNames)); + } + /// /// Adds a policy to vary the cached responses by custom values. /// diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs index e02696dc0e40..a1109366787e 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs @@ -20,7 +20,7 @@ public VaryByHeaderPolicy() } /// - /// Creates a policy that varies the cached content based on the specified header. + /// Creates a policy that varies the cached content based on the specified header name. /// public VaryByHeaderPolicy(string header) { @@ -30,7 +30,7 @@ public VaryByHeaderPolicy(string header) } /// - /// Creates a policy that varies the cached content based on the specified query string keys. + /// Creates a policy that varies the cached content based on the specified header names. /// public VaryByHeaderPolicy(params string[] headerNames) { diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByRouteValuePolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByRouteValuePolicy.cs new file mode 100644 index 000000000000..bf15d23b3b18 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/VaryByRouteValuePolicy.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// When applied, the cached content will be different for every value of the provided route values. +/// +internal sealed class VaryByRouteValuePolicy : IOutputCachePolicy +{ + private readonly StringValues _routeValueNames; + + /// + /// Creates a policy that doesn't vary the cached content based on route values. + /// + public VaryByRouteValuePolicy() + { + } + + /// + /// Creates a policy that varies the cached content based on the specified route value name. + /// + public VaryByRouteValuePolicy(string routeValue) + { + ArgumentNullException.ThrowIfNull(routeValue); + + _routeValueNames = routeValue; + } + + /// + /// Creates a policy that varies the cached content based on the specified route value names. + /// + public VaryByRouteValuePolicy(params string[] routeValueNames) + { + ArgumentNullException.ThrowIfNull(routeValueNames); + + _routeValueNames = routeValueNames; + } + + /// + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) + { + // No vary by route value? + if (_routeValueNames.Count == 0) + { + context.CacheVaryByRules.RouteValues = _routeValueNames; + return ValueTask.CompletedTask; + } + + context.CacheVaryByRules.RouteValues = StringValues.Concat(context.CacheVaryByRules.RouteValues, _routeValueNames); + + return ValueTask.CompletedTask; + } + + /// + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } + + /// + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } +} diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index 4868d30e1963..2973409993d5 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -6,6 +6,8 @@ Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.HeaderNames.get -> Microsoft Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.HeaderNames.set -> void Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.QueryKeys.get -> Microsoft.Extensions.Primitives.StringValues Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.QueryKeys.set -> void +Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.RouteValues.get -> Microsoft.Extensions.Primitives.StringValues +Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.RouteValues.set -> void Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.VaryByCustom.get -> System.Collections.Generic.IDictionary! Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.VaryByPrefix.get -> Microsoft.Extensions.Primitives.StringValues Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.VaryByPrefix.set -> void @@ -31,6 +33,8 @@ Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaderNames.get -> Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaderNames.init -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.get -> string![]? Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.init -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByRouteValues.get -> string![]? +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByRouteValues.init -> void Microsoft.AspNetCore.OutputCaching.OutputCacheContext.EnableOutputCaching.get -> bool Microsoft.AspNetCore.OutputCaching.OutputCacheContext.EnableOutputCaching.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.AddBasePolicy(Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy! policy) -> void @@ -41,14 +45,15 @@ Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.AddPolicy(System.Type! policyType) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.AddPolicy() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.AllowLocking(bool lockResponse = true) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Cache() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Clear() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Enable() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Expire(System.TimeSpan expiration) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.NoCache() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.OutputCachePolicyBuilder() -> void Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Tag(params string![]! tags) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByHeader(params string![]! headerNames) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByQuery(params string![]! queryKeys) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByRouteValue(params string![]! routeValueNames) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! diff --git a/src/Middleware/OutputCaching/test/OutputCacheAttributeTests.cs b/src/Middleware/OutputCaching/test/OutputCacheAttributeTests.cs new file mode 100644 index 000000000000..5f56000e6509 --- /dev/null +++ b/src/Middleware/OutputCaching/test/OutputCacheAttributeTests.cs @@ -0,0 +1,145 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Castle.Core.Internal; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.OutputCaching.Tests; + +public class OutputCacheAttributeTests +{ + [Fact] + public void Attribute_CreatesDefaultPolicy() + { + var cache = new TestOutputCache(); + var context = TestUtils.CreateTestContext(cache); + + var attribute = OutputCacheMethods.GetAttribute(nameof(OutputCacheMethods.Default)); + var policy = attribute.BuildPolicy(); + + Assert.Equal(DefaultPolicy.Instance, policy); + } + + [Fact] + public async Task Attribute_CreatesExpirePolicy() + { + var cache = new TestOutputCache(); + var context = TestUtils.CreateTestContext(cache); + + var attribute = OutputCacheMethods.GetAttribute(nameof(OutputCacheMethods.Duration)); + await attribute.BuildPolicy().CacheRequestAsync(context, cancellation: default); + + Assert.True(context.EnableOutputCaching); + Assert.Equal(42, context.ResponseExpirationTimeSpan?.TotalSeconds); + } + + [Fact] + public async Task Attribute_CreatesNoStorePolicy() + { + var cache = new TestOutputCache(); + var context = TestUtils.CreateTestContext(cache); + + var attribute = OutputCacheMethods.GetAttribute(nameof(OutputCacheMethods.NoStore)); + await attribute.BuildPolicy().CacheRequestAsync(context, cancellation: default); + + Assert.False(context.EnableOutputCaching); + } + + [Fact] + public async Task Attribute_CreatesNamedPolicy() + { + var cache = new TestOutputCache(); + var options = new OutputCacheOptions(); + options.AddPolicy("MyPolicy", b => b.Expire(TimeSpan.FromSeconds(42))); + + var context = TestUtils.CreateTestContext(cache, options); + + var attribute = OutputCacheMethods.GetAttribute(nameof(OutputCacheMethods.PolicyName)); + await attribute.BuildPolicy().CacheRequestAsync(context, cancellation: default); + + Assert.True(context.EnableOutputCaching); + Assert.Equal(42, context.ResponseExpirationTimeSpan?.TotalSeconds); + } + + [Fact] + public async Task Attribute_CreatesVaryByHeaderPolicy() + { + var cache = new TestOutputCache(); + var context = TestUtils.CreateTestContext(cache); + context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; + context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; + + var attribute = OutputCacheMethods.GetAttribute(nameof(OutputCacheMethods.VaryByHeaderNames)); + await attribute.BuildPolicy().CacheRequestAsync(context, cancellation: default); + + Assert.True(context.EnableOutputCaching); + Assert.Contains("HeaderA", (IEnumerable)context.CacheVaryByRules.HeaderNames); + Assert.Contains("HeaderC", (IEnumerable)context.CacheVaryByRules.HeaderNames); + Assert.DoesNotContain("HeaderB", (IEnumerable)context.CacheVaryByRules.HeaderNames); + } + + [Fact] + public async Task Attribute_CreatesVaryByQueryPolicy() + { + var cache = new TestOutputCache(); + var context = TestUtils.CreateTestContext(cache); + context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); + + var attribute = OutputCacheMethods.GetAttribute(nameof(OutputCacheMethods.VaryByQueryKeys)); + await attribute.BuildPolicy().CacheRequestAsync(context, cancellation: default); + + Assert.True(context.EnableOutputCaching); + Assert.Contains("QueryA", (IEnumerable)context.CacheVaryByRules.QueryKeys); + Assert.Contains("QueryC", (IEnumerable)context.CacheVaryByRules.QueryKeys); + Assert.DoesNotContain("QueryB", (IEnumerable)context.CacheVaryByRules.QueryKeys); + } + + [Fact] + public async Task Attribute_CreatesVaryByRoutePolicy() + { + var cache = new TestOutputCache(); + var context = TestUtils.CreateTestContext(cache); + context.HttpContext.Request.RouteValues = new Routing.RouteValueDictionary() + { + ["RouteA"] = "ValueA", + ["RouteB"] = 123.456, + }; + + var attribute = OutputCacheMethods.GetAttribute(nameof(OutputCacheMethods.VaryByRouteValues)); + await attribute.BuildPolicy().CacheRequestAsync(context, cancellation: default); + + Assert.True(context.EnableOutputCaching); + Assert.Contains("RouteA", (IEnumerable)context.CacheVaryByRules.RouteValues); + Assert.Contains("RouteC", (IEnumerable)context.CacheVaryByRules.RouteValues); + Assert.DoesNotContain("RouteB", (IEnumerable)context.CacheVaryByRules.RouteValues); + } + + private class OutputCacheMethods + { + public static OutputCacheAttribute GetAttribute(string methodName) + { + return typeof(OutputCacheMethods).GetMethod(methodName, System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public).GetAttribute(); + } + + [OutputCache()] + public static void Default() { } + + [OutputCache(Duration = 42)] + public static void Duration() { } + + [OutputCache(NoStore = true)] + public static void NoStore() { } + + [OutputCache(PolicyName = "MyPolicy")] + public static void PolicyName() { } + + [OutputCache(VaryByHeaderNames = new[] { "HeaderA", "HeaderC" })] + public static void VaryByHeaderNames() { } + + [OutputCache(VaryByQueryKeys = new[] { "QueryA", "QueryC" })] + public static void VaryByQueryKeys() { } + + [OutputCache(VaryByRouteValues = new[] { "RouteA", "RouteC" })] + public static void VaryByRouteValues() { } + } +} diff --git a/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs index a31a18a70a52..02e03e737b4c 100644 --- a/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Globalization; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.OutputCaching.Tests; @@ -74,6 +75,46 @@ public void OutputCachingKeyProvider_CreateStorageKey_ReturnsCachedVaryByGuid_If Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}", cacheKeyProvider.CreateStorageKey(context)); } + [Fact] + public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedRouteValuesOnly() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.RouteValues["RouteA"] = "ValueA"; + context.HttpContext.Request.RouteValues["RouteB"] = "ValueB"; + context.CacheVaryByRules = new CacheVaryByRules() + { + RouteValues = new string[] { "RouteA", "RouteC" } + }; + + Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}R{KeyDelimiter}RouteA=ValueA{KeyDelimiter}RouteC=", + cacheKeyProvider.CreateStorageKey(context)); + } + + [Fact] + public void OutputCachingKeyProvider_CreateStorageVaryKey_SerializeRouteValueToStringInvariantCulture() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.RouteValues["RouteA"] = 123.456; + context.CacheVaryByRules = new CacheVaryByRules() + { + RouteValues = new string[] { "RouteA", "RouteC" } + }; + + var culture = Thread.CurrentThread.CurrentCulture; + try + { + Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("fr-FR"); + Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}R{KeyDelimiter}RouteA=123.456{KeyDelimiter}RouteC=", + cacheKeyProvider.CreateStorageKey(context)); + } + finally + { + Thread.CurrentThread.CurrentCulture = culture; + } + } + [Fact] public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly() { @@ -193,21 +234,24 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesAreSort } [Fact] - public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndQueryKeys() + public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndQueryKeysAndRouteValues() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); + context.HttpContext.Request.RouteValues["RouteA"] = "ValueA"; + context.HttpContext.Request.RouteValues["RouteB"] = "ValueB"; context.CacheVaryByRules = new CacheVaryByRules() { VaryByPrefix = Guid.NewGuid().ToString("n"), HeaderNames = new string[] { "HeaderA", "HeaderC" }, - QueryKeys = new string[] { "QueryA", "QueryC" } + QueryKeys = new string[] { "QueryA", "QueryC" }, + RouteValues = new string[] { "RouteA", "RouteC" }, }; - Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", + Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC={KeyDelimiter}R{KeyDelimiter}RouteA=ValueA{KeyDelimiter}RouteC=", cacheKeyProvider.CreateStorageKey(context)); } } diff --git a/src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs b/src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs new file mode 100644 index 000000000000..057410b5e04f --- /dev/null +++ b/src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs @@ -0,0 +1,227 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.OutputCaching.Tests; + +public class OutputCachePolicyBuilderTests +{ + [Fact] + public void BuildPolicy_CreatesDefaultPolicy() + { + var builder = new OutputCachePolicyBuilder(); + var policy = builder.Build(); + + Assert.Equal(DefaultPolicy.Instance, policy); + } + + [Fact] + public async Task BuildPolicy_CreatesExpirePolicy() + { + var context = CreateTestContext(); + var duration = 42; + + var builder = new OutputCachePolicyBuilder(); + var policy = builder.Expire(TimeSpan.FromSeconds(duration)).Build(); + await policy.CacheRequestAsync(context, cancellation: default); + + Assert.True(context.EnableOutputCaching); + Assert.Equal(duration, context.ResponseExpirationTimeSpan?.TotalSeconds); + } + + [Fact] + public async Task BuildPolicy_CreatesNoStorePolicy() + { + var context = CreateTestContext(); + + var builder = new OutputCachePolicyBuilder(); + var policy = builder.NoCache().Build(); + await policy.CacheRequestAsync(context, cancellation: default); + + Assert.False(context.EnableOutputCaching); + } + + [Fact] + public async Task BuildPolicy_AddsCustomPolicy() + { + var options = new OutputCacheOptions(); + var name = "MyPolicy"; + var duration = 42; + options.AddPolicy(name, b => b.Expire(TimeSpan.FromSeconds(duration))); + + var context = CreateTestContext(options: options); + + var builder = new OutputCachePolicyBuilder(); + var policy = builder.AddPolicy(new NamedPolicy(name)).Build(); + await policy.CacheRequestAsync(context, cancellation: default); + + Assert.True(context.EnableOutputCaching); + Assert.Equal(duration, context.ResponseExpirationTimeSpan?.TotalSeconds); + } + + [Fact] + public async Task BuildPolicy_CreatesVaryByHeaderPolicy() + { + var context = CreateTestContext(); + context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; + context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; + + var builder = new OutputCachePolicyBuilder(); + var policy = builder.VaryByHeader("HeaderA", "HeaderC").Build(); + await policy.CacheRequestAsync(context, cancellation: default); + + Assert.True(context.EnableOutputCaching); + Assert.Contains("HeaderA", (IEnumerable)context.CacheVaryByRules.HeaderNames); + Assert.Contains("HeaderC", (IEnumerable)context.CacheVaryByRules.HeaderNames); + Assert.DoesNotContain("HeaderB", (IEnumerable)context.CacheVaryByRules.HeaderNames); + } + + [Fact] + public async Task BuildPolicy_CreatesVaryByQueryPolicy() + { + var context = CreateTestContext(); + context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); + + var builder = new OutputCachePolicyBuilder(); + var policy = builder.VaryByQuery("QueryA", "QueryC").Build(); + await policy.CacheRequestAsync(context, cancellation: default); + + Assert.True(context.EnableOutputCaching); + Assert.Contains("QueryA", (IEnumerable)context.CacheVaryByRules.QueryKeys); + Assert.Contains("QueryC", (IEnumerable)context.CacheVaryByRules.QueryKeys); + Assert.DoesNotContain("QueryB", (IEnumerable)context.CacheVaryByRules.QueryKeys); + } + + [Fact] + public async Task BuildPolicy_CreatesVaryByRoutePolicy() + { + var context = CreateTestContext(); + context.HttpContext.Request.RouteValues = new Routing.RouteValueDictionary() + { + ["RouteA"] = "ValueA", + ["RouteB"] = 123.456, + }; + + var builder = new OutputCachePolicyBuilder(); + var policy = builder.VaryByRouteValue("RouteA", "RouteC").Build(); + await policy.CacheRequestAsync(context, cancellation: default); + + Assert.True(context.EnableOutputCaching); + Assert.Contains("RouteA", (IEnumerable)context.CacheVaryByRules.RouteValues); + Assert.Contains("RouteC", (IEnumerable)context.CacheVaryByRules.RouteValues); + Assert.DoesNotContain("RouteB", (IEnumerable)context.CacheVaryByRules.RouteValues); + } + + [Fact] + public async Task BuildPolicy_CreatesVaryByValuePolicy() + { + var context = CreateTestContext(); + + var builder = new OutputCachePolicyBuilder(); + var policy = builder.VaryByValue(context => new KeyValuePair("color", "blue")).Build(); + await policy.CacheRequestAsync(context, cancellation: default); + + Assert.True(context.EnableOutputCaching); + Assert.Equal("blue", context.CacheVaryByRules.VaryByCustom["color"]); + } + + [Fact] + public async Task BuildPolicy_CreatesTagPolicy() + { + var context = CreateTestContext(); + + var builder = new OutputCachePolicyBuilder(); + var policy = builder.Tag("tag1", "tag2").Build(); + await policy.CacheRequestAsync(context, cancellation: default); + + Assert.True(context.EnableOutputCaching); + Assert.Contains("tag1", context.Tags); + Assert.Contains("tag2", context.Tags); + } + + [Fact] + public async Task BuildPolicy_AllowsLocking() + { + var context = CreateTestContext(); + + var builder = new OutputCachePolicyBuilder(); + var policy = builder.Build(); + await policy.CacheRequestAsync(context, cancellation: default); + + Assert.True(context.AllowLocking); + } + + [Fact] + public async Task BuildPolicy_EnablesLocking() + { + var cache = new TestOutputCache(); + var context = TestUtils.CreateTestContext(cache); + + var builder = new OutputCachePolicyBuilder(); + var policy = builder.AllowLocking(true).Build(); + await policy.CacheRequestAsync(context, cancellation: default); + + Assert.True(context.AllowLocking); + } + + [Fact] + public async Task BuildPolicy_DisablesLocking() + { + var context = CreateTestContext(); + + var builder = new OutputCachePolicyBuilder(); + var policy = builder.AllowLocking(false).Build(); + await policy.CacheRequestAsync(context, cancellation: default); + + Assert.False(context.AllowLocking); + } + + [Fact] + public async Task BuildPolicy_ClearsDefaultPolicy() + { + var context = CreateTestContext(); + + var builder = new OutputCachePolicyBuilder(); + var policy = builder.Clear().Build(); + await policy.CacheRequestAsync(context, cancellation: default); + + Assert.False(context.AllowLocking); + Assert.False(context.AllowCacheLookup); + Assert.False(context.AllowCacheStorage); + Assert.False(context.EnableOutputCaching); + } + + [Fact] + public async Task BuildPolicy_DisablesCache() + { + var context = CreateTestContext(); + + var builder = new OutputCachePolicyBuilder(); + var policy = builder.NoCache().Build(); + await policy.CacheRequestAsync(context, cancellation: default); + + Assert.False(context.EnableOutputCaching); + } + + [Fact] + public async Task BuildPolicy_EnablesCache() + { + var context = CreateTestContext(); + + var builder = new OutputCachePolicyBuilder(); + var policy = builder.NoCache().Cache().Build(); + await policy.CacheRequestAsync(context, cancellation: default); + + Assert.True(context.EnableOutputCaching); + } + + private static OutputCacheContext CreateTestContext(IOutputCacheStore? cache = null, OutputCacheOptions? options = null) + { + return new OutputCacheContext(new DefaultHttpContext(), cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCacheOptions()).Value, NullLogger.Instance) + { + }; + } +} From 5c81ab1005708dfe532d73e6b8ef71670c7f3c3a Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 9 Aug 2022 14:33:19 -0700 Subject: [PATCH 06/12] Adding OutputCacheContext default constructor --- .../OutputCaching/src/LoggerExtensions.cs | 39 +++------ .../OutputCaching/src/OutputCacheContext.cs | 19 +---- .../src/OutputCacheMiddleware.cs | 14 ++-- .../src/Policies/DefaultPolicy.cs | 4 - .../OutputCaching/src/Policies/NamedPolicy.cs | 15 +++- .../OutputCaching/src/Policies/TypedPolicy.cs | 4 +- .../OutputCaching/src/PublicAPI.Unshipped.txt | 3 + .../test/OutputCacheAttributeTests.cs | 21 ++--- .../test/OutputCacheMiddlewareTests.cs | 79 +++++++++++-------- .../test/OutputCachePoliciesTests.cs | 7 +- .../test/OutputCachePolicyBuilderTests.cs | 38 ++++----- .../test/OutputCachePolicyProviderTests.cs | 47 ++++------- .../OutputCaching/test/TestUtils.cs | 77 ++++++++---------- 13 files changed, 165 insertions(+), 202 deletions(-) diff --git a/src/Middleware/OutputCaching/src/LoggerExtensions.cs b/src/Middleware/OutputCaching/src/LoggerExtensions.cs index b542b8d8f767..6a4749383193 100644 --- a/src/Middleware/OutputCaching/src/LoggerExtensions.cs +++ b/src/Middleware/OutputCaching/src/LoggerExtensions.cs @@ -11,59 +11,44 @@ namespace Microsoft.AspNetCore.OutputCaching; /// internal static partial class LoggerExtensions { - [LoggerMessage(1, LogLevel.Debug, "The request cannot be served from cache because it uses the HTTP method: {Method}.", - EventName = "RequestMethodNotCacheable")] - internal static partial void RequestMethodNotCacheable(this ILogger logger, string method); - - [LoggerMessage(2, LogLevel.Debug, "The request cannot be served from cache because it contains an 'Authorization' header.", - EventName = "RequestWithAuthorizationNotCacheable")] - internal static partial void RequestWithAuthorizationNotCacheable(this ILogger logger); - - [LoggerMessage(3, LogLevel.Debug, "Response is not cacheable because it contains a 'SetCookie' header.", EventName = "ResponseWithSetCookieNotCacheable")] - internal static partial void ResponseWithSetCookieNotCacheable(this ILogger logger); - - [LoggerMessage(4, LogLevel.Debug, "Response is not cacheable because its status code {StatusCode} does not indicate success.", - EventName = "ResponseWithUnsuccessfulStatusCodeNotCacheable")] - internal static partial void ResponseWithUnsuccessfulStatusCodeNotCacheable(this ILogger logger, int statusCode); - - [LoggerMessage(5, LogLevel.Debug, "The 'IfNoneMatch' header of the request contains a value of *.", EventName = "NotModifiedIfNoneMatchStar")] + [LoggerMessage(1, LogLevel.Debug, "The 'IfNoneMatch' header of the request contains a value of *.", EventName = "NotModifiedIfNoneMatchStar")] internal static partial void NotModifiedIfNoneMatchStar(this ILogger logger); - [LoggerMessage(6, LogLevel.Debug, "The ETag {ETag} in the 'IfNoneMatch' header matched the ETag of a cached entry.", + [LoggerMessage(2, LogLevel.Debug, "The ETag {ETag} in the 'IfNoneMatch' header matched the ETag of a cached entry.", EventName = "NotModifiedIfNoneMatchMatched")] internal static partial void NotModifiedIfNoneMatchMatched(this ILogger logger, EntityTagHeaderValue etag); - [LoggerMessage(7, LogLevel.Debug, "The last modified date of {LastModified} is before the date {IfModifiedSince} specified in the 'IfModifiedSince' header.", + [LoggerMessage(3, LogLevel.Debug, "The last modified date of {LastModified} is before the date {IfModifiedSince} specified in the 'IfModifiedSince' header.", EventName = "NotModifiedIfModifiedSinceSatisfied")] internal static partial void NotModifiedIfModifiedSinceSatisfied(this ILogger logger, DateTimeOffset lastModified, DateTimeOffset ifModifiedSince); - [LoggerMessage(8, LogLevel.Information, "The content requested has not been modified.", EventName = "NotModifiedServed")] + [LoggerMessage(4, LogLevel.Information, "The content requested has not been modified.", EventName = "NotModifiedServed")] internal static partial void NotModifiedServed(this ILogger logger); - [LoggerMessage(9, LogLevel.Information, "Serving response from cache.", EventName = "CachedResponseServed")] + [LoggerMessage(5, LogLevel.Information, "Serving response from cache.", EventName = "CachedResponseServed")] internal static partial void CachedResponseServed(this ILogger logger); - [LoggerMessage(10, LogLevel.Information, "No cached response available for this request and the 'only-if-cached' cache directive was specified.", + [LoggerMessage(6, LogLevel.Information, "No cached response available for this request and the 'only-if-cached' cache directive was specified.", EventName = "GatewayTimeoutServed")] internal static partial void GatewayTimeoutServed(this ILogger logger); - [LoggerMessage(11, LogLevel.Information, "No cached response available for this request.", EventName = "NoResponseServed")] + [LoggerMessage(7, LogLevel.Information, "No cached response available for this request.", EventName = "NoResponseServed")] internal static partial void NoResponseServed(this ILogger logger); - [LoggerMessage(12, LogLevel.Debug, "Vary by rules were updated. Headers: {HeaderNames}, Query keys: {QueryKeys}, Route values: {RouteValues}", EventName = "VaryByRulesUpdated")] + [LoggerMessage(8, LogLevel.Debug, "Vary by rules were updated. Headers: {HeaderNames}, Query keys: {QueryKeys}, Route values: {RouteValues}", EventName = "VaryByRulesUpdated")] internal static partial void VaryByRulesUpdated(this ILogger logger, string headerNames, string queryKeys, string routeValues); - [LoggerMessage(13, LogLevel.Information, "The response has been cached.", EventName = "ResponseCached")] + [LoggerMessage(9, LogLevel.Information, "The response has been cached.", EventName = "ResponseCached")] internal static partial void ResponseCached(this ILogger logger); - [LoggerMessage(14, LogLevel.Information, "The response could not be cached for this request.", EventName = "ResponseNotCached")] + [LoggerMessage(10, LogLevel.Information, "The response could not be cached for this request.", EventName = "ResponseNotCached")] internal static partial void ResponseNotCached(this ILogger logger); - [LoggerMessage(15, LogLevel.Warning, "The response could not be cached for this request because the 'Content-Length' did not match the body length.", + [LoggerMessage(11, LogLevel.Warning, "The response could not be cached for this request because the 'Content-Length' did not match the body length.", EventName = "ResponseContentLengthMismatchNotCached")] internal static partial void ResponseContentLengthMismatchNotCached(this ILogger logger); - [LoggerMessage(16, LogLevel.Debug, "The response time of the entry is {ResponseTime} and has exceeded its expiry date.", + [LoggerMessage(12, LogLevel.Debug, "The response time of the entry is {ResponseTime} and has exceeded its expiry date.", EventName = "ExpirationExpiresExceeded")] internal static partial void ExpirationExpiresExceeded(this ILogger logger, DateTimeOffset responseTime); diff --git a/src/Middleware/OutputCaching/src/OutputCacheContext.cs b/src/Middleware/OutputCaching/src/OutputCacheContext.cs index 98b49f8bc52a..ef264a476709 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheContext.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheContext.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.OutputCaching; @@ -11,12 +10,8 @@ namespace Microsoft.AspNetCore.OutputCaching; /// public sealed class OutputCacheContext { - internal OutputCacheContext(HttpContext httpContext, IOutputCacheStore store, OutputCacheOptions options, ILogger logger) + public OutputCacheContext() { - HttpContext = httpContext; - Logger = logger; - Store = store; - Options = options; } /// @@ -42,12 +37,12 @@ internal OutputCacheContext(HttpContext httpContext, IOutputCacheStore store, Ou /// /// Gets the . /// - public HttpContext HttpContext { get; } + public required HttpContext HttpContext { get; init; } /// - /// Gets the response time. + /// Gets or sets the response time. /// - public DateTimeOffset? ResponseTime { get; internal set; } + public DateTimeOffset? ResponseTime { get; set; } /// /// Gets the instance. @@ -79,10 +74,4 @@ internal OutputCacheContext(HttpContext httpContext, IOutputCacheStore store, Ou internal Stream OriginalResponseStream { get; set; } = default!; internal OutputCacheStream OutputCacheStream { get; set; } = default!; - - internal ILogger Logger { get; } - - internal OutputCacheOptions Options { get; } - - internal IOutputCacheStore Store { get; } } diff --git a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs index 5fdb179155b6..bf02f6e09e00 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs @@ -92,7 +92,7 @@ public Task Invoke(HttpContext httpContext) private async Task InvokeAwaited(HttpContext httpContext, IReadOnlyList policies) { - var context = new OutputCacheContext(httpContext, _store, _options, _logger); + var context = new OutputCacheContext { HttpContext = httpContext }; // Add IOutputCacheFeature AddOutputCacheFeature(context); @@ -247,7 +247,7 @@ internal async Task TryServeCachedResponseAsync(OutputCacheContext context // Validate expiration if (context.CachedEntryAge <= TimeSpan.Zero) { - context.Logger.ExpirationExpiresExceeded(context.ResponseTime!.Value); + _logger.ExpirationExpiresExceeded(context.ResponseTime!.Value); context.IsCacheEntryFresh = false; } @@ -316,7 +316,7 @@ internal async Task TryServeFromCacheAsync(OutputCacheContext cacheContext // TODO: should it be part of the cache implementations or can we assume all caches would benefit from it? // It makes sense for caches that use IO (disk, network) or need to deserialize the state but could also be a global option - var cacheEntry = await _outputCacheEntryDispatcher.ScheduleAsync(cacheContext.CacheKey, cacheContext, static async (key, cacheContext) => await OutputCacheEntryFormatter.GetAsync(key, cacheContext.Store, cacheContext.HttpContext.RequestAborted)); + var cacheEntry = await _outputCacheEntryDispatcher.ScheduleAsync(cacheContext.CacheKey, (Store: _store, CacheContext: cacheContext), static async (key, state) => await OutputCacheEntryFormatter.GetAsync(key, state.Store, state.CacheContext.HttpContext.RequestAborted)); if (await TryServeCachedResponseAsync(cacheContext, cacheEntry, policies)) { @@ -519,7 +519,7 @@ internal static void UnshimResponseStream(OutputCacheContext context) RemoveOutputCacheFeature(context.HttpContext); } - internal static bool ContentIsNotModified(OutputCacheContext context) + internal bool ContentIsNotModified(OutputCacheContext context) { var cachedResponseHeaders = context.CachedResponse.Headers; var ifNoneMatchHeader = context.HttpContext.Request.Headers.IfNoneMatch; @@ -528,7 +528,7 @@ internal static bool ContentIsNotModified(OutputCacheContext context) { if (ifNoneMatchHeader.Count == 1 && StringSegment.Equals(ifNoneMatchHeader[0], EntityTagHeaderValue.Any.Tag, StringComparison.OrdinalIgnoreCase)) { - context.Logger.NotModifiedIfNoneMatchStar(); + _logger.NotModifiedIfNoneMatchStar(); return true; } @@ -541,7 +541,7 @@ internal static bool ContentIsNotModified(OutputCacheContext context) var requestETag = ifNoneMatchEtags[i]; if (eTag.Compare(requestETag, useStrongComparison: false)) { - context.Logger.NotModifiedIfNoneMatchMatched(requestETag); + _logger.NotModifiedIfNoneMatchMatched(requestETag); return true; } } @@ -561,7 +561,7 @@ internal static bool ContentIsNotModified(OutputCacheContext context) if (HeaderUtilities.TryParseDate(ifModifiedSince.ToString(), out var modifiedSince) && modified <= modifiedSince) { - context.Logger.NotModifiedIfModifiedSinceSatisfied(modified, modifiedSince); + _logger.NotModifiedIfModifiedSinceSatisfied(modified, modifiedSince); return true; } } diff --git a/src/Middleware/OutputCaching/src/Policies/DefaultPolicy.cs b/src/Middleware/OutputCaching/src/Policies/DefaultPolicy.cs index a9c0c461b5d3..cc9ea67ec2dd 100644 --- a/src/Middleware/OutputCaching/src/Policies/DefaultPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/DefaultPolicy.cs @@ -46,7 +46,6 @@ ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, Canc // Verify existence of cookie headers if (!StringValues.IsNullOrEmpty(response.Headers.SetCookie)) { - context.Logger.ResponseWithSetCookieNotCacheable(); context.AllowCacheStorage = false; return ValueTask.CompletedTask; } @@ -54,7 +53,6 @@ ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, Canc // Check response code if (response.StatusCode != StatusCodes.Status200OK) { - context.Logger.ResponseWithUnsuccessfulStatusCodeNotCacheable(response.StatusCode); context.AllowCacheStorage = false; return ValueTask.CompletedTask; } @@ -71,14 +69,12 @@ private static bool AttemptOutputCaching(OutputCacheContext context) // Verify the method if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsHead(request.Method)) { - context.Logger.RequestMethodNotCacheable(request.Method); return false; } // Verify existence of authorization headers if (!StringValues.IsNullOrEmpty(request.Headers.Authorization) || request.HttpContext.User?.Identity?.IsAuthenticated == true) { - context.Logger.RequestWithAuthorizationNotCacheable(); return false; } diff --git a/src/Middleware/OutputCaching/src/Policies/NamedPolicy.cs b/src/Middleware/OutputCaching/src/Policies/NamedPolicy.cs index 4967d8b1b3fc..1a202aa1bf32 100644 --- a/src/Middleware/OutputCaching/src/Policies/NamedPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NamedPolicy.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + namespace Microsoft.AspNetCore.OutputCaching; /// @@ -9,6 +12,8 @@ namespace Microsoft.AspNetCore.OutputCaching; internal sealed class NamedPolicy : IOutputCachePolicy { private readonly string _policyName; + private IOptions? _options; + private readonly object _synLock = new(); /// /// Create a new instance. @@ -60,7 +65,15 @@ ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, Cance internal IOutputCachePolicy? GetProfilePolicy(OutputCacheContext context) { - var policies = context.Options.NamedPolicies; + if (_options == null) + { + lock (_synLock) + { + _options = context.HttpContext.RequestServices.GetRequiredService>(); + } + } + + var policies = _options!.Value.NamedPolicies; return policies != null && policies.TryGetValue(_policyName, out var cacheProfile) ? cacheProfile diff --git a/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs b/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs index d01f60a24374..96dfe7e7aa15 100644 --- a/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.OutputCaching.Policies; @@ -29,7 +30,8 @@ public TypedPolicy([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Pu private IOutputCachePolicy? CreatePolicy(OutputCacheContext context) { - return _instance ??= ActivatorUtilities.CreateInstance(context.Options.ApplicationServices, _policyType) as IOutputCachePolicy; + var options = context.HttpContext.RequestServices.GetRequiredService>(); + return _instance ??= ActivatorUtilities.CreateInstance(options.Value.ApplicationServices, _policyType) as IOutputCachePolicy; } /// diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index 2973409993d5..326a2b330ebc 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -37,6 +37,9 @@ Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByRouteValues.get -> Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByRouteValues.init -> void Microsoft.AspNetCore.OutputCaching.OutputCacheContext.EnableOutputCaching.get -> bool Microsoft.AspNetCore.OutputCaching.OutputCacheContext.EnableOutputCaching.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheContext.HttpContext.init -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheContext.OutputCacheContext() -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheContext.ResponseTime.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.AddBasePolicy(Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy! policy) -> void Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.AddBasePolicy(System.Action! build) -> void Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.AddPolicy(string! name, Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy! policy) -> void diff --git a/src/Middleware/OutputCaching/test/OutputCacheAttributeTests.cs b/src/Middleware/OutputCaching/test/OutputCacheAttributeTests.cs index 5f56000e6509..56248b7762ff 100644 --- a/src/Middleware/OutputCaching/test/OutputCacheAttributeTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCacheAttributeTests.cs @@ -11,8 +11,7 @@ public class OutputCacheAttributeTests [Fact] public void Attribute_CreatesDefaultPolicy() { - var cache = new TestOutputCache(); - var context = TestUtils.CreateTestContext(cache); + var context = TestUtils.CreateUninitializedContext(); var attribute = OutputCacheMethods.GetAttribute(nameof(OutputCacheMethods.Default)); var policy = attribute.BuildPolicy(); @@ -23,8 +22,7 @@ public void Attribute_CreatesDefaultPolicy() [Fact] public async Task Attribute_CreatesExpirePolicy() { - var cache = new TestOutputCache(); - var context = TestUtils.CreateTestContext(cache); + var context = TestUtils.CreateUninitializedContext(); var attribute = OutputCacheMethods.GetAttribute(nameof(OutputCacheMethods.Duration)); await attribute.BuildPolicy().CacheRequestAsync(context, cancellation: default); @@ -36,8 +34,7 @@ public async Task Attribute_CreatesExpirePolicy() [Fact] public async Task Attribute_CreatesNoStorePolicy() { - var cache = new TestOutputCache(); - var context = TestUtils.CreateTestContext(cache); + var context = TestUtils.CreateUninitializedContext(); var attribute = OutputCacheMethods.GetAttribute(nameof(OutputCacheMethods.NoStore)); await attribute.BuildPolicy().CacheRequestAsync(context, cancellation: default); @@ -48,11 +45,10 @@ public async Task Attribute_CreatesNoStorePolicy() [Fact] public async Task Attribute_CreatesNamedPolicy() { - var cache = new TestOutputCache(); var options = new OutputCacheOptions(); options.AddPolicy("MyPolicy", b => b.Expire(TimeSpan.FromSeconds(42))); - var context = TestUtils.CreateTestContext(cache, options); + var context = TestUtils.CreateTestContext(options: options); var attribute = OutputCacheMethods.GetAttribute(nameof(OutputCacheMethods.PolicyName)); await attribute.BuildPolicy().CacheRequestAsync(context, cancellation: default); @@ -64,8 +60,7 @@ public async Task Attribute_CreatesNamedPolicy() [Fact] public async Task Attribute_CreatesVaryByHeaderPolicy() { - var cache = new TestOutputCache(); - var context = TestUtils.CreateTestContext(cache); + var context = TestUtils.CreateUninitializedContext(); context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; @@ -81,8 +76,7 @@ public async Task Attribute_CreatesVaryByHeaderPolicy() [Fact] public async Task Attribute_CreatesVaryByQueryPolicy() { - var cache = new TestOutputCache(); - var context = TestUtils.CreateTestContext(cache); + var context = TestUtils.CreateUninitializedContext(); context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); var attribute = OutputCacheMethods.GetAttribute(nameof(OutputCacheMethods.VaryByQueryKeys)); @@ -97,8 +91,7 @@ public async Task Attribute_CreatesVaryByQueryPolicy() [Fact] public async Task Attribute_CreatesVaryByRoutePolicy() { - var cache = new TestOutputCache(); - var context = TestUtils.CreateTestContext(cache); + var context = TestUtils.CreateUninitializedContext(); context.HttpContext.Request.RouteValues = new Routing.RouteValueDictionary() { ["RouteA"] = "ValueA", diff --git a/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs b/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs index 690031b2a78e..2faea5a43250 100644 --- a/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs @@ -19,7 +19,7 @@ public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504() var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); - var context = TestUtils.CreateTestContext(cache); + var context = TestUtils.CreateTestContext(cache: cache); context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() { OnlyIfCached = true @@ -39,7 +39,7 @@ public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails() var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); - var context = TestUtils.CreateTestContext(cache); + var context = TestUtils.CreateTestContext(cache: cache); middleware.TryGetRequestPolicies(context.HttpContext, out var policies); Assert.False(await middleware.TryServeFromCacheAsync(context, policies)); @@ -55,7 +55,7 @@ public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds() var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); - var context = TestUtils.CreateTestContext(cache); + var context = TestUtils.CreateTestContext(cache: cache); middleware.TryGetRequestPolicies(context.HttpContext, out var policies); await OutputCacheEntryFormatter.StoreAsync( @@ -82,7 +82,7 @@ public async Task TryServeFromCacheAsync_CachedResponseFound_OverwritesExistingH var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); - var context = TestUtils.CreateTestContext(cache); + var context = TestUtils.CreateTestContext(cache: cache); middleware.TryGetRequestPolicies(context.HttpContext, out var policies); context.CacheKey = "BaseKey"; @@ -114,7 +114,7 @@ public async Task TryServeFromCacheAsync_CachedResponseFound_Serves304IfPossible var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); - var context = TestUtils.CreateTestContext(cache); + var context = TestUtils.CreateTestContext(cache: cache); context.HttpContext.Request.Headers.IfNoneMatch = "*"; middleware.TryGetRequestPolicies(context.HttpContext, out var policies); @@ -132,17 +132,20 @@ await OutputCacheEntryFormatter.StoreAsync("BaseKey", Assert.Equal(1, cache.GetCount); TestUtils.AssertLoggedMessages( sink.Writes, + LoggedMessage.NotModifiedIfNoneMatchStar, LoggedMessage.NotModifiedServed); } [Fact] public void ContentIsNotModified_NotConditionalRequest_False() { + var cache = new TestOutputCache(); var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); + var context = TestUtils.CreateTestContext(testSink: sink); context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; - Assert.False(OutputCacheMiddleware.ContentIsNotModified(context)); + Assert.False(middleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } @@ -151,24 +154,25 @@ public void ContentIsNotModified_IfModifiedSince_FallsbackToDateHeader() { var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var context = TestUtils.CreateTestContext(testSink: sink); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; context.HttpContext.Request.Headers.IfModifiedSince = HeaderUtilities.FormatDate(utcNow); // Verify modifications in the past succeeds context.CachedResponse.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); - Assert.True(OutputCacheMiddleware.ContentIsNotModified(context)); + Assert.True(middleware.ContentIsNotModified(context)); Assert.Single(sink.Writes); // Verify modifications at present succeeds context.CachedResponse.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); - Assert.True(OutputCacheMiddleware.ContentIsNotModified(context)); + Assert.True(middleware.ContentIsNotModified(context)); Assert.Equal(2, sink.Writes.Count); // Verify modifications in the future fails context.CachedResponse.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); - Assert.False(OutputCacheMiddleware.ContentIsNotModified(context)); + Assert.False(middleware.ContentIsNotModified(context)); // Verify logging TestUtils.AssertLoggedMessages( @@ -182,7 +186,8 @@ public void ContentIsNotModified_IfModifiedSince_LastModifiedOverridesDateHeader { var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; context.HttpContext.Request.Headers.IfModifiedSince = HeaderUtilities.FormatDate(utcNow); @@ -190,19 +195,19 @@ public void ContentIsNotModified_IfModifiedSince_LastModifiedOverridesDateHeader // Verify modifications in the past succeeds context.CachedResponse.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); context.CachedResponse.Headers[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); - Assert.True(OutputCacheMiddleware.ContentIsNotModified(context)); + Assert.True(middleware.ContentIsNotModified(context)); Assert.Single(sink.Writes); // Verify modifications at present context.CachedResponse.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); context.CachedResponse.Headers[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow); - Assert.True(OutputCacheMiddleware.ContentIsNotModified(context)); + Assert.True(middleware.ContentIsNotModified(context)); Assert.Equal(2, sink.Writes.Count); // Verify modifications in the future fails context.CachedResponse.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); context.CachedResponse.Headers[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); - Assert.False(OutputCacheMiddleware.ContentIsNotModified(context)); + Assert.False(middleware.ContentIsNotModified(context)); // Verify logging TestUtils.AssertLoggedMessages( @@ -216,7 +221,8 @@ public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToTrue() { var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; // This would fail the IfModifiedSince checks @@ -224,7 +230,7 @@ public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToTrue() context.CachedResponse.Headers[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); context.HttpContext.Request.Headers.IfNoneMatch = EntityTagHeaderValue.Any.ToString(); - Assert.True(OutputCacheMiddleware.ContentIsNotModified(context)); + Assert.True(middleware.ContentIsNotModified(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.NotModifiedIfNoneMatchStar); @@ -235,7 +241,8 @@ public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToFalse() { var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; // This would pass the IfModifiedSince checks @@ -243,7 +250,7 @@ public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToFalse() context.CachedResponse.Headers[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); context.HttpContext.Request.Headers.IfNoneMatch = "\"E1\""; - Assert.False(OutputCacheMiddleware.ContentIsNotModified(context)); + Assert.False(middleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } @@ -251,11 +258,12 @@ public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToFalse() public void ContentIsNotModified_IfNoneMatch_AnyWithoutETagInResponse_False() { var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; context.HttpContext.Request.Headers.IfNoneMatch = "\"E1\""; - Assert.False(OutputCacheMiddleware.ContentIsNotModified(context)); + Assert.False(middleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } @@ -278,12 +286,13 @@ public static TheoryData EquivalentW public void ContentIsNotModified_IfNoneMatch_ExplicitWithMatch_True(EntityTagHeaderValue responseETag, EntityTagHeaderValue requestETag) { var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; context.CachedResponse.Headers[HeaderNames.ETag] = responseETag.ToString(); context.HttpContext.Request.Headers.IfNoneMatch = requestETag.ToString(); - Assert.True(OutputCacheMiddleware.ContentIsNotModified(context)); + Assert.True(middleware.ContentIsNotModified(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.NotModifiedIfNoneMatchMatched); @@ -293,12 +302,13 @@ public void ContentIsNotModified_IfNoneMatch_ExplicitWithMatch_True(EntityTagHea public void ContentIsNotModified_IfNoneMatch_ExplicitWithoutMatch_False() { var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; context.CachedResponse.Headers[HeaderNames.ETag] = "\"E2\""; context.HttpContext.Request.Headers.IfNoneMatch = "\"E1\""; - Assert.False(OutputCacheMiddleware.ContentIsNotModified(context)); + Assert.False(middleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } @@ -306,12 +316,13 @@ public void ContentIsNotModified_IfNoneMatch_ExplicitWithoutMatch_False() public void ContentIsNotModified_IfNoneMatch_MatchesAtLeastOneValue_True() { var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; context.CachedResponse.Headers[HeaderNames.ETag] = "\"E2\""; context.HttpContext.Request.Headers.IfNoneMatch = new string[] { "\"E0\", \"E1\"", "\"E1\", \"E2\"" }; - Assert.True(OutputCacheMiddleware.ContentIsNotModified(context)); + Assert.True(middleware.ContentIsNotModified(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.NotModifiedIfNoneMatchMatched); @@ -485,7 +496,7 @@ public void FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_NullOrEmptyRules(S var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); - var context = TestUtils.CreateTestContext(cache); + var context = TestUtils.CreateTestContext(cache: cache); context.HttpContext.Response.Headers.Vary = vary; context.HttpContext.Features.Set(new OutputCacheFeature(context)); @@ -573,7 +584,7 @@ public async Task FinalizeCacheBody_Cache_IfContentLengthMatches() var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); - var context = TestUtils.CreateTestContext(cache); + var context = TestUtils.CreateTestContext(cache: cache); middleware.ShimResponseStream(context); context.HttpContext.Response.ContentLength = 20; @@ -600,7 +611,7 @@ public async Task FinalizeCacheBody_DoNotCache_IfContentLengthMismatches(string var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); - var context = TestUtils.CreateTestContext(cache); + var context = TestUtils.CreateTestContext(cache: cache); middleware.ShimResponseStream(context); context.HttpContext.Response.ContentLength = 9; @@ -628,7 +639,7 @@ public async Task FinalizeCacheBody_RequestHead_Cache_IfContentLengthPresent_And var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); - var context = TestUtils.CreateTestContext(cache); + var context = TestUtils.CreateTestContext(cache: cache); middleware.ShimResponseStream(context); context.HttpContext.Response.ContentLength = 10; @@ -658,7 +669,7 @@ public async Task FinalizeCacheBody_Cache_IfContentLengthAbsent() var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); - var context = TestUtils.CreateTestContext(cache); + var context = TestUtils.CreateTestContext(cache: cache); middleware.ShimResponseStream(context); @@ -682,7 +693,7 @@ public async Task FinalizeCacheBody_DoNotCache_IfIsResponseCacheableFalse() var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); - var context = TestUtils.CreateTestContext(cache); + var context = TestUtils.CreateTestContext(cache: cache); middleware.ShimResponseStream(context); await context.HttpContext.Response.WriteAsync(new string('0', 10)); @@ -703,7 +714,7 @@ public async Task FinalizeCacheBody_DoNotCache_IfBufferingDisabled() var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); - var context = TestUtils.CreateTestContext(cache); + var context = TestUtils.CreateTestContext(cache: cache); middleware.ShimResponseStream(context); await context.HttpContext.Response.WriteAsync(new string('0', 10)); diff --git a/src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs b/src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs index 64b4a7846f31..a33e4f103a52 100644 --- a/src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs @@ -126,9 +126,10 @@ public async Task PredicatePolicy_Filters(bool filter, bool enabled, bool expect [Fact] public async Task ProfilePolicy_UsesNamedProfile() { - var context = TestUtils.CreateUninitializedContext(); - context.Options.AddPolicy("enabled", EnableCachePolicy.Enabled); - context.Options.AddPolicy("disabled", EnableCachePolicy.Disabled); + var options = new OutputCacheOptions(); + options.AddPolicy("enabled", EnableCachePolicy.Enabled); + options.AddPolicy("disabled", EnableCachePolicy.Disabled); + var context = TestUtils.CreateUninitializedContext(options: options); IOutputCachePolicy policy = new NamedPolicy("enabled"); diff --git a/src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs b/src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs index 057410b5e04f..8a3847988c09 100644 --- a/src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.OutputCaching.Tests; @@ -21,7 +19,7 @@ public void BuildPolicy_CreatesDefaultPolicy() [Fact] public async Task BuildPolicy_CreatesExpirePolicy() { - var context = CreateTestContext(); + var context = TestUtils.CreateUninitializedContext(); var duration = 42; var builder = new OutputCachePolicyBuilder(); @@ -35,7 +33,7 @@ public async Task BuildPolicy_CreatesExpirePolicy() [Fact] public async Task BuildPolicy_CreatesNoStorePolicy() { - var context = CreateTestContext(); + var context = TestUtils.CreateUninitializedContext(); var builder = new OutputCachePolicyBuilder(); var policy = builder.NoCache().Build(); @@ -52,7 +50,7 @@ public async Task BuildPolicy_AddsCustomPolicy() var duration = 42; options.AddPolicy(name, b => b.Expire(TimeSpan.FromSeconds(duration))); - var context = CreateTestContext(options: options); + var context = TestUtils.CreateUninitializedContext(options: options); var builder = new OutputCachePolicyBuilder(); var policy = builder.AddPolicy(new NamedPolicy(name)).Build(); @@ -65,7 +63,7 @@ public async Task BuildPolicy_AddsCustomPolicy() [Fact] public async Task BuildPolicy_CreatesVaryByHeaderPolicy() { - var context = CreateTestContext(); + var context = TestUtils.CreateUninitializedContext(); context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; @@ -82,7 +80,7 @@ public async Task BuildPolicy_CreatesVaryByHeaderPolicy() [Fact] public async Task BuildPolicy_CreatesVaryByQueryPolicy() { - var context = CreateTestContext(); + var context = TestUtils.CreateUninitializedContext(); context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); var builder = new OutputCachePolicyBuilder(); @@ -98,7 +96,7 @@ public async Task BuildPolicy_CreatesVaryByQueryPolicy() [Fact] public async Task BuildPolicy_CreatesVaryByRoutePolicy() { - var context = CreateTestContext(); + var context = TestUtils.CreateUninitializedContext(); context.HttpContext.Request.RouteValues = new Routing.RouteValueDictionary() { ["RouteA"] = "ValueA", @@ -118,7 +116,7 @@ public async Task BuildPolicy_CreatesVaryByRoutePolicy() [Fact] public async Task BuildPolicy_CreatesVaryByValuePolicy() { - var context = CreateTestContext(); + var context = TestUtils.CreateUninitializedContext(); var builder = new OutputCachePolicyBuilder(); var policy = builder.VaryByValue(context => new KeyValuePair("color", "blue")).Build(); @@ -131,7 +129,7 @@ public async Task BuildPolicy_CreatesVaryByValuePolicy() [Fact] public async Task BuildPolicy_CreatesTagPolicy() { - var context = CreateTestContext(); + var context = TestUtils.CreateUninitializedContext(); var builder = new OutputCachePolicyBuilder(); var policy = builder.Tag("tag1", "tag2").Build(); @@ -145,7 +143,7 @@ public async Task BuildPolicy_CreatesTagPolicy() [Fact] public async Task BuildPolicy_AllowsLocking() { - var context = CreateTestContext(); + var context = TestUtils.CreateUninitializedContext(); var builder = new OutputCachePolicyBuilder(); var policy = builder.Build(); @@ -157,8 +155,7 @@ public async Task BuildPolicy_AllowsLocking() [Fact] public async Task BuildPolicy_EnablesLocking() { - var cache = new TestOutputCache(); - var context = TestUtils.CreateTestContext(cache); + var context = TestUtils.CreateUninitializedContext(); var builder = new OutputCachePolicyBuilder(); var policy = builder.AllowLocking(true).Build(); @@ -170,7 +167,7 @@ public async Task BuildPolicy_EnablesLocking() [Fact] public async Task BuildPolicy_DisablesLocking() { - var context = CreateTestContext(); + var context = TestUtils.CreateUninitializedContext(); var builder = new OutputCachePolicyBuilder(); var policy = builder.AllowLocking(false).Build(); @@ -182,7 +179,7 @@ public async Task BuildPolicy_DisablesLocking() [Fact] public async Task BuildPolicy_ClearsDefaultPolicy() { - var context = CreateTestContext(); + var context = TestUtils.CreateUninitializedContext(); var builder = new OutputCachePolicyBuilder(); var policy = builder.Clear().Build(); @@ -197,7 +194,7 @@ public async Task BuildPolicy_ClearsDefaultPolicy() [Fact] public async Task BuildPolicy_DisablesCache() { - var context = CreateTestContext(); + var context = TestUtils.CreateUninitializedContext(); var builder = new OutputCachePolicyBuilder(); var policy = builder.NoCache().Build(); @@ -209,7 +206,7 @@ public async Task BuildPolicy_DisablesCache() [Fact] public async Task BuildPolicy_EnablesCache() { - var context = CreateTestContext(); + var context = TestUtils.CreateUninitializedContext(); var builder = new OutputCachePolicyBuilder(); var policy = builder.NoCache().Cache().Build(); @@ -217,11 +214,4 @@ public async Task BuildPolicy_EnablesCache() Assert.True(context.EnableOutputCaching); } - - private static OutputCacheContext CreateTestContext(IOutputCacheStore? cache = null, OutputCacheOptions? options = null) - { - return new OutputCacheContext(new DefaultHttpContext(), cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCacheOptions()).Value, NullLogger.Instance) - { - }; - } } diff --git a/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs index 6238b09791aa..ce93251c1869 100644 --- a/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs @@ -46,7 +46,7 @@ public static TheoryData NonCacheableMethods public async Task AttemptOutputCaching_CacheableMethods_IsAllowed(string method) { var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var context = TestUtils.CreateTestContext(testSink: sink); var policies = new[] { new OutputCachePolicyBuilder().Build() }; context.HttpContext.Request.Method = method; @@ -65,7 +65,7 @@ public async Task AttemptOutputCaching_CacheableMethods_IsAllowed(string method) public async Task AttemptOutputCaching_UncacheableMethods_NotAllowed(string method) { var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var context = TestUtils.CreateTestContext(testSink: sink); var policy = new OutputCachePolicyBuilder().Build(); context.HttpContext.Request.Method = method; @@ -73,16 +73,13 @@ public async Task AttemptOutputCaching_UncacheableMethods_NotAllowed(string meth Assert.False(context.AllowCacheLookup); Assert.False(context.AllowCacheStorage); - TestUtils.AssertLoggedMessages( - sink.Writes, - LoggedMessage.RequestMethodNotCacheable); } [Fact] public async Task AttemptResponseCaching_AuthorizationHeaders_NotAllowed() { var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Headers.Authorization = "Placeholder"; @@ -92,17 +89,13 @@ public async Task AttemptResponseCaching_AuthorizationHeaders_NotAllowed() Assert.False(context.AllowCacheStorage); Assert.False(context.AllowCacheLookup); - - TestUtils.AssertLoggedMessages( - sink.Writes, - LoggedMessage.RequestWithAuthorizationNotCacheable); } [Fact] public async Task AllowCacheStorage_NoStore_IsAllowed() { var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() { @@ -120,7 +113,7 @@ public async Task AllowCacheStorage_NoStore_IsAllowed() public async Task AllowCacheLookup_LegacyDirectives_OverridenByCacheControl() { var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Headers.Pragma = "no-cache"; context.HttpContext.Request.Headers.CacheControl = "max-age=10"; @@ -136,7 +129,7 @@ public async Task AllowCacheLookup_LegacyDirectives_OverridenByCacheControl() public async Task IsResponseCacheable_NoPublic_IsAllowed() { var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var context = TestUtils.CreateTestContext(testSink: sink); var policy = new OutputCachePolicyBuilder().Build(); await policy.ServeResponseAsync(context, default); @@ -150,7 +143,7 @@ public async Task IsResponseCacheable_NoPublic_IsAllowed() public async Task IsResponseCacheable_Public_IsAllowed() { var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() { Public = true @@ -168,7 +161,7 @@ public async Task IsResponseCacheable_Public_IsAllowed() public async Task IsResponseCacheable_NoCache_IsAllowed() { var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() { NoCache = true @@ -186,7 +179,7 @@ public async Task IsResponseCacheable_NoCache_IsAllowed() public async Task IsResponseCacheable_ResponseNoStore_IsAllowed() { var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() { NoStore = true @@ -204,7 +197,7 @@ public async Task IsResponseCacheable_ResponseNoStore_IsAllowed() public async Task IsResponseCacheable_SetCookieHeader_NotAllowed() { var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.HttpContext.Response.Headers.SetCookie = "cookieName=cookieValue"; var policy = new OutputCachePolicyBuilder().Build(); @@ -212,16 +205,13 @@ public async Task IsResponseCacheable_SetCookieHeader_NotAllowed() Assert.False(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); - TestUtils.AssertLoggedMessages( - sink.Writes, - LoggedMessage.ResponseWithSetCookieNotCacheable); } [Fact] public async Task IsResponseCacheable_VaryHeaderByStar_IsAllowed() { var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.HttpContext.Response.Headers.Vary = "*"; var policy = new OutputCachePolicyBuilder().Build(); await policy.ServeResponseAsync(context, default); @@ -235,7 +225,7 @@ public async Task IsResponseCacheable_VaryHeaderByStar_IsAllowed() public async Task IsResponseCacheable_Private_IsAllowed() { var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() { Private = true @@ -254,7 +244,7 @@ public async Task IsResponseCacheable_Private_IsAllowed() public async Task IsResponseCacheable_SuccessStatusCodes_IsAllowed(int statusCode) { var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.HttpContext.Response.StatusCode = statusCode; var policy = new OutputCachePolicyBuilder().Build(); @@ -330,7 +320,7 @@ public async Task IsResponseCacheable_SuccessStatusCodes_IsAllowed(int statusCod public async Task IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode) { var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.HttpContext.Response.StatusCode = statusCode; var policy = new OutputCachePolicyBuilder().Build(); @@ -338,16 +328,13 @@ public async Task IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statu Assert.True(context.AllowCacheLookup); Assert.False(context.AllowCacheStorage); - TestUtils.AssertLoggedMessages( - sink.Writes, - LoggedMessage.ResponseWithUnsuccessfulStatusCodeNotCacheable); } [Fact] public async Task IsResponseCacheable_NoExpiryRequirements_IsAllowed() { var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; var utcNow = DateTimeOffset.UtcNow; @@ -367,7 +354,7 @@ public async Task IsResponseCacheable_MaxAgeOverridesExpiry_IsAllowed() { var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() { @@ -390,7 +377,7 @@ public async Task IsResponseCacheable_SharedMaxAgeOverridesMaxAge_IsAllowed() { var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); + var context = TestUtils.CreateTestContext(testSink: sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() { diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs index 149b26f1ebd6..28cff873d3a4 100644 --- a/src/Middleware/OutputCaching/test/TestUtils.cs +++ b/src/Middleware/OutputCaching/test/TestUtils.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable enable +using System; using System.Net.Http; using System.Text; using Microsoft.AspNetCore.Builder; @@ -16,6 +17,7 @@ using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; +using Moq; namespace Microsoft.AspNetCore.OutputCaching.Tests; @@ -194,37 +196,24 @@ internal static OutputCacheMiddleware CreateTestMiddleware( return new OutputCacheMiddleware( next, Options.Create(options), - testSink == null ? (ILoggerFactory)NullLoggerFactory.Instance : new TestLoggerFactory(testSink, true), + testSink == null ? NullLoggerFactory.Instance : new TestLoggerFactory(testSink, true), cache, keyProvider); } - internal static OutputCacheContext CreateTestContext(IOutputCacheStore? cache = null, OutputCacheOptions? options = null) + internal static OutputCacheContext CreateTestContext(HttpContext? httpContext = null, IOutputCacheStore? cache = null, OutputCacheOptions? options = null, ITestSink? testSink = null) { - return new OutputCacheContext(new DefaultHttpContext(), cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCacheOptions()).Value, NullLogger.Instance) - { - EnableOutputCaching = true, - AllowCacheStorage = true, - AllowCacheLookup = true, - ResponseTime = DateTimeOffset.UtcNow - }; - } + var serviceProvider = new Mock(); + serviceProvider.Setup(x => x.GetService(typeof(IOutputCacheStore))).Returns(cache ?? new TestOutputCache()); + serviceProvider.Setup(x => x.GetService(typeof(IOptions))).Returns(Options.Create(options ?? new OutputCacheOptions())); + serviceProvider.Setup(x => x.GetService(typeof(ILogger))).Returns(testSink == null ? NullLogger.Instance : new TestLogger("OutputCachingTests", testSink, true)); - internal static OutputCacheContext CreateTestContext(HttpContext httpContext, IOutputCacheStore? cache = null, OutputCacheOptions? options = null) - { - return new OutputCacheContext(httpContext, cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCacheOptions()).Value, NullLogger.Instance) - { - EnableOutputCaching = true, - AllowCacheStorage = true, - AllowCacheLookup = true, - ResponseTime = DateTimeOffset.UtcNow - }; - } + httpContext ??= new DefaultHttpContext(); + httpContext.RequestServices = serviceProvider.Object; - internal static OutputCacheContext CreateTestContext(ITestSink testSink, IOutputCacheStore? cache = null, OutputCacheOptions? options = null) - { - return new OutputCacheContext(new DefaultHttpContext(), cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCacheOptions()).Value, new TestLogger("OutputCachingTests", testSink, true)) + return new OutputCacheContext() { + HttpContext = httpContext, EnableOutputCaching = true, AllowCacheStorage = true, AllowCacheLookup = true, @@ -232,13 +221,21 @@ internal static OutputCacheContext CreateTestContext(ITestSink testSink, IOutput }; } - internal static OutputCacheContext CreateUninitializedContext(IOutputCacheStore? cache = null, OutputCacheOptions? options = null) + internal static OutputCacheContext CreateUninitializedContext(HttpContext? httpContext = null, IOutputCacheStore? cache = null, OutputCacheOptions? options = null, ITestSink? testSink = null) { - return new OutputCacheContext(new DefaultHttpContext(), cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCacheOptions()).Value, NullLogger.Instance) + var serviceProvider = new Mock(); + serviceProvider.Setup(x => x.GetService(typeof(IOutputCacheStore))).Returns(cache ?? new TestOutputCache()); + serviceProvider.Setup(x => x.GetService(typeof(IOptions))).Returns(Options.Create(options ?? new OutputCacheOptions())); + serviceProvider.Setup(x => x.GetService(typeof(ILogger))).Returns(testSink == null ? NullLogger.Instance : new TestLogger("OutputCachingTests", testSink, true)); + + httpContext ??= new DefaultHttpContext(); + httpContext.RequestServices = serviceProvider.Object; + + return new OutputCacheContext() { + HttpContext = httpContext, }; } - internal static void AssertLoggedMessages(IEnumerable messages, params LoggedMessage[] expectedMessages) { var messageList = messages.ToList(); @@ -271,22 +268,18 @@ internal static void Write(this HttpResponse response, string text) internal class LoggedMessage { - internal static LoggedMessage RequestMethodNotCacheable => new LoggedMessage(1, LogLevel.Debug); - internal static LoggedMessage RequestWithAuthorizationNotCacheable => new LoggedMessage(2, LogLevel.Debug); - internal static LoggedMessage ResponseWithSetCookieNotCacheable => new LoggedMessage(3, LogLevel.Debug); - internal static LoggedMessage ResponseWithUnsuccessfulStatusCodeNotCacheable => new LoggedMessage(4, LogLevel.Debug); - internal static LoggedMessage NotModifiedIfNoneMatchStar => new LoggedMessage(5, LogLevel.Debug); - internal static LoggedMessage NotModifiedIfNoneMatchMatched => new LoggedMessage(6, LogLevel.Debug); - internal static LoggedMessage NotModifiedIfModifiedSinceSatisfied => new LoggedMessage(7, LogLevel.Debug); - internal static LoggedMessage NotModifiedServed => new LoggedMessage(8, LogLevel.Information); - internal static LoggedMessage CachedResponseServed => new LoggedMessage(9, LogLevel.Information); - internal static LoggedMessage GatewayTimeoutServed => new LoggedMessage(10, LogLevel.Information); - internal static LoggedMessage NoResponseServed => new LoggedMessage(11, LogLevel.Information); - internal static LoggedMessage VaryByRulesUpdated => new LoggedMessage(12, LogLevel.Debug); - internal static LoggedMessage ResponseCached => new LoggedMessage(13, LogLevel.Information); - internal static LoggedMessage ResponseNotCached => new LoggedMessage(14, LogLevel.Information); - internal static LoggedMessage ResponseContentLengthMismatchNotCached => new LoggedMessage(15, LogLevel.Warning); - internal static LoggedMessage ExpirationExpiresExceeded => new LoggedMessage(15, LogLevel.Debug); + internal static LoggedMessage NotModifiedIfNoneMatchStar => new LoggedMessage(1, LogLevel.Debug); + internal static LoggedMessage NotModifiedIfNoneMatchMatched => new LoggedMessage(2, LogLevel.Debug); + internal static LoggedMessage NotModifiedIfModifiedSinceSatisfied => new LoggedMessage(3, LogLevel.Debug); + internal static LoggedMessage NotModifiedServed => new LoggedMessage(4, LogLevel.Information); + internal static LoggedMessage CachedResponseServed => new LoggedMessage(5, LogLevel.Information); + internal static LoggedMessage GatewayTimeoutServed => new LoggedMessage(6, LogLevel.Information); + internal static LoggedMessage NoResponseServed => new LoggedMessage(7, LogLevel.Information); + internal static LoggedMessage VaryByRulesUpdated => new LoggedMessage(8, LogLevel.Debug); + internal static LoggedMessage ResponseCached => new LoggedMessage(9, LogLevel.Information); + internal static LoggedMessage ResponseNotCached => new LoggedMessage(10, LogLevel.Information); + internal static LoggedMessage ResponseContentLengthMismatchNotCached => new LoggedMessage(11, LogLevel.Warning); + internal static LoggedMessage ExpirationExpiresExceeded => new LoggedMessage(12, LogLevel.Debug); private LoggedMessage(int evenId, LogLevel logLevel) { From 568c87cce9beee187b8fa1ed637d724c6262f1a1 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 9 Aug 2022 15:39:34 -0700 Subject: [PATCH 07/12] Add unit tests --- .../test/OutputCacheMiddlewareTests.cs | 95 +++++++++++++++++++ .../test/OutputCachePolicyBuilderTests.cs | 59 ++++++++++++ 2 files changed, 154 insertions(+) diff --git a/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs b/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs index 2faea5a43250..2b426dabd541 100644 --- a/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.Metrics; +using System.Threading.Tasks; +using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.OutputCaching.Memory; @@ -810,4 +813,96 @@ public void GetOrderCasingNormalizedStringValues_PreservesCommas() Assert.Equal(originalStrings, normalizedStrings); } + + [Fact] + public async Task Locking_PreventsConcurrentRequests() + { + var timeout = TimeSpan.FromSeconds(1); + var responseCounter = 0; + + var task1Executing = new ManualResetEventSlim(false); + var task2Executing = new ManualResetEventSlim(false); + + var options = new OutputCacheOptions(); + options.AddBasePolicy(build => build.Cache()); + + var middleware = TestUtils.CreateTestMiddleware(options: options, next: c => + { + responseCounter++; + task1Executing.Set(); + + // Wait for the second request to be processed before processing the first one + task2Executing.Wait(); + + c.Response.Write("Hello" + responseCounter); + return Task.CompletedTask; + }); + + var context1 = TestUtils.CreateTestContext(); + context1.HttpContext.Request.Method = "GET"; + context1.HttpContext.Request.Path = "/"; + + var context2 = TestUtils.CreateTestContext(); + context2.HttpContext.Request.Method = "GET"; + context2.HttpContext.Request.Path = "/"; + + var task1 = Task.Run(() => middleware.Invoke(context1.HttpContext)); + + // Wait for the first request to be processed before sending a second one + task1Executing.Wait(timeout); + + var task2 = Task.Run(() => middleware.Invoke(context2.HttpContext)); + + task2Executing.Set(); + + await Task.WhenAll(task1, task2); + + Assert.Equal(1, responseCounter); + } + + [Fact] + public async Task Locking_ExecuteAllRequestsWhenDisabled() + { + var timeout = TimeSpan.FromSeconds(1); + var responseCounter = 0; + + var task1Executing = new ManualResetEventSlim(false); + var task2Executing = new ManualResetEventSlim(false); + + var options = new OutputCacheOptions(); + options.AddBasePolicy(build => build.Cache().AllowLocking(false)); + + var middleware = TestUtils.CreateTestMiddleware(options: options, next: c => + { + responseCounter++; + task1Executing.Set(); + + // Wait for the second request to be processed before processing the first one + task2Executing.Wait(); + + c.Response.Write("Hello" + responseCounter); + return Task.CompletedTask; + }); + + var context1 = TestUtils.CreateTestContext(); + context1.HttpContext.Request.Method = "GET"; + context1.HttpContext.Request.Path = "/"; + + var context2 = TestUtils.CreateTestContext(); + context2.HttpContext.Request.Method = "GET"; + context2.HttpContext.Request.Path = "/"; + + var task1 = Task.Run(() => middleware.Invoke(context1.HttpContext)); + + // Wait for the first request to be processed before sending a second one + task1Executing.Wait(timeout); + + var task2 = Task.Run(() => middleware.Invoke(context2.HttpContext)); + + task2Executing.Set(); + + await Task.WhenAll(task1, task2); + + Assert.Equal(2, responseCounter); + } } diff --git a/src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs b/src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs index 8a3847988c09..a9da48cd9f43 100644 --- a/src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net.Http; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.OutputCaching.Tests; @@ -214,4 +215,62 @@ public async Task BuildPolicy_EnablesCache() Assert.True(context.EnableOutputCaching); } + + [Theory] + [InlineData(0, 1)] + [InlineData(1, 2)] + [InlineData(2, 3)] + public async Task BuildPolicy_ChecksWithPredicate(int source, int expected) + { + // Each predicate should override the duration from the first base policy + var options = new OutputCacheOptions(); + options.AddBasePolicy(build => build.Expire(TimeSpan.FromSeconds(1))); + options.AddBasePolicy(build => build.With(c => source == 1).Expire(TimeSpan.FromSeconds(2))); + options.AddBasePolicy(build => build.With(c => source == 2).Expire(TimeSpan.FromSeconds(3))); + + var context = TestUtils.CreateUninitializedContext(options: options); + + foreach (var policy in options.BasePolicies) + { + await policy.CacheRequestAsync(context, default); + } + + Assert.True(context.EnableOutputCaching); + Assert.Equal(expected, context.ResponseExpirationTimeSpan?.TotalSeconds); + } + + [Fact] + public async Task BuildPolicy_NoDefaultWithFalsePredicate() + { + // Each predicate should override the duration from the first base policy + var options = new OutputCacheOptions(); + options.AddBasePolicy(build => build.With(c => false).Expire(TimeSpan.FromSeconds(2))); + + var context = TestUtils.CreateUninitializedContext(options: options); + + foreach (var policy in options.BasePolicies) + { + await policy.CacheRequestAsync(context, default); + } + + Assert.False(context.EnableOutputCaching); + Assert.NotEqual(2, context.ResponseExpirationTimeSpan?.TotalSeconds); + } + + [Fact] + public async Task BuildPolicy_CacheReturnsDefault() + { + // Each predicate should override the duration from the first base policy + var options = new OutputCacheOptions(); + options.AddBasePolicy(build => build.Cache()); + + var context = TestUtils.CreateUninitializedContext(options: options); + + foreach (var policy in options.BasePolicies) + { + await policy.CacheRequestAsync(context, default); + } + + Assert.True(context.EnableOutputCaching); + } } From 83abb4594728e836d4b0484a062df395aa2dba9f Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 9 Aug 2022 16:18:07 -0700 Subject: [PATCH 08/12] Nits --- src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs | 2 +- src/Middleware/OutputCaching/src/Policies/NamedPolicy.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs b/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs index fe57daae8c8b..85c6a3621e9a 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs @@ -171,7 +171,7 @@ public string CreateStorageKey(OutputCacheContext context) var routeValuesCount = varyByRules?.RouteValues.Count ?? 0; if (routeValuesCount > 0) { - // Append a group separator for the query key segment of the cache key + // Append a group separator for the route values segment of the cache key builder.Append(KeyDelimiter) .Append('R'); diff --git a/src/Middleware/OutputCaching/src/Policies/NamedPolicy.cs b/src/Middleware/OutputCaching/src/Policies/NamedPolicy.cs index 1a202aa1bf32..43ba24e9a017 100644 --- a/src/Middleware/OutputCaching/src/Policies/NamedPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NamedPolicy.cs @@ -69,7 +69,7 @@ ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, Cance { lock (_synLock) { - _options = context.HttpContext.RequestServices.GetRequiredService>(); + _options ??= context.HttpContext.RequestServices.GetRequiredService>(); } } From 64fd032bf1b13ad30c2789e477c6e5b34aa3366c Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 10 Aug 2022 15:59:59 -0700 Subject: [PATCH 09/12] Remove locking tests timeouts --- .../OutputCaching/test/OutputCacheMiddlewareTests.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs b/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs index 2b426dabd541..e91cc9602886 100644 --- a/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs @@ -817,7 +817,6 @@ public void GetOrderCasingNormalizedStringValues_PreservesCommas() [Fact] public async Task Locking_PreventsConcurrentRequests() { - var timeout = TimeSpan.FromSeconds(1); var responseCounter = 0; var task1Executing = new ManualResetEventSlim(false); @@ -849,7 +848,7 @@ public async Task Locking_PreventsConcurrentRequests() var task1 = Task.Run(() => middleware.Invoke(context1.HttpContext)); // Wait for the first request to be processed before sending a second one - task1Executing.Wait(timeout); + task1Executing.Wait(); var task2 = Task.Run(() => middleware.Invoke(context2.HttpContext)); @@ -863,7 +862,6 @@ public async Task Locking_PreventsConcurrentRequests() [Fact] public async Task Locking_ExecuteAllRequestsWhenDisabled() { - var timeout = TimeSpan.FromSeconds(1); var responseCounter = 0; var task1Executing = new ManualResetEventSlim(false); @@ -895,7 +893,7 @@ public async Task Locking_ExecuteAllRequestsWhenDisabled() var task1 = Task.Run(() => middleware.Invoke(context1.HttpContext)); // Wait for the first request to be processed before sending a second one - task1Executing.Wait(timeout); + task1Executing.Wait(); var task2 = Task.Run(() => middleware.Invoke(context2.HttpContext)); From 0e70c1ee93898f7ac5a48642ff520a78b20da78c Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Thu, 11 Aug 2022 09:12:41 -0700 Subject: [PATCH 10/12] Fix flaky tests --- .../test/OutputCacheMiddlewareTests.cs | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs b/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs index e91cc9602886..550e2077cb29 100644 --- a/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs @@ -825,16 +825,18 @@ public async Task Locking_PreventsConcurrentRequests() var options = new OutputCacheOptions(); options.AddBasePolicy(build => build.Cache()); - var middleware = TestUtils.CreateTestMiddleware(options: options, next: c => + var middleware = TestUtils.CreateTestMiddleware(options: options, next: async c => { responseCounter++; task1Executing.Set(); - // Wait for the second request to be processed before processing the first one + // Wait for the second request to start before processing the first one task2Executing.Wait(); + // Simluate some delay to allow for the second request to run while this one is pending + await Task.Delay(500); + c.Response.Write("Hello" + responseCounter); - return Task.CompletedTask; }); var context1 = TestUtils.CreateTestContext(); @@ -873,11 +875,19 @@ public async Task Locking_ExecuteAllRequestsWhenDisabled() var middleware = TestUtils.CreateTestMiddleware(options: options, next: c => { responseCounter++; - task1Executing.Set(); - - // Wait for the second request to be processed before processing the first one - task2Executing.Wait(); + switch (responseCounter) + { + case 1: + task1Executing.Set(); + task2Executing.Wait(); + break; + case 2: + task1Executing.Wait(); + task2Executing.Set(); + break; + } + c.Response.Write("Hello" + responseCounter); return Task.CompletedTask; }); @@ -892,13 +902,8 @@ public async Task Locking_ExecuteAllRequestsWhenDisabled() var task1 = Task.Run(() => middleware.Invoke(context1.HttpContext)); - // Wait for the first request to be processed before sending a second one - task1Executing.Wait(); - var task2 = Task.Run(() => middleware.Invoke(context2.HttpContext)); - task2Executing.Set(); - await Task.WhenAll(task1, task2); Assert.Equal(2, responseCounter); From 4c80ace48cbfa81165ac3a72bf14e31481533f12 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Thu, 11 Aug 2022 13:46:56 -0700 Subject: [PATCH 11/12] Rename properties --- .../OutputCaching/src/CacheVaryByRules.cs | 6 ++--- .../OutputCaching/src/LoggerExtensions.cs | 4 +-- .../OutputCaching/src/OutputCacheAttribute.cs | 6 ++--- .../src/OutputCacheKeyProvider.cs | 14 +++++----- .../src/OutputCacheMiddleware.cs | 10 +++---- .../src/Policies/VaryByRouteValuePolicy.cs | 4 +-- .../OutputCaching/src/PublicAPI.Unshipped.txt | 8 +++--- .../test/OutputCacheAttributeTests.cs | 12 ++++----- .../test/OutputCacheKeyProviderTests.cs | 6 ++--- .../test/OutputCachePolicyBuilderTests.cs | 6 ++--- .../OutputCaching/test/TestUtils.cs | 27 ++++++++++++------- 11 files changed, 55 insertions(+), 48 deletions(-) diff --git a/src/Middleware/OutputCaching/src/CacheVaryByRules.cs b/src/Middleware/OutputCaching/src/CacheVaryByRules.cs index b67ac54b4111..abbd9f8d62aa 100644 --- a/src/Middleware/OutputCaching/src/CacheVaryByRules.cs +++ b/src/Middleware/OutputCaching/src/CacheVaryByRules.cs @@ -21,12 +21,12 @@ public sealed class CacheVaryByRules public IDictionary VaryByCustom => _varyByCustom ??= new(); /// - /// Gets or sets the list of route values to vary by. + /// Gets or sets the list of route value names to vary by. /// - public StringValues RouteValues { get; set; } + public StringValues RouteValueNames { get; set; } /// - /// Gets or sets the list of headers to vary by. + /// Gets or sets the list of header names to vary by. /// public StringValues HeaderNames { get; set; } diff --git a/src/Middleware/OutputCaching/src/LoggerExtensions.cs b/src/Middleware/OutputCaching/src/LoggerExtensions.cs index 6a4749383193..7a05e21f7c4c 100644 --- a/src/Middleware/OutputCaching/src/LoggerExtensions.cs +++ b/src/Middleware/OutputCaching/src/LoggerExtensions.cs @@ -35,8 +35,8 @@ internal static partial class LoggerExtensions [LoggerMessage(7, LogLevel.Information, "No cached response available for this request.", EventName = "NoResponseServed")] internal static partial void NoResponseServed(this ILogger logger); - [LoggerMessage(8, LogLevel.Debug, "Vary by rules were updated. Headers: {HeaderNames}, Query keys: {QueryKeys}, Route values: {RouteValues}", EventName = "VaryByRulesUpdated")] - internal static partial void VaryByRulesUpdated(this ILogger logger, string headerNames, string queryKeys, string routeValues); + [LoggerMessage(8, LogLevel.Debug, "Vary by rules were updated. Header names: {HeaderNames}, Query keys: {QueryKeys}, Route value names: {RouteValueNames}", EventName = "VaryByRulesUpdated")] + internal static partial void VaryByRulesUpdated(this ILogger logger, string headerNames, string queryKeys, string routeValueNames); [LoggerMessage(9, LogLevel.Information, "The response has been cached.", EventName = "ResponseCached")] internal static partial void ResponseCached(this ILogger logger); diff --git a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs index cf5dbd6b961f..b5bbd028bdb6 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs @@ -52,7 +52,7 @@ public bool NoStore /// /// Gets or sets the route value names to vary by. /// - public string[]? VaryByRouteValues { get; init; } + public string[]? VaryByRouteValueNames { get; init; } /// /// Gets or sets the value of the cache policy name. @@ -88,9 +88,9 @@ internal IOutputCachePolicy BuildPolicy() builder.VaryByHeader(VaryByHeaderNames); } - if (VaryByRouteValues != null) + if (VaryByRouteValueNames != null) { - builder.VaryByRouteValue(VaryByRouteValues); + builder.VaryByRouteValue(VaryByRouteValueNames); } if (_duration != null) diff --git a/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs b/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs index 85c6a3621e9a..456ff0147220 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs @@ -80,7 +80,7 @@ public string CreateStorageKey(OutputCacheContext context) } } - // Vary by headers + // Vary by header names var headersCount = varyByRules?.HeaderNames.Count ?? 0; if (headersCount > 0) { @@ -167,20 +167,20 @@ public string CreateStorageKey(OutputCacheContext context) } } - // Vary by route values - var routeValuesCount = varyByRules?.RouteValues.Count ?? 0; - if (routeValuesCount > 0) + // Vary by route value names + var routeValueNamesCount = varyByRules?.RouteValueNames.Count ?? 0; + if (routeValueNamesCount > 0) { // Append a group separator for the route values segment of the cache key builder.Append(KeyDelimiter) .Append('R'); - for (var i = 0; i < routeValuesCount; i++) + for (var i = 0; i < routeValueNamesCount; i++) { // The lookup key can't be null - var routeValueName = varyByRules!.RouteValues[i] ?? string.Empty; + var routeValueName = varyByRules!.RouteValueNames[i] ?? string.Empty; - // RouteValues returns null if the key doesn't exist + // RouteValueNames returns null if the key doesn't exist var routeValueValue = context.HttpContext.Request.RouteValues[routeValueName]; builder.Append(KeyDelimiter) diff --git a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs index bf02f6e09e00..115754b621b0 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs @@ -342,17 +342,17 @@ internal void CreateCacheKey(OutputCacheContext context) } var varyHeaderNames = context.CacheVaryByRules.HeaderNames; - var varyRouteValues = context.CacheVaryByRules.RouteValues; + var varyRouteValueNames = context.CacheVaryByRules.RouteValueNames; var varyQueryKeys = context.CacheVaryByRules.QueryKeys; var varyByCustomKeys = context.CacheVaryByRules.HasVaryByCustom ? context.CacheVaryByRules.VaryByCustom : null; var varyByPrefix = context.CacheVaryByRules.VaryByPrefix; // Check if any vary rules exist - if (!StringValues.IsNullOrEmpty(varyHeaderNames) || !StringValues.IsNullOrEmpty(varyRouteValues) || !StringValues.IsNullOrEmpty(varyQueryKeys) || !StringValues.IsNullOrEmpty(varyByPrefix) || varyByCustomKeys?.Count > 0) + if (!StringValues.IsNullOrEmpty(varyHeaderNames) || !StringValues.IsNullOrEmpty(varyRouteValueNames) || !StringValues.IsNullOrEmpty(varyQueryKeys) || !StringValues.IsNullOrEmpty(varyByPrefix) || varyByCustomKeys?.Count > 0) { // Normalize order and casing of vary by rules var normalizedVaryHeaderNames = GetOrderCasingNormalizedStringValues(varyHeaderNames); - var normalizedVaryRouteValues = GetOrderCasingNormalizedStringValues(varyRouteValues); + var normalizedVaryRouteValueNames = GetOrderCasingNormalizedStringValues(varyRouteValueNames); var normalizedVaryQueryKeys = GetOrderCasingNormalizedStringValues(varyQueryKeys); var normalizedVaryByCustom = GetOrderCasingNormalizedDictionary(varyByCustomKeys); @@ -361,7 +361,7 @@ internal void CreateCacheKey(OutputCacheContext context) { VaryByPrefix = varyByPrefix + normalizedVaryByCustom, HeaderNames = normalizedVaryHeaderNames, - RouteValues = normalizedVaryRouteValues, + RouteValueNames = normalizedVaryRouteValueNames, QueryKeys = normalizedVaryQueryKeys }; @@ -369,7 +369,7 @@ internal void CreateCacheKey(OutputCacheContext context) // Always overwrite the CachedVaryByRules to update the expiry information if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.VaryByRulesUpdated(normalizedVaryHeaderNames.ToString(), normalizedVaryQueryKeys.ToString(), normalizedVaryRouteValues.ToString()); + _logger.VaryByRulesUpdated(normalizedVaryHeaderNames.ToString(), normalizedVaryQueryKeys.ToString(), normalizedVaryRouteValueNames.ToString()); } } diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByRouteValuePolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByRouteValuePolicy.cs index bf15d23b3b18..d3a3686f14a3 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByRouteValuePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByRouteValuePolicy.cs @@ -45,11 +45,11 @@ ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, Cance // No vary by route value? if (_routeValueNames.Count == 0) { - context.CacheVaryByRules.RouteValues = _routeValueNames; + context.CacheVaryByRules.RouteValueNames = _routeValueNames; return ValueTask.CompletedTask; } - context.CacheVaryByRules.RouteValues = StringValues.Concat(context.CacheVaryByRules.RouteValues, _routeValueNames); + context.CacheVaryByRules.RouteValueNames = StringValues.Concat(context.CacheVaryByRules.RouteValueNames, _routeValueNames); return ValueTask.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index 326a2b330ebc..fd1ea274618d 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -6,8 +6,8 @@ Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.HeaderNames.get -> Microsoft Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.HeaderNames.set -> void Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.QueryKeys.get -> Microsoft.Extensions.Primitives.StringValues Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.QueryKeys.set -> void -Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.RouteValues.get -> Microsoft.Extensions.Primitives.StringValues -Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.RouteValues.set -> void +Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.RouteValueNames.get -> Microsoft.Extensions.Primitives.StringValues +Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.RouteValueNames.set -> void Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.VaryByCustom.get -> System.Collections.Generic.IDictionary! Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.VaryByPrefix.get -> Microsoft.Extensions.Primitives.StringValues Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.VaryByPrefix.set -> void @@ -33,8 +33,8 @@ Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaderNames.get -> Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaderNames.init -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.get -> string![]? Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.init -> void -Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByRouteValues.get -> string![]? -Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByRouteValues.init -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByRouteValueNames.get -> string![]? +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByRouteValueNames.init -> void Microsoft.AspNetCore.OutputCaching.OutputCacheContext.EnableOutputCaching.get -> bool Microsoft.AspNetCore.OutputCaching.OutputCacheContext.EnableOutputCaching.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheContext.HttpContext.init -> void diff --git a/src/Middleware/OutputCaching/test/OutputCacheAttributeTests.cs b/src/Middleware/OutputCaching/test/OutputCacheAttributeTests.cs index 56248b7762ff..cd43d2d58a34 100644 --- a/src/Middleware/OutputCaching/test/OutputCacheAttributeTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCacheAttributeTests.cs @@ -98,13 +98,13 @@ public async Task Attribute_CreatesVaryByRoutePolicy() ["RouteB"] = 123.456, }; - var attribute = OutputCacheMethods.GetAttribute(nameof(OutputCacheMethods.VaryByRouteValues)); + var attribute = OutputCacheMethods.GetAttribute(nameof(OutputCacheMethods.VaryByRouteValueNames)); await attribute.BuildPolicy().CacheRequestAsync(context, cancellation: default); Assert.True(context.EnableOutputCaching); - Assert.Contains("RouteA", (IEnumerable)context.CacheVaryByRules.RouteValues); - Assert.Contains("RouteC", (IEnumerable)context.CacheVaryByRules.RouteValues); - Assert.DoesNotContain("RouteB", (IEnumerable)context.CacheVaryByRules.RouteValues); + Assert.Contains("RouteA", (IEnumerable)context.CacheVaryByRules.RouteValueNames); + Assert.Contains("RouteC", (IEnumerable)context.CacheVaryByRules.RouteValueNames); + Assert.DoesNotContain("RouteB", (IEnumerable)context.CacheVaryByRules.RouteValueNames); } private class OutputCacheMethods @@ -132,7 +132,7 @@ public static void VaryByHeaderNames() { } [OutputCache(VaryByQueryKeys = new[] { "QueryA", "QueryC" })] public static void VaryByQueryKeys() { } - [OutputCache(VaryByRouteValues = new[] { "RouteA", "RouteC" })] - public static void VaryByRouteValues() { } + [OutputCache(VaryByRouteValueNames = new[] { "RouteA", "RouteC" })] + public static void VaryByRouteValueNames() { } } } diff --git a/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs index 02e03e737b4c..44b92a9df25f 100644 --- a/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs @@ -84,7 +84,7 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedRouteVal context.HttpContext.Request.RouteValues["RouteB"] = "ValueB"; context.CacheVaryByRules = new CacheVaryByRules() { - RouteValues = new string[] { "RouteA", "RouteC" } + RouteValueNames = new string[] { "RouteA", "RouteC" } }; Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}R{KeyDelimiter}RouteA=ValueA{KeyDelimiter}RouteC=", @@ -99,7 +99,7 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_SerializeRouteValueToS context.HttpContext.Request.RouteValues["RouteA"] = 123.456; context.CacheVaryByRules = new CacheVaryByRules() { - RouteValues = new string[] { "RouteA", "RouteC" } + RouteValueNames = new string[] { "RouteA", "RouteC" } }; var culture = Thread.CurrentThread.CurrentCulture; @@ -248,7 +248,7 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersA VaryByPrefix = Guid.NewGuid().ToString("n"), HeaderNames = new string[] { "HeaderA", "HeaderC" }, QueryKeys = new string[] { "QueryA", "QueryC" }, - RouteValues = new string[] { "RouteA", "RouteC" }, + RouteValueNames = new string[] { "RouteA", "RouteC" }, }; Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC={KeyDelimiter}R{KeyDelimiter}RouteA=ValueA{KeyDelimiter}RouteC=", diff --git a/src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs b/src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs index a9da48cd9f43..6fbe8e193fe7 100644 --- a/src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs @@ -109,9 +109,9 @@ public async Task BuildPolicy_CreatesVaryByRoutePolicy() await policy.CacheRequestAsync(context, cancellation: default); Assert.True(context.EnableOutputCaching); - Assert.Contains("RouteA", (IEnumerable)context.CacheVaryByRules.RouteValues); - Assert.Contains("RouteC", (IEnumerable)context.CacheVaryByRules.RouteValues); - Assert.DoesNotContain("RouteB", (IEnumerable)context.CacheVaryByRules.RouteValues); + Assert.Contains("RouteA", (IEnumerable)context.CacheVaryByRules.RouteValueNames); + Assert.Contains("RouteC", (IEnumerable)context.CacheVaryByRules.RouteValueNames); + Assert.DoesNotContain("RouteB", (IEnumerable)context.CacheVaryByRules.RouteValueNames); } [Fact] diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs index 28cff873d3a4..f8fe9fce581a 100644 --- a/src/Middleware/OutputCaching/test/TestUtils.cs +++ b/src/Middleware/OutputCaching/test/TestUtils.cs @@ -311,6 +311,7 @@ internal class TestOutputCache : IOutputCacheStore private readonly Dictionary _storage = new(); public int GetCount { get; private set; } public int SetCount { get; private set; } + private readonly object synLock = new(); public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken) { @@ -321,23 +322,29 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken { ArgumentNullException.ThrowIfNull(key); - GetCount++; - try + lock (synLock) { - return ValueTask.FromResult(_storage[key]); - } - catch - { - return ValueTask.FromResult(default(byte[])); + GetCount++; + try + { + return ValueTask.FromResult(_storage[key]); + } + catch + { + return ValueTask.FromResult(default(byte[])); + } } } public ValueTask SetAsync(string key, byte[] entry, string[]? tags, TimeSpan validFor, CancellationToken cancellationToken) { - SetCount++; - _storage[key] = entry; + lock (synLock) + { + SetCount++; + _storage[key] = entry; - return ValueTask.CompletedTask; + return ValueTask.CompletedTask; + } } } From c7d718c0dd6f7ff96781c08902fb5cab0469c9b2 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Thu, 11 Aug 2022 13:59:09 -0700 Subject: [PATCH 12/12] Reuse CacheVaryBules instance --- .../OutputCaching/src/OutputCacheContext.cs | 2 +- .../src/OutputCacheMiddleware.cs | 12 ++- .../test/OutputCacheKeyProviderTests.cs | 73 +++++-------------- 3 files changed, 26 insertions(+), 61 deletions(-) diff --git a/src/Middleware/OutputCaching/src/OutputCacheContext.cs b/src/Middleware/OutputCaching/src/OutputCacheContext.cs index ef264a476709..be51451ec169 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheContext.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheContext.cs @@ -47,7 +47,7 @@ public OutputCacheContext() /// /// Gets the instance. /// - public CacheVaryByRules CacheVaryByRules { get; internal set; } = new(); + public CacheVaryByRules CacheVaryByRules { get; } = new(); /// /// Gets the tags of the cached response. diff --git a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs index 115754b621b0..78ea55b1566c 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs @@ -357,13 +357,11 @@ internal void CreateCacheKey(OutputCacheContext context) var normalizedVaryByCustom = GetOrderCasingNormalizedDictionary(varyByCustomKeys); // Update vary rules with normalized values - context.CacheVaryByRules = new CacheVaryByRules - { - VaryByPrefix = varyByPrefix + normalizedVaryByCustom, - HeaderNames = normalizedVaryHeaderNames, - RouteValueNames = normalizedVaryRouteValueNames, - QueryKeys = normalizedVaryQueryKeys - }; + context.CacheVaryByRules.VaryByCustom.Clear(); + context.CacheVaryByRules.VaryByPrefix = varyByPrefix + normalizedVaryByCustom; + context.CacheVaryByRules.HeaderNames = normalizedVaryHeaderNames; + context.CacheVaryByRules.RouteValueNames = normalizedVaryRouteValueNames; + context.CacheVaryByRules.QueryKeys = normalizedVaryQueryKeys; // TODO: Add same condition on LogLevel in Response Caching // Always overwrite the CachedVaryByRules to update the expiry information diff --git a/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs index 44b92a9df25f..92809d018fa4 100644 --- a/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs @@ -67,10 +67,7 @@ public void OutputCachingKeyProvider_CreateStorageKey_ReturnsCachedVaryByGuid_If { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); - context.CacheVaryByRules = new CacheVaryByRules() - { - VaryByPrefix = Guid.NewGuid().ToString("n"), - }; + context.CacheVaryByRules.VaryByPrefix = Guid.NewGuid().ToString("n"); Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}", cacheKeyProvider.CreateStorageKey(context)); } @@ -82,10 +79,7 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedRouteVal var context = TestUtils.CreateTestContext(); context.HttpContext.Request.RouteValues["RouteA"] = "ValueA"; context.HttpContext.Request.RouteValues["RouteB"] = "ValueB"; - context.CacheVaryByRules = new CacheVaryByRules() - { - RouteValueNames = new string[] { "RouteA", "RouteC" } - }; + context.CacheVaryByRules.RouteValueNames = new string[] { "RouteA", "RouteC" }; Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}R{KeyDelimiter}RouteA=ValueA{KeyDelimiter}RouteC=", cacheKeyProvider.CreateStorageKey(context)); @@ -97,10 +91,7 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_SerializeRouteValueToS var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.RouteValues["RouteA"] = 123.456; - context.CacheVaryByRules = new CacheVaryByRules() - { - RouteValueNames = new string[] { "RouteA", "RouteC" } - }; + context.CacheVaryByRules.RouteValueNames = new string[] { "RouteA", "RouteC" }; var culture = Thread.CurrentThread.CurrentCulture; try @@ -122,10 +113,7 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersO var context = TestUtils.CreateTestContext(); context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; - context.CacheVaryByRules = new CacheVaryByRules() - { - HeaderNames = new string[] { "HeaderA", "HeaderC" } - }; + context.CacheVaryByRules.HeaderNames = new string[] { "HeaderA", "HeaderC" }; Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=", cacheKeyProvider.CreateStorageKey(context)); @@ -138,10 +126,7 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_HeaderValuesAreSorted( var context = TestUtils.CreateTestContext(); context.HttpContext.Request.Headers["HeaderA"] = "ValueB"; context.HttpContext.Request.Headers.Append("HeaderA", "ValueA"); - context.CacheVaryByRules = new CacheVaryByRules() - { - HeaderNames = new string[] { "HeaderA", "HeaderC" } - }; + context.CacheVaryByRules.HeaderNames = new string[] { "HeaderA", "HeaderC" }; Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueAValueB{KeyDelimiter}HeaderC=", cacheKeyProvider.CreateStorageKey(context)); @@ -153,11 +138,8 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedQueryKey var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); - context.CacheVaryByRules = new CacheVaryByRules() - { - VaryByPrefix = Guid.NewGuid().ToString("n"), - QueryKeys = new string[] { "QueryA", "QueryC" } - }; + context.CacheVaryByRules.VaryByPrefix = Guid.NewGuid().ToString("n"); + context.CacheVaryByRules.QueryKeys = new string[] { "QueryA", "QueryC" }; Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", cacheKeyProvider.CreateStorageKey(context)); @@ -169,11 +151,8 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesQueryKeys_Quer var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.QueryString = new QueryString("?queryA=ValueA&queryB=ValueB"); - context.CacheVaryByRules = new CacheVaryByRules() - { - VaryByPrefix = Guid.NewGuid().ToString("n"), - QueryKeys = new string[] { "QueryA", "QueryC" } - }; + context.CacheVaryByRules.VaryByPrefix = Guid.NewGuid().ToString("n"); + context.CacheVaryByRules.QueryKeys = new string[] { "QueryA", "QueryC" }; Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", cacheKeyProvider.CreateStorageKey(context)); @@ -185,11 +164,8 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesAllQueryKeysGi var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); - context.CacheVaryByRules = new CacheVaryByRules() - { - VaryByPrefix = Guid.NewGuid().ToString("n"), - QueryKeys = new string[] { "*" } - }; + context.CacheVaryByRules.VaryByPrefix = Guid.NewGuid().ToString("n"); + context.CacheVaryByRules.QueryKeys = new string[] { "*" }; // To support case insensitivity, all query keys are converted to upper case. // Explicit query keys uses the casing specified in the setting. @@ -203,12 +179,9 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesNotCons var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryA=ValueB"); - context.CacheVaryByRules = new CacheVaryByRules() - { - VaryByPrefix = Guid.NewGuid().ToString("n"), - QueryKeys = new string[] { "*" } - }; - + context.CacheVaryByRules.VaryByPrefix = Guid.NewGuid().ToString("n"); + context.CacheVaryByRules.QueryKeys = new string[] { "*" }; + // To support case insensitivity, all query keys are converted to upper case. // Explicit query keys uses the casing specified in the setting. Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB", @@ -221,11 +194,8 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesAreSort var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueB&QueryA=ValueA"); - context.CacheVaryByRules = new CacheVaryByRules() - { - VaryByPrefix = Guid.NewGuid().ToString("n"), - QueryKeys = new string[] { "*" } - }; + context.CacheVaryByRules.VaryByPrefix = Guid.NewGuid().ToString("n"); + context.CacheVaryByRules.QueryKeys = new string[] { "*" }; // To support case insensitivity, all query keys are converted to upper case. // Explicit query keys uses the casing specified in the setting. @@ -243,13 +213,10 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersA context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); context.HttpContext.Request.RouteValues["RouteA"] = "ValueA"; context.HttpContext.Request.RouteValues["RouteB"] = "ValueB"; - context.CacheVaryByRules = new CacheVaryByRules() - { - VaryByPrefix = Guid.NewGuid().ToString("n"), - HeaderNames = new string[] { "HeaderA", "HeaderC" }, - QueryKeys = new string[] { "QueryA", "QueryC" }, - RouteValueNames = new string[] { "RouteA", "RouteC" }, - }; + context.CacheVaryByRules.VaryByPrefix = Guid.NewGuid().ToString("n"); + context.CacheVaryByRules.HeaderNames = new string[] { "HeaderA", "HeaderC" }; + context.CacheVaryByRules.QueryKeys = new string[] { "QueryA", "QueryC" }; + context.CacheVaryByRules.RouteValueNames = new string[] { "RouteA", "RouteC" }; Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC={KeyDelimiter}R{KeyDelimiter}RouteA=ValueA{KeyDelimiter}RouteC=", cacheKeyProvider.CreateStorageKey(context));