Repository: App-vNext/Polly

Issue:
[Feature request]: Add AddStrategy API with no options
### Is your feature request related to a specific problem? Or an existing feature?

The `AddStrategy` extension methods take a `ResilienceStrategyOptions options` argument, which gets validated using DataAnnotations. This makes the method incompatible with trimming and native AOT, and it is annotated as such:

https://github.com/App-vNext/Polly/blob/82474bf10b5097d4bf6934100c9daa1efdb02f4b/src/Polly.Core/ResiliencePipelineBuilderExtensions.cs#L68-L70

https://github.com/App-vNext/Polly/blob/82474bf10b5097d4bf6934100c9daa1efdb02f4b/src/Polly.Core/ResiliencePipelineBuilderExtensions.cs#L90-L93

https://github.com/App-vNext/Polly/blob/82474bf10b5097d4bf6934100c9daa1efdb02f4b/src/Polly.Core/ResiliencePipelineBuilderExtensions.cs#L114-L117

Callers who want to use this API in a trim compatible way need to define an empty class derived from `ResilienceStrategyOptions` and suppress the warning from calling this API.

### Describe the solution you'd like

We should add overloads to these `AddStrategy` methods that are trim compatible, and don't take an `options` argument. Similar to the `AddPipeline` methods.

```diff
 [RequiresUnreferencedCode(Constants.OptionsValidation)] 
 public static TBuilder AddStrategy<TBuilder>(this TBuilder builder, Func<StrategyBuilderContext, ResilienceStrategy> factory, ResilienceStrategyOptions options) 
     where TBuilder : ResiliencePipelineBuilderBase  {}

+public static TBuilder AddStrategy<TBuilder>(this TBuilder builder, Func<StrategyBuilderContext, ResilienceStrategy> factory) 
+    where TBuilder : ResiliencePipelineBuilderBase  {}

 [RequiresUnreferencedCode(Constants.OptionsValidation)] 
 public static ResiliencePipelineBuilder AddStrategy( 
     this ResiliencePipelineBuilder builder, Func<StrategyBuilderContext, ResilienceStrategy<object>> factory, 
     ResilienceStrategyOptions options) {}

+public static ResiliencePipelineBuilder AddStrategy(
+    this ResiliencePipelineBuilder builder, Func<StrategyBuilderContext, ResilienceStrategy<object>> factory) {}

 [RequiresUnreferencedCode(Constants.OptionsValidation)] 
 public static ResiliencePipelineBuilder<TResult> AddStrategy<TResult>( 
     this ResiliencePipelineBuilder<TResult> builder, Func<StrategyBuilderContext, ResilienceStrategy<TResult>> factory, 
     ResilienceStrategyOptions options) {}

+ public static ResiliencePipelineBuilder<TResult> AddStrategy<TResult>(
+    this ResiliencePipelineBuilder<TResult> builder, Func<StrategyBuilderContext, ResilienceStrategy<TResult>> factory) {}
```

### Additional context

This is being used by Microsoft.Extensions.Http.Resilience here:
https://github.com/dotnet/extensions/pull/4962#discussion_r1497965809

Adding this API would make the caller simpler. It wouldn't need to declare an empty class, and suppress the trim warning.


Hints:
Seems reasonable to me, plus would make the API easier to use even without wanting to use AoT.
This seems to be actually a bug, at least in the documentation. The `options` parameter in the `ResiliencePipelineBuilderExtensions.AddStrategy` methods is described like this:

> The options associated with the strategy. If none are provided the default instance of ResilienceStrategyOptions is created.

So one would expect the parameter already to be optional. There is the ´EmptyOptions´ class, but it's internal, so I need to copy it if I don't need any options for my strategy.

Patch:
diff --git a/src/Polly.Core/PublicAPI.Unshipped.txt b/src/Polly.Core/PublicAPI.Unshipped.txt
index ab058de62d4..e49c0f73fd7 100644
--- a/src/Polly.Core/PublicAPI.Unshipped.txt
+++ b/src/Polly.Core/PublicAPI.Unshipped.txt
@@ -1,1 +1,4 @@
 ﻿#nullable enable
+static Polly.ResiliencePipelineBuilderExtensions.AddStrategy(this Polly.ResiliencePipelineBuilder! builder, System.Func<Polly.StrategyBuilderContext!, Polly.ResilienceStrategy<object!>!>! factory) -> Polly.ResiliencePipelineBuilder!
+static Polly.ResiliencePipelineBuilderExtensions.AddStrategy<TBuilder>(this TBuilder! builder, System.Func<Polly.StrategyBuilderContext!, Polly.ResilienceStrategy!>! factory) -> TBuilder!
+static Polly.ResiliencePipelineBuilderExtensions.AddStrategy<TResult>(this Polly.ResiliencePipelineBuilder<TResult>! builder, System.Func<Polly.StrategyBuilderContext!, Polly.ResilienceStrategy<TResult>!>! factory) -> Polly.ResiliencePipelineBuilder<TResult>!
diff --git a/src/Polly.Core/ResiliencePipelineBuilderExtensions.cs b/src/Polly.Core/ResiliencePipelineBuilderExtensions.cs
index 7b450ccabb9..0b5a4b06160 100644
--- a/src/Polly.Core/ResiliencePipelineBuilderExtensions.cs
+++ b/src/Polly.Core/ResiliencePipelineBuilderExtensions.cs
@@ -60,13 +60,16 @@ public static ResiliencePipelineBuilder<TResult> AddPipeline<TResult>(this Resil
     /// <typeparam name="TBuilder">The builder type.</typeparam>
     /// <param name="builder">The builder instance.</param>
     /// <param name="factory">The factory that creates a resilience strategy.</param>
-    /// <param name="options">The options associated with the strategy. If none are provided the default instance of <see cref="ResilienceStrategyOptions"/> is created.</param>
+    /// <param name="options">The options associated with the strategy.</param>
     /// <returns>The same builder instance.</returns>
     /// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="factory"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
     /// <exception cref="InvalidOperationException">Thrown when this builder was already used to create a pipeline. The builder cannot be modified after it has been used.</exception>
     /// <exception cref="ValidationException">Thrown when <paramref name="options"/> is invalid.</exception>
     [RequiresUnreferencedCode(Constants.OptionsValidation)]
-    public static TBuilder AddStrategy<TBuilder>(this TBuilder builder, Func<StrategyBuilderContext, ResilienceStrategy> factory, ResilienceStrategyOptions options)
+    public static TBuilder AddStrategy<TBuilder>(
+        this TBuilder builder,
+        Func<StrategyBuilderContext, ResilienceStrategy> factory,
+        ResilienceStrategyOptions options)
         where TBuilder : ResiliencePipelineBuilderBase
     {
         Guard.NotNull(builder);
@@ -82,14 +85,15 @@ public static TBuilder AddStrategy<TBuilder>(this TBuilder builder, Func<Strateg
     /// </summary>
     /// <param name="builder">The builder instance.</param>
     /// <param name="factory">The factory that creates a resilience strategy.</param>
-    /// <param name="options">The options associated with the strategy. If none are provided the default instance of <see cref="ResilienceStrategyOptions"/> is created.</param>
+    /// <param name="options">The options associated with the strategy.</param>
     /// <returns>The same builder instance.</returns>
     /// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="factory"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
     /// <exception cref="InvalidOperationException">Thrown when this builder was already used to create a pipeline. The builder cannot be modified after it has been used.</exception>
     /// <exception cref="ValidationException">Thrown when <paramref name="options"/> is invalid.</exception>
     [RequiresUnreferencedCode(Constants.OptionsValidation)]
     public static ResiliencePipelineBuilder AddStrategy(
-        this ResiliencePipelineBuilder builder, Func<StrategyBuilderContext, ResilienceStrategy<object>> factory,
+        this ResiliencePipelineBuilder builder,
+        Func<StrategyBuilderContext, ResilienceStrategy<object>> factory,
         ResilienceStrategyOptions options)
     {
         Guard.NotNull(builder);
@@ -106,14 +110,15 @@ public static ResiliencePipelineBuilder AddStrategy(
     /// <typeparam name="TResult">The type of the result.</typeparam>
     /// <param name="builder">The builder instance.</param>
     /// <param name="factory">The factory that creates a resilience strategy.</param>
-    /// <param name="options">The options associated with the strategy. If none are provided the default instance of <see cref="ResilienceStrategyOptions"/> is created.</param>
+    /// <param name="options">The options associated with the strategy.</param>
     /// <returns>The same builder instance.</returns>
     /// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="factory"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
     /// <exception cref="InvalidOperationException">Thrown when this builder was already used to create a pipeline. The builder cannot be modified after it has been used.</exception>
     /// <exception cref="ValidationException">Thrown when <paramref name="options"/> is invalid.</exception>
     [RequiresUnreferencedCode(Constants.OptionsValidation)]
     public static ResiliencePipelineBuilder<TResult> AddStrategy<TResult>(
-        this ResiliencePipelineBuilder<TResult> builder, Func<StrategyBuilderContext, ResilienceStrategy<TResult>> factory,
+        this ResiliencePipelineBuilder<TResult> builder,
+        Func<StrategyBuilderContext, ResilienceStrategy<TResult>> factory,
         ResilienceStrategyOptions options)
     {
         Guard.NotNull(builder);
@@ -124,6 +129,78 @@ public static ResiliencePipelineBuilder<TResult> AddStrategy<TResult>(
         return builder;
     }
 
+    /// <summary>
+    /// Adds a reactive resilience strategy to the builder.
+    /// </summary>
+    /// <typeparam name="TBuilder">The builder type.</typeparam>
+    /// <param name="builder">The builder instance.</param>
+    /// <param name="factory">The strategy factory.</param>
+    /// <returns>The same builder instance.</returns>
+    /// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="factory"/> is <see langword="null"/>.</exception>
+    /// <exception cref="InvalidOperationException">Thrown when this builder was already used to create a pipeline. The builder cannot be modified after it has been used.</exception>
+    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(EmptyOptions))]
+    [UnconditionalSuppressMessage(
+        "Trimming",
+        "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
+        Justification = "All options members preserved for the empty options.")]
+    public static TBuilder AddStrategy<TBuilder>(
+        this TBuilder builder,
+        Func<StrategyBuilderContext, ResilienceStrategy> factory)
+        where TBuilder : ResiliencePipelineBuilderBase
+    {
+        Guard.NotNull(builder);
+        Guard.NotNull(factory);
+
+        return builder.AddStrategy(factory, EmptyOptions.Instance);
+    }
+
+    /// <summary>
+    /// Adds a proactive resilience strategy to the builder.
+    /// </summary>
+    /// <param name="builder">The builder instance.</param>
+    /// <param name="factory">The strategy instance.</param>
+    /// <returns>The same builder instance.</returns>
+    /// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="factory"/> is <see langword="null"/>.</exception>
+    /// <exception cref="InvalidOperationException">Thrown when this builder was already used to create a pipeline. The builder cannot be modified after it has been used.</exception>
+    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(EmptyOptions))]
+    [UnconditionalSuppressMessage(
+        "Trimming",
+        "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
+        Justification = "All options members preserved for the empty options.")]
+    public static ResiliencePipelineBuilder AddStrategy(
+        this ResiliencePipelineBuilder builder,
+        Func<StrategyBuilderContext, ResilienceStrategy<object>> factory)
+    {
+        Guard.NotNull(builder);
+        Guard.NotNull(factory);
+
+        return builder.AddStrategy(factory, EmptyOptions.Instance);
+    }
+
+    /// <summary>
+    /// Adds a reactive resilience strategy to the builder.
+    /// </summary>
+    /// <typeparam name="TResult">The result type.</typeparam>
+    /// <param name="builder">The builder instance.</param>
+    /// <param name="factory">The strategy instance.</param>
+    /// <returns>The same builder instance.</returns>
+    /// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="factory"/> is <see langword="null"/>.</exception>
+    /// <exception cref="InvalidOperationException">Thrown when this builder was already used to create a pipeline. The builder cannot be modified after it has been used.</exception>
+    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(EmptyOptions))]
+    [UnconditionalSuppressMessage(
+        "Trimming",
+        "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
+        Justification = "All options members preserved for the empty options.")]
+    public static ResiliencePipelineBuilder<TResult> AddStrategy<TResult>(
+        this ResiliencePipelineBuilder<TResult> builder,
+        Func<StrategyBuilderContext, ResilienceStrategy<TResult>> factory)
+    {
+        Guard.NotNull(builder);
+        Guard.NotNull(factory);
+
+        return builder.AddStrategy(factory, EmptyOptions.Instance);
+    }
+
     internal sealed class EmptyOptions : ResilienceStrategyOptions
     {
         public static readonly EmptyOptions Instance = new();


Test patch:
diff --git a/test/Polly.Core.Tests/GenericResiliencePipelineBuilderTests.cs b/test/Polly.Core.Tests/GenericResiliencePipelineBuilderTests.cs
index 5a7a166eec2..62bad5fef7d 100644
--- a/test/Polly.Core.Tests/GenericResiliencePipelineBuilderTests.cs
+++ b/test/Polly.Core.Tests/GenericResiliencePipelineBuilderTests.cs
@@ -1,5 +1,6 @@
 using Microsoft.Extensions.Time.Testing;
 using NSubstitute;
+using Polly.Testing;
 using Polly.Utils.Pipeline;
 
 namespace Polly.Core.Tests;
@@ -63,4 +64,19 @@ public void AddGenericStrategy_Ok()
             .Should()
             .Be(strategy);
     }
+
+    [Fact]
+    public void AddStrategy_ExplicitInstance_Ok()
+    {
+        var builder = new ResiliencePipelineBuilder<string>();
+        var strategy = Substitute.For<ResilienceStrategy<string>>();
+
+        builder.AddStrategy(_ => strategy);
+
+        builder
+            .Build()
+            .GetPipelineDescriptor()
+            .FirstStrategy.StrategyInstance.Should()
+            .BeSameAs(strategy);
+    }
 }
diff --git a/test/Polly.Core.Tests/ResiliencePipelineBuilderTests.cs b/test/Polly.Core.Tests/ResiliencePipelineBuilderTests.cs
index 43e1b48acd5..160793dc4be 100644
--- a/test/Polly.Core.Tests/ResiliencePipelineBuilderTests.cs
+++ b/test/Polly.Core.Tests/ResiliencePipelineBuilderTests.cs
@@ -114,6 +114,36 @@ public void AddPipeline_Multiple_Ok()
         executions.Should().HaveCount(7);
     }
 
+    [Fact]
+    public void AddStrategy_ExplicitProactiveInstance_Ok()
+    {
+        var builder = new ResiliencePipelineBuilder();
+        var strategy = new TestResilienceStrategy();
+
+        builder.AddStrategy(_ => strategy);
+
+        builder
+            .Build()
+            .GetPipelineDescriptor()
+            .FirstStrategy.StrategyInstance.Should()
+            .BeSameAs(strategy);
+    }
+
+    [Fact]
+    public void AddStrategy_ExplicitReactiveInstance_Ok()
+    {
+        var builder = new ResiliencePipelineBuilder();
+        var strategy = Substitute.For<ResilienceStrategy<object>>();
+
+        builder.AddStrategy(_ => strategy);
+
+        builder
+            .Build()
+            .GetPipelineDescriptor()
+            .FirstStrategy.StrategyInstance.Should()
+            .BeSameAs(strategy);
+    }
+
     [Fact]
     public void Validator_Ok()
     {