diff --git a/src/Middleware/OutputCaching/src/DefaultOutputCachePolicyProvider.cs b/src/Middleware/OutputCaching/src/DefaultOutputCachePolicyProvider.cs new file mode 100644 index 000000000000..9cdeb01637a1 --- /dev/null +++ b/src/Middleware/OutputCaching/src/DefaultOutputCachePolicyProvider.cs @@ -0,0 +1,47 @@ +// 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.Options; + +namespace Microsoft.AspNetCore.OutputCaching; + +internal sealed class DefaultOutputCachePolicyProvider : IOutputCachePolicyProvider +{ + private readonly OutputCacheOptions _options; + + /// + /// Creates a new instance of . + /// + /// The options configured for the application. + public DefaultOutputCachePolicyProvider(IOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + _options = options.Value; + } + + /// + public IReadOnlyList GetBasePolicies() + { + if (_options.BasePolicies is not null && _options.BasePolicies.Count > 0) + { + return _options.BasePolicies; + } + return Array.Empty(); + } + + /// + public ValueTask GetPolicyAsync(string policyName) + { + ArgumentNullException.ThrowIfNull(policyName); + + IOutputCachePolicy? policy = null; + + if (_options.NamedPolicies is not null && _options.NamedPolicies.TryGetValue(policyName, out var value)) + { + policy = value; + } + + return ValueTask.FromResult(policy); + } +} diff --git a/src/Middleware/OutputCaching/src/IOutputCachePolicyProvider.cs b/src/Middleware/OutputCaching/src/IOutputCachePolicyProvider.cs new file mode 100644 index 000000000000..a9c459158fe4 --- /dev/null +++ b/src/Middleware/OutputCaching/src/IOutputCachePolicyProvider.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// A provider to get output cache policies for a given request. +/// +public interface IOutputCachePolicyProvider +{ + /// + /// Gets the base output cache policies. + /// + /// A readonly list of instances. + IReadOnlyList GetBasePolicies(); + + /// + /// Gets a from the given . + /// + /// The policy name to retrieve. + /// The named . + ValueTask GetPolicyAsync(string policyName); +} diff --git a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs index 1296da58ecd5..1289fcc467ed 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs @@ -24,6 +24,7 @@ internal sealed class OutputCacheMiddleware private readonly ILogger _logger; private readonly IOutputCacheStore _store; private readonly IOutputCacheKeyProvider _keyProvider; + private readonly IOutputCachePolicyProvider _policyProvider; private readonly WorkDispatcher _outputCacheEntryDispatcher; private readonly WorkDispatcher _requestDispatcher; @@ -35,18 +36,21 @@ internal sealed class OutputCacheMiddleware /// The used for logging. /// The store. /// The used for creating instances. + /// The used to resolve cache policies. public OutputCacheMiddleware( RequestDelegate next, IOptions options, ILoggerFactory loggerFactory, IOutputCacheStore outputCache, - ObjectPoolProvider poolProvider + ObjectPoolProvider poolProvider, + IOutputCachePolicyProvider policyProvider ) : this( next, options, loggerFactory, outputCache, + policyProvider, new OutputCacheKeyProvider(poolProvider, options)) { } @@ -56,18 +60,21 @@ internal OutputCacheMiddleware( IOptions options, ILoggerFactory loggerFactory, IOutputCacheStore cache, + IOutputCachePolicyProvider policyProvider, IOutputCacheKeyProvider keyProvider) { ArgumentNullException.ThrowIfNull(next); ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(loggerFactory); ArgumentNullException.ThrowIfNull(cache); + ArgumentNullException.ThrowIfNull(policyProvider); ArgumentNullException.ThrowIfNull(keyProvider); _next = next; _options = options.Value; _logger = loggerFactory.CreateLogger(); _store = cache; + _policyProvider = policyProvider; _keyProvider = keyProvider; _outputCacheEntryDispatcher = new(); _requestDispatcher = new(); @@ -217,10 +224,11 @@ internal bool TryGetRequestPolicies(HttpContext httpContext, out IReadOnlyList(); List? result = null; - if (_options.BasePolicies != null) + var basePolicies = _policyProvider.GetBasePolicies(); + if (basePolicies.Count > 0) { result = new(); - result.AddRange(_options.BasePolicies); + result.AddRange(basePolicies); } var metadata = httpContext.GetEndpoint()?.Metadata; diff --git a/src/Middleware/OutputCaching/src/OutputCacheServiceCollectionExtensions.cs b/src/Middleware/OutputCaching/src/OutputCacheServiceCollectionExtensions.cs index 86267be94f29..9b94e47c84a4 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheServiceCollectionExtensions.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheServiceCollectionExtensions.cs @@ -27,6 +27,7 @@ public static IServiceCollection AddOutputCache(this IServiceCollection services services.AddTransient, OutputCacheOptionsSetup>(); services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(sp => { diff --git a/src/Middleware/OutputCaching/src/Policies/NamedPolicy.cs b/src/Middleware/OutputCaching/src/Policies/NamedPolicy.cs index 55672c3005a1..b887b6f53204 100644 --- a/src/Middleware/OutputCaching/src/Policies/NamedPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NamedPolicy.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.OutputCaching; @@ -12,9 +11,6 @@ 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. /// @@ -25,58 +21,52 @@ public NamedPolicy(string policyName) } /// - ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) + async ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) { - var policy = GetProfilePolicy(context); + var policy = await GetProfilePolicy(context); if (policy == null) { - return ValueTask.CompletedTask; + return; } - return policy.ServeResponseAsync(context, cancellationToken); + await policy.ServeResponseAsync(context, cancellationToken); } /// - ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) + async ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) { - var policy = GetProfilePolicy(context); + var policy = await GetProfilePolicy(context); if (policy == null) { - return ValueTask.CompletedTask; + return; } - return policy.ServeFromCacheAsync(context, cancellationToken); + await policy.ServeFromCacheAsync(context, cancellationToken); } /// - ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) + async ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) { - var policy = GetProfilePolicy(context); + var policy = await GetProfilePolicy(context); if (policy == null) { - return ValueTask.CompletedTask; + return; } - return policy.CacheRequestAsync(context, cancellationToken); + await policy.CacheRequestAsync(context, cancellationToken); } - internal IOutputCachePolicy? GetProfilePolicy(OutputCacheContext context) + internal ValueTask GetProfilePolicy(OutputCacheContext context) { - if (_options == null) + var provider = context.HttpContext.RequestServices.GetRequiredService(); + if (provider == null) { - lock (_synLock) - { - _options ??= context.HttpContext.RequestServices.GetRequiredService>(); - } + return ValueTask.FromResult(null); } - var policies = _options!.Value.NamedPolicies; - - return policies != null && policies.TryGetValue(_policyName, out var cacheProfile) - ? cacheProfile - : null; + return provider.GetPolicyAsync(_policyName); } } diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..c21ad98c2cad 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +Microsoft.AspNetCore.OutputCaching.IOutputCachePolicyProvider +Microsoft.AspNetCore.OutputCaching.IOutputCachePolicyProvider.GetBasePolicies() -> System.Collections.Generic.IReadOnlyList! +Microsoft.AspNetCore.OutputCaching.IOutputCachePolicyProvider.GetPolicyAsync(string! policyName) -> System.Threading.Tasks.ValueTask diff --git a/src/Middleware/OutputCaching/test/DefaultOutputCachePolicyProviderTests.cs b/src/Middleware/OutputCaching/test/DefaultOutputCachePolicyProviderTests.cs new file mode 100644 index 000000000000..261275889cde --- /dev/null +++ b/src/Middleware/OutputCaching/test/DefaultOutputCachePolicyProviderTests.cs @@ -0,0 +1,212 @@ +// 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.OutputCaching; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.OutputCaching.Tests; + +public class DefaultOutputCachePolicyProviderTests +{ + [Fact] + public void GetBasePolicies_WithBasePolicies_ReturnsConfiguredPolicies() + { + // Arrange + var options = new OutputCacheOptions(); + var policy1 = new OutputCachePolicyBuilder().Expire(TimeSpan.FromMinutes(5)).Build(); + var policy2 = new OutputCachePolicyBuilder().Expire(TimeSpan.FromMinutes(10)).Build(); + + options.AddBasePolicy(policy1); + options.AddBasePolicy(policy2); + + var outputCacheOptions = Options.Create(options); + var policyProvider = new DefaultOutputCachePolicyProvider(outputCacheOptions); + + // Act + var actualPolicies = policyProvider.GetBasePolicies(); + + // Assert + Assert.Equal(2, actualPolicies.Count); + Assert.Same(policy1, actualPolicies[0]); + Assert.Same(policy2, actualPolicies[1]); + } + + [Fact] + public void GetBasePolicies_WithoutBasePolicies_ReturnsEmptyList() + { + // Arrange + var options = new OutputCacheOptions(); + var outputCacheOptions = Options.Create(options); + var policyProvider = new DefaultOutputCachePolicyProvider(outputCacheOptions); + + // Act + var actualPolicies = policyProvider.GetBasePolicies(); + + // Assert + Assert.NotNull(actualPolicies); + Assert.Empty(actualPolicies); + } + + [Fact] + public async Task GetPolicyAsync_WithNamedPolicy_ReturnsPolicy() + { + // Arrange + var options = new OutputCacheOptions(); + var policy = new OutputCachePolicyBuilder().Expire(TimeSpan.FromMinutes(15)).Build(); + options.AddPolicy("TestPolicy", policy); + + var outputCacheOptions = Options.Create(options); + var policyProvider = new DefaultOutputCachePolicyProvider(outputCacheOptions); + + // Act + var actualPolicy = await policyProvider.GetPolicyAsync("TestPolicy"); + + // Assert + Assert.NotNull(actualPolicy); + Assert.Same(policy, actualPolicy); + } + + [Theory] + [InlineData("")] + [InlineData("PolicyName")] + [InlineData("AnotherPolicy")] + public async Task GetPolicyAsync_GetsNamedPolicy(string policyName) + { + // Arrange + var options = new OutputCacheOptions(); + var policy = new OutputCachePolicyBuilder().Expire(TimeSpan.FromSeconds(30)).Build(); + options.AddPolicy(policyName, policy); + + var outputCacheOptions = Options.Create(options); + var policyProvider = new DefaultOutputCachePolicyProvider(outputCacheOptions); + + // Act + var actualPolicy = await policyProvider.GetPolicyAsync(policyName); + + // Assert + Assert.NotNull(actualPolicy); + Assert.Same(policy, actualPolicy); + } + + [Fact] + public async Task GetPolicyAsync_WithNonExistentPolicy_ReturnsNull() + { + // Arrange + var options = new OutputCacheOptions(); + var outputCacheOptions = Options.Create(options); + var policyProvider = new DefaultOutputCachePolicyProvider(outputCacheOptions); + + // Act + var actualPolicy = await policyProvider.GetPolicyAsync("NonExistentPolicy"); + + // Assert + Assert.Null(actualPolicy); + } + + [Fact] + public async Task GetPolicyAsync_WithMultipleNamedPolicies_ReturnsCorrectPolicy() + { + // Arrange + var options = new OutputCacheOptions(); + var policy1 = new OutputCachePolicyBuilder().Expire(TimeSpan.FromMinutes(5)).Build(); + var policy2 = new OutputCachePolicyBuilder().Expire(TimeSpan.FromMinutes(10)).Build(); + var policy3 = new OutputCachePolicyBuilder().Expire(TimeSpan.FromMinutes(15)).Build(); + + options.AddPolicy("Fast", policy1); + options.AddPolicy("Medium", policy2); + options.AddPolicy("Slow", policy3); + + var outputCacheOptions = Options.Create(options); + var policyProvider = new DefaultOutputCachePolicyProvider(outputCacheOptions); + + // Act + var actualPolicy1 = await policyProvider.GetPolicyAsync("Fast"); + var actualPolicy2 = await policyProvider.GetPolicyAsync("Medium"); + var actualPolicy3 = await policyProvider.GetPolicyAsync("Slow"); + + // Assert + Assert.Same(policy1, actualPolicy1); + Assert.Same(policy2, actualPolicy2); + Assert.Same(policy3, actualPolicy3); + } + + [Fact] + public async Task GetPolicyAsync_CaseInsensitivePolicyName() + { + // Arrange + var options = new OutputCacheOptions(); + var policy = new OutputCachePolicyBuilder().Expire(TimeSpan.FromMinutes(20)).Build(); + options.AddPolicy("TestPolicy", policy); + + var outputCacheOptions = Options.Create(options); + var policyProvider = new DefaultOutputCachePolicyProvider(outputCacheOptions); + + // Act + var actualPolicyLower = await policyProvider.GetPolicyAsync("testpolicy"); + var actualPolicyUpper = await policyProvider.GetPolicyAsync("TESTPOLICY"); + var actualPolicyMixed = await policyProvider.GetPolicyAsync("TeStPoLiCy"); + + // Assert + Assert.Same(policy, actualPolicyLower); + Assert.Same(policy, actualPolicyUpper); + Assert.Same(policy, actualPolicyMixed); + } + + [Fact] + public void GetBasePolicies_WithBuilderAction_ReturnsPolicy() + { + // Arrange + var options = new OutputCacheOptions(); + options.AddBasePolicy(builder => builder.Expire(TimeSpan.FromMinutes(30))); + + var outputCacheOptions = Options.Create(options); + var policyProvider = new DefaultOutputCachePolicyProvider(outputCacheOptions); + + // Act + var actualPolicies = policyProvider.GetBasePolicies(); + + // Assert + Assert.Single(actualPolicies); + Assert.NotNull(actualPolicies[0]); + } + + [Fact] + public async Task GetPolicyAsync_WithBuilderAction_ReturnsPolicy() + { + // Arrange + var options = new OutputCacheOptions(); + options.AddPolicy("BuilderPolicy", builder => builder + .Expire(TimeSpan.FromMinutes(45)) + .SetVaryByQuery("version")); + + var outputCacheOptions = Options.Create(options); + var policyProvider = new DefaultOutputCachePolicyProvider(outputCacheOptions); + + // Act + var actualPolicy = await policyProvider.GetPolicyAsync("BuilderPolicy"); + + // Assert + Assert.NotNull(actualPolicy); + } + + [Fact] + public void Constructor_WithNullOptions_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => new DefaultOutputCachePolicyProvider(null!)); + } + + [Fact] + public async Task GetPolicyAsync_WithNullPolicyName_ThrowsArgumentNullException() + { + // Arrange + var options = new OutputCacheOptions(); + var outputCacheOptions = Options.Create(options); + var policyProvider = new DefaultOutputCachePolicyProvider(outputCacheOptions); + + // Act & Assert + await Assert.ThrowsAsync( + async () => await policyProvider.GetPolicyAsync(null!)); + } +} + diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs index ddaca73bd01b..d6446ff14259 100644 --- a/src/Middleware/OutputCaching/test/TestUtils.cs +++ b/src/Middleware/OutputCaching/test/TestUtils.cs @@ -191,7 +191,8 @@ internal static OutputCacheMiddleware CreateTestMiddleware( IOutputCacheStore? cache = null, OutputCacheOptions? options = null, TestSink? testSink = null, - IOutputCacheKeyProvider? keyProvider = null + IOutputCacheKeyProvider? keyProvider = null, + IOutputCachePolicyProvider? policyProvider = null ) { if (next == null) @@ -210,21 +211,28 @@ internal static OutputCacheMiddleware CreateTestMiddleware( { keyProvider = new OutputCacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); } + if (policyProvider == null) + { + policyProvider = new DefaultOutputCachePolicyProvider(Options.Create(options)); + } return new OutputCacheMiddleware( next, Options.Create(options), testSink == null ? NullLoggerFactory.Instance : new TestLoggerFactory(testSink, true), cache, + policyProvider, keyProvider); } internal static OutputCacheContext CreateTestContext(HttpContext? httpContext = null, IOutputCacheStore? cache = null, OutputCacheOptions? options = null, ITestSink? testSink = null) { + var actualOptions = options ?? new OutputCacheOptions(); var serviceProvider = new Mock(); serviceProvider.Setup(x => x.GetService(typeof(IOutputCacheStore))).Returns(cache ?? new SimpleTestOutputCache()); - serviceProvider.Setup(x => x.GetService(typeof(IOptions))).Returns(Options.Create(options ?? new OutputCacheOptions())); + serviceProvider.Setup(x => x.GetService(typeof(IOptions))).Returns(Options.Create(actualOptions)); serviceProvider.Setup(x => x.GetService(typeof(ILogger))).Returns(testSink == null ? NullLogger.Instance : new TestLogger("OutputCachingTests", testSink, true)); + serviceProvider.Setup(x => x.GetService(typeof(IOutputCachePolicyProvider))).Returns(new DefaultOutputCachePolicyProvider(Options.Create(actualOptions))); httpContext ??= new DefaultHttpContext(); httpContext.RequestServices = serviceProvider.Object; @@ -241,10 +249,12 @@ internal static OutputCacheContext CreateTestContext(HttpContext? httpContext = internal static OutputCacheContext CreateUninitializedContext(HttpContext? httpContext = null, IOutputCacheStore? cache = null, OutputCacheOptions? options = null, ITestSink? testSink = null) { + var actualOptions = options ?? new OutputCacheOptions(); var serviceProvider = new Mock(); serviceProvider.Setup(x => x.GetService(typeof(IOutputCacheStore))).Returns(cache ?? new SimpleTestOutputCache()); - serviceProvider.Setup(x => x.GetService(typeof(IOptions))).Returns(Options.Create(options ?? new OutputCacheOptions())); + serviceProvider.Setup(x => x.GetService(typeof(IOptions))).Returns(Options.Create(actualOptions)); serviceProvider.Setup(x => x.GetService(typeof(ILogger))).Returns(testSink == null ? NullLogger.Instance : new TestLogger("OutputCachingTests", testSink, true)); + serviceProvider.Setup(x => x.GetService(typeof(IOutputCachePolicyProvider))).Returns(new DefaultOutputCachePolicyProvider(Options.Create(actualOptions))); httpContext ??= new DefaultHttpContext(); httpContext.RequestServices = serviceProvider.Object;