diff --git a/src/Polly/Caching/AsyncCachePolicy.cs b/src/Polly/Caching/AsyncCachePolicy.cs
index bfa7bc9335c..89e60c6bed8 100644
--- a/src/Polly/Caching/AsyncCachePolicy.cs
+++ b/src/Polly/Caching/AsyncCachePolicy.cs
@@ -4,7 +4,6 @@ namespace Polly.Caching;
///
/// A cache policy that can be applied to the results of delegate executions.
///
-#pragma warning disable CA1062 // Validate arguments of public methods
public class AsyncCachePolicy : AsyncPolicy
{
private readonly IAsyncCacheProvider _asyncCacheProvider;
@@ -43,13 +42,31 @@ protected override Task ImplementationAsync(
Func action,
Context context,
CancellationToken cancellationToken,
- bool continueOnCapturedContext) => action(context, cancellationToken); // Pass-through/NOOP policy action, for void-returning executions through the cache policy.
+ bool continueOnCapturedContext)
+ {
+ if (action is null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ // Pass-through/NOOP policy action, for void-returning executions through the cache policy.
+ return action(context, cancellationToken);
+ }
///
[DebuggerStepThrough]
- protected override Task ImplementationAsync(Func> action, Context context, CancellationToken cancellationToken,
- bool continueOnCapturedContext) =>
- AsyncCacheEngine.ImplementationAsync(
+ protected override Task ImplementationAsync(
+ Func> action,
+ Context context,
+ CancellationToken cancellationToken,
+ bool continueOnCapturedContext)
+ {
+ if (action is null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ return AsyncCacheEngine.ImplementationAsync(
_asyncCacheProvider.AsyncFor(),
_ttlStrategy.For(),
_cacheKeyStrategy,
@@ -62,6 +79,7 @@ protected override Task ImplementationAsync(Func
@@ -104,9 +122,18 @@ internal AsyncCachePolicy(
///
[DebuggerStepThrough]
- protected override Task ImplementationAsync(Func> action, Context context, CancellationToken cancellationToken,
- bool continueOnCapturedContext) =>
- AsyncCacheEngine.ImplementationAsync(
+ protected override Task ImplementationAsync(
+ Func> action,
+ Context context,
+ CancellationToken cancellationToken,
+ bool continueOnCapturedContext)
+ {
+ if (action is null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ return AsyncCacheEngine.ImplementationAsync(
_asyncCacheProvider,
_ttlStrategy,
_cacheKeyStrategy,
@@ -119,5 +146,6 @@ protected override Task ImplementationAsync(Func> action = null!;
+ Func actionVoid = null!;
+
+ IAsyncCacheProvider asyncCacheProvider = new StubCacheProvider();
+ ITtlStrategy ttlStrategy = new ContextualTtl();
+ Func cacheKeyStrategy = (_) => string.Empty;
+ Action onCacheGet = (_, _) => { };
+ Action onCacheMiss = (_, _) => { };
+ Action onCachePut = (_, _) => { };
+ Action? onCacheGetError = null;
+ Action? onCachePutError = null;
+
+ var instance = Activator.CreateInstance(
+ typeof(AsyncCachePolicy),
+ flags,
+ null,
+ [
+ asyncCacheProvider,
+ ttlStrategy,
+ cacheKeyStrategy,
+ onCacheGet,
+ onCacheMiss,
+ onCachePut,
+ onCacheGetError,
+ onCachePutError,
+ ],
+ null)!;
+ var instanceType = instance.GetType();
+ var methods = instanceType.GetMethods(flags);
+ var methodInfo = methods.First(method => method is { Name: "ImplementationAsync", ReturnType.Name: "Task`1" });
+ var generic = methodInfo.MakeGenericMethod(typeof(EmptyStruct));
+
+ var func = () => generic.Invoke(instance, [action, new Context(), CancellationToken.None, false]);
+
+ var exceptionAssertions = func.Should().Throw();
+ exceptionAssertions.And.Message.Should().Be("Exception has been thrown by the target of an invocation.");
+ exceptionAssertions.And.InnerException.Should().BeOfType()
+ .Which.ParamName.Should().Be("action");
+
+ methodInfo = methods.First(method => method is { Name: "ImplementationAsync", ReturnType.Name: "Task" });
+
+ func = () => methodInfo.Invoke(instance, [actionVoid, new Context(), CancellationToken.None, false]);
+
+ exceptionAssertions = func.Should().Throw();
+ exceptionAssertions.And.Message.Should().Be("Exception has been thrown by the target of an invocation.");
+ exceptionAssertions.And.InnerException.Should().BeOfType()
+ .Which.ParamName.Should().Be("action");
+ }
+
[Fact]
public void Should_throw_when_cache_provider_is_null()
{
diff --git a/test/Polly.Specs/Caching/CacheTResultAsyncSpecs.cs b/test/Polly.Specs/Caching/CacheTResultAsyncSpecs.cs
index 8b4b9874d98..8f5d58464fd 100644
--- a/test/Polly.Specs/Caching/CacheTResultAsyncSpecs.cs
+++ b/test/Polly.Specs/Caching/CacheTResultAsyncSpecs.cs
@@ -5,6 +5,48 @@ public class CacheTResultAsyncSpecs : IDisposable
{
#region Configuration
+ [Fact]
+ public void Should_throw_when_action_is_null()
+ {
+ var flags = BindingFlags.NonPublic | BindingFlags.Instance;
+ Func> action = null!;
+
+ IAsyncCacheProvider asyncCacheProvider = new StubCacheProvider().AsyncFor();
+ ITtlStrategy ttlStrategy = new ContextualTtl().For();
+ Func cacheKeyStrategy = (_) => string.Empty;
+ Action onCacheGet = (_, _) => { };
+ Action onCacheMiss = (_, _) => { };
+ Action onCachePut = (_, _) => { };
+ Action? onCacheGetError = null;
+ Action? onCachePutError = null;
+
+ var instance = Activator.CreateInstance(
+ typeof(AsyncCachePolicy),
+ flags,
+ null,
+ [
+ asyncCacheProvider,
+ ttlStrategy,
+ cacheKeyStrategy,
+ onCacheGet,
+ onCacheMiss,
+ onCachePut,
+ onCacheGetError,
+ onCachePutError,
+ ],
+ null)!;
+ var instanceType = instance.GetType();
+ var methods = instanceType.GetMethods(flags);
+ var methodInfo = methods.First(method => method is { Name: "ImplementationAsync", ReturnType.Name: "Task`1" });
+
+ var func = () => methodInfo.Invoke(instance, [action, new Context(), CancellationToken.None, false]);
+
+ var exceptionAssertions = func.Should().Throw();
+ exceptionAssertions.And.Message.Should().Be("Exception has been thrown by the target of an invocation.");
+ exceptionAssertions.And.InnerException.Should().BeOfType()
+ .Which.ParamName.Should().Be("action");
+ }
+
[Fact]
public void Should_throw_when_cache_provider_is_null()
{