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;