diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index eb8f1d6dc80..c100f18aa05 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -7,6 +7,12 @@ "commands": [ "dotnet-cake" ] + }, + "markdownsnippets.tool": { + "version": "25.1.0", + "commands": [ + "mdsnippets" + ] } } -} +} \ No newline at end of file diff --git a/.github/workflows/on-push-do-docs.yml b/.github/workflows/on-push-do-docs.yml new file mode 100644 index 00000000000..78bf38102f0 --- /dev/null +++ b/.github/workflows/on-push-do-docs.yml @@ -0,0 +1,96 @@ +name: on-push-do-docs + +on: + push: + branches: [main] + paths: [ "src/Snippets/**" ] + workflow_dispatch: + +permissions: + contents: read + +jobs: + update-docs: + name: update-docs + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 + + - name: Update documentation + id: update-docs + shell: pwsh + env: + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + run: | + $ErrorActionPreference = "Stop" + $ProgressPreference = "SilentlyContinue" + + dotnet tool restore + dotnet mdsnippets + + $GitStatus = (git status --porcelain) + if ([string]::IsNullOrEmpty($GitStatus)) { + Write-Host "No changes to commit." + exit 0 + } + + $TimeStamp = Get-Date -Format "yyyy-MM-dd-HH-mm" + $BranchName = "docs/update-docs-$TimeStamp" + "branchName=$BranchName" >> $env:GITHUB_OUTPUT + + $GitEmail = "138034000+polly-updater-bot[bot]@users.noreply.github.com" + $GitUser = "polly-updater-bot[bot]" + + git config user.email $GitEmail | Out-Null + git config user.name $GitUser | Out-Null + git remote set-url "${{ github.server_url }}/${{ github.repository }}.git" | Out-Null + git fetch origin | Out-Null + git rev-parse --verify --quiet ("remotes/origin/" + $BranchName) | Out-Null + + if ($LASTEXITCODE -eq 0) { + Write-Host "Branch $BranchName already exists." + exit 0 + } + + git checkout -b $BranchName + git add . + git commit -m "Update the code-snippets in the documentation" + git push -u origin $BranchName + "updated-docs=true" >> $env:GITHUB_OUTPUT + + - name: Generate GitHub application token + if: steps.update-docs.outputs.updated-docs == 'true' + id: generate-application-token + uses: peter-murray/workflow-application-token-action@8e1ba3bf1619726336414f1014e37f17fbadf1db # v2.1.0 + with: + application_id: ${{ secrets.POLLY_UPDATER_BOT_APP_ID }} + application_private_key: ${{ secrets.POLLY_UPDATER_BOT_KEY }} + permissions: "contents:write, pull_requests:write" + + - name: Create pull request + if: steps.update-docs.outputs.updated-docs == 'true' + uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 + with: + github-token: ${{ steps.generate-application-token.outputs.token }} + script: | + const { repo, owner } = context.repo; + const workflowUrl = `${{ github.server_url }}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`; + const branchName = "${{ steps.update-docs.outputs.branchName }}"; + const result = await github.rest.pulls.create({ + title: 'Update the code-snippets in the documentation', + owner, + repo, + head: branchName, + base: 'main', + body: [ + 'This PR updates the code-snippets in the documentation.', + '', + `This pull request was generated by [GitHub Actions](${workflowUrl}).` + ].join('\n') + }); diff --git a/Directory.Packages.props b/Directory.Packages.props index cc64d160c98..8fbd4b6b9c6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,7 +25,9 @@ + + diff --git a/Polly.sln b/Polly.sln index 97851b37fda..bf30ece2e28 100644 --- a/Polly.sln +++ b/Polly.sln @@ -56,6 +56,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Polly.Testing", "src\Polly. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Polly.Testing.Tests", "test\Polly.Testing.Tests\Polly.Testing.Tests.csproj", "{D333B5CE-982D-4C11-BDAF-4217AA02306E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snippets", "src\Snippets\Snippets.csproj", "{D812B941-79B0-4E1E-BB70-4FAE345B5234}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -114,6 +116,10 @@ Global {D333B5CE-982D-4C11-BDAF-4217AA02306E}.Debug|Any CPU.Build.0 = Debug|Any CPU {D333B5CE-982D-4C11-BDAF-4217AA02306E}.Release|Any CPU.ActiveCfg = Release|Any CPU {D333B5CE-982D-4C11-BDAF-4217AA02306E}.Release|Any CPU.Build.0 = Release|Any CPU + {D812B941-79B0-4E1E-BB70-4FAE345B5234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D812B941-79B0-4E1E-BB70-4FAE345B5234}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D812B941-79B0-4E1E-BB70-4FAE345B5234}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D812B941-79B0-4E1E-BB70-4FAE345B5234}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -133,6 +139,7 @@ Global {C04DEE61-C1EA-4028-B457-CDBD304B8ED9} = {A6CC41B9-E0B9-44F8-916B-3E4A78DA3BFB} {9AD2D6AD-56E4-49D6-B6F1-EE975D5760B9} = {B7BF406B-B06F-4025-83E6-7219C53196A6} {D333B5CE-982D-4C11-BDAF-4217AA02306E} = {A6CC41B9-E0B9-44F8-916B-3E4A78DA3BFB} + {D812B941-79B0-4E1E-BB70-4FAE345B5234} = {B7BF406B-B06F-4025-83E6-7219C53196A6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2E5D54CD-770A-4345-B585-1848FC2EA6F4} diff --git a/bench/README.md b/bench/README.md index 685b604fdcd..9f489c67a34 100644 --- a/bench/README.md +++ b/bench/README.md @@ -1,4 +1,4 @@ -# Benchmarks +# Benchmarks To run the benchmarks, use the `benchmarks.ps1` script in this repository: diff --git a/build.cake b/build.cake index 4d69f8d3725..e1d8143561a 100644 --- a/build.cake +++ b/build.cake @@ -233,6 +233,16 @@ Task("__CreateNuGetPackages") } }); +Task("__ValidateDocs") + .Does(() => +{ + var result = StartProcess("dotnet", "mdsnippets --validate-content"); + if (result != 0) + { + throw new InvalidOperationException($"Failed to validate the documentation snippets. Are the links correct?"); + } +}); + ////////////////////////////////////////////////////////////////////// // BUILD TASKS ////////////////////////////////////////////////////////////////////// @@ -240,6 +250,7 @@ Task("__CreateNuGetPackages") Task("Build") .IsDependentOn("__Clean") .IsDependentOn("__RestoreNuGetPackages") + .IsDependentOn("__ValidateDocs") .IsDependentOn("__BuildSolutions") .IsDependentOn("__RunTests") .IsDependentOn("__RunMutationTests") diff --git a/mdsnippets.json b/mdsnippets.json new file mode 100644 index 00000000000..6ebcb8e4a74 --- /dev/null +++ b/mdsnippets.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/schema.json", + "ExcludeDirectories": [ "test", "artifacts", "bench", "eng" ], + "ExcludeSnippetDirectories": [ "src/Polly", "src/Polly.Core", "src/Polly.RateLimiting", "src/Polly.Testing", "src/Polly.Extensions", "artifacts", "test" ], + "OmitSnippetLinks": true, + "Convention": "InPlaceOverwrite" + } \ No newline at end of file diff --git a/src/Polly.Core/README.md b/src/Polly.Core/README.md index a51ce884bc5..b3b20a2e041 100644 --- a/src/Polly.Core/README.md +++ b/src/Polly.Core/README.md @@ -71,24 +71,18 @@ The resilience pipeline may consist of one or more individual resilience strateg Here's an example of a non-reactive strategy that executes a user-provided callback: -```csharp + +```cs internal class MyCustomStrategy : ResilienceStrategy { - private readonly TimeProvider _timeProvider; - - public MyCustomStrategy(TimeProvider timeProvider) - { - _timeProvider = timeProvider; - } - - protected override async ValueTask ExecuteCore( - Func>> callback, - ResilienceContext context, + protected override async ValueTask> ExecuteCore( + Func>> callback, + ResilienceContext context, TState state) { // Perform actions before execution - var outcome = await callback(context, state).ContinueOnCapturedContext(context.ContinueOnCapturedContext); + var outcome = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); // Perform actions after execution @@ -96,6 +90,7 @@ internal class MyCustomStrategy : ResilienceStrategy } } ``` + ### About Synchronous and Asynchronous Executions @@ -113,23 +108,27 @@ To construct a resilience pipeline, chain various extensions on the `ResilienceP ### Creating a non-generic pipeline -```csharp -var pipeline = new ResiliencePipelineBuilder() + +```cs +ResiliencePipeline pipeline = new ResiliencePipelineBuilder() .AddRetry(new()) .AddCircuitBreaker(new()) - .AddTimeout(new TimeoutStrategyOptions() { ... }) + .AddTimeout(TimeSpan.FromSeconds(1)) .Build(); ``` + ### Creating a generic pipeline -```csharp -var pipeline = new ResiliencePipelineBuilder() + +```cs +ResiliencePipeline pipeline = new ResiliencePipelineBuilder() .AddRetry(new()) .AddCircuitBreaker(new()) - .AddTimeout(new TimeoutStrategyOptions() { ... }) + .AddTimeout(TimeSpan.FromSeconds(1)) .Build(); ``` + ## Extensibility @@ -137,13 +136,23 @@ Extending the resilience functionality is straightforward. You can create extens Here's an example: -```csharp + +```cs public static TBuilder AddMyCustomStrategy(this TBuilder builder, MyCustomStrategyOptions options) where TBuilder : ResiliencePipelineBuilderBase { return builder.AddStrategy(context => new MyCustomStrategy(), options); } + +public class MyCustomStrategyOptions : ResilienceStrategyOptions +{ + public MyCustomStrategyOptions() + { + Name = "MyCustomStrategy"; + } +} ``` + To gain insights into implementing custom resilience strategies, you can explore the following Polly strategy examples: @@ -185,7 +194,8 @@ These delegates accept either `Args` or `Args` arguments, which encapsu For non-reactive strategies, the `Args` structure might resemble: -```csharp + +```cs public readonly struct OnTimeoutArguments { public OnTimeoutArguments(ResilienceContext context, TimeSpan timeout) @@ -195,39 +205,23 @@ public readonly struct OnTimeoutArguments } public ResilienceContext Context { get; } // Include the Context property - public TimeSpan Timeout { get; } // Additional event-related properties -} -``` -For reactive strategies, `Args` could look like: - -```csharp -public readonly struct OnRetryArguments -{ - public OnRetryArguments(ResilienceContext context, Outcome outcome, int attemptNumber) - { - Context = context; - Outcome = outcome; - AttemptNumber = attemptNumber; - } - - public ResilienceContext Context { get; } // Include the Context property - public Outcome Outcome { get; } // Includes the outcome associated with the event - public int AttemptNumber { get; } + public TimeSpan Timeout { get; } // Additional event-related properties } ``` + ### Example: Usage of Delegates Below are some examples illustrating the usage of these delegates: -```csharp + +```cs new ResiliencePipelineBuilder() .AddRetry(new RetryStrategyOptions { - // Non-Generic predicate for multiple result types - ShouldHandle = args => args switch + ShouldHandle = args => args.Outcome switch { { Exception: InvalidOperationException } => PredicateResult.True(), { Result: string result } when result == "Failure" => PredicateResult.True(), @@ -236,22 +230,21 @@ new ResiliencePipelineBuilder() }, }) .Build(); -``` -```csharp new ResiliencePipelineBuilder() .AddRetry(new RetryStrategyOptions { // Generic predicate for a single result type - ShouldHandle = args => args switch + ShouldHandle = args => args.Outcome switch { { Exception: InvalidOperationException } => PredicateResult.True(), - { Result: result } when result == "Failure" => PredicateResult.True(), + { Result: { } result } when result == "Failure" => PredicateResult.True(), _ => PredicateResult.False() }, }) .Build(); ``` + ## Telemetry @@ -259,4 +252,4 @@ Each resilience strategy can generate telemetry data through the [`ResiliencePip To leverage this telemetry data, users should assign a `TelemetryListener` instance to `ResiliencePipelineBuilder.TelemetryListener` and then consume the `TelemetryEventArguments`. -For common scenarios, it is expected that users would make use of `Polly.Extensions`. This extension enables telemetry configuration through the `ResiliencePipelineBuilder.ConfigureTelemetry(...)` method, which processes `TelemetryEventArguments` to generate logs and metrics. \ No newline at end of file +For common scenarios, it is expected that users would make use of `Polly.Extensions`. This extension enables telemetry configuration through the `ResiliencePipelineBuilder.ConfigureTelemetry(...)` method, which processes `TelemetryEventArguments` to generate logs and metrics. diff --git a/src/Polly.Extensions/README.md b/src/Polly.Extensions/README.md index 98ad82ea4e4..8d227885e67 100644 --- a/src/Polly.Extensions/README.md +++ b/src/Polly.Extensions/README.md @@ -4,7 +4,8 @@ Below is an example illustrating the usage of `AddResiliencePipeline` extension method: -``` csharp + +```cs var services = new ServiceCollection(); // Define a resilience pipeline @@ -13,13 +14,16 @@ services.AddResiliencePipeline( builder => builder.AddTimeout(TimeSpan.FromSeconds(10))); // Define a resilience pipeline with custom options -services.AddResiliencePipeline( - "my-timeout", - (builder, context) => - { - var myOptions = context.ServiceProvider.GetRequiredService>().Value; - builder.AddTimeout(myOptions.Timeout); - }); +services + .Configure(options => options.Timeout = TimeSpan.FromSeconds(10)) + .AddResiliencePipeline( + "my-timeout", + (builder, context) => + { + var myOptions = context.GetOptions(); + + builder.AddTimeout(myOptions.Timeout); + }); // Resolve the resilience pipeline var serviceProvider = services.BuildServiceProvider(); @@ -27,8 +31,9 @@ var pipelineProvider = serviceProvider.GetRequiredService { ... }); +await pipeline.ExecuteAsync(async cancellation => await Task.Delay(100, cancellation)); ``` + > [!NOTE] > Telemetry is enabled by default when utilizing the `AddResiliencePipeline` extension method. @@ -37,11 +42,13 @@ await pipeline.ExecuteAsync(cancellation => { ... }); Upon invoking the `ConfigureTelemetry` extension method, Polly begins to emit logs and metrics. Here's an example: -``` csharp -var telemetryOptions = new TelemetryOptions(); - -// Configure logging -telemetryOptions.LoggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); + +```cs +var telemetryOptions = new TelemetryOptions +{ + // Configure logging + LoggerFactory = LoggerFactory.Create(builder => builder.AddConsole()) +}; // Configure enrichers telemetryOptions.MeteringEnrichers.Add(new MyMeteringEnricher()); @@ -53,8 +60,12 @@ var builder = new ResiliencePipelineBuilder() .AddTimeout(TimeSpan.FromSeconds(1)) .ConfigureTelemetry(telemetryOptions) // This method enables telemetry in the builder .Build(); +``` + -class MyTelemetryListener : TelemetryListener + +```cs +internal class MyTelemetryListener : TelemetryListener { public override void Write(in TelemetryEventArguments args) { @@ -62,22 +73,23 @@ class MyTelemetryListener : TelemetryListener } } -class MyMeteringEnricher : MeteringEnricher +internal class MyMeteringEnricher : MeteringEnricher { public override void Enrich(in EnrichmentContext context) { - context.Tags.Add(new("my-custom-tag", "custom-value"); + context.Tags.Add(new("my-custom-tag", "custom-value")); } } ``` + Alternatively, you can use the `AddResiliencePipeline` extension which automatically adds telemetry: -``` csharp + +```cs var serviceCollection = new ServiceCollection() .AddLogging(builder => builder.AddConsole()) .AddResiliencePipeline("my-strategy", builder => builder.AddTimeout(TimeSpan.FromSeconds(1))) - // Configure the default settings for TelemetryOptions .Configure(options => { // Configure enrichers @@ -87,6 +99,7 @@ var serviceCollection = new ServiceCollection() options.TelemetryListeners.Add(new MyTelemetryListener()); }); ``` + ### Emitted Metrics diff --git a/src/Polly.RateLimiting/README.md b/src/Polly.RateLimiting/README.md index e86596e1d54..ff346fb90a9 100644 --- a/src/Polly.RateLimiting/README.md +++ b/src/Polly.RateLimiting/README.md @@ -8,7 +8,10 @@ The `Polly.RateLimiting` package adopts the [.NET Rate Limiting](https://devblog Example: -``` csharp + +```cs +ResiliencePipelineBuilder builder = new ResiliencePipelineBuilder(); + // Convenience extension method for ConcurrencyLimiter builder.AddConcurrencyLimiter(permitLimit: 10, queueLimit: 10); @@ -45,4 +48,4 @@ builder.AddRateLimiter(new RateLimiterStrategyOptions } }); ``` - + diff --git a/src/Polly.Testing/README.md b/src/Polly.Testing/README.md index 1593b34f83c..c2720652ce0 100644 --- a/src/Polly.Testing/README.md +++ b/src/Polly.Testing/README.md @@ -2,26 +2,28 @@ This package exposes APIs and utilities that can be used to assert on the composition of resilience pipelines. -``` csharp + +```cs // Build your resilience pipeline. ResiliencePipeline pipeline = new ResiliencePipelineBuilder() .AddRetry(new RetryStrategyOptions { - RetryCount = 4 + MaxRetryAttempts = 4 }) .AddTimeout(TimeSpan.FromSeconds(1)) .ConfigureTelemetry(NullLoggerFactory.Instance) .Build(); // Retrieve inner strategies. -ResiliencePipelineDescriptor descriptor = strategy.GetPipelineDescriptor(); +ResiliencePipelineDescriptor descriptor = pipeline.GetPipelineDescriptor(); // Assert the composition. Assert.Equal(2, descriptor.Strategies.Count); var retryOptions = Assert.IsType(descriptor.Strategies[0].Options); -Assert.Equal(4, retryOptions.RetryCount); +Assert.Equal(4, retryOptions.MaxRetryAttempts); var timeoutOptions = Assert.IsType(descriptor.Strategies[0].Options); Assert.Equal(TimeSpan.FromSeconds(1), timeoutOptions.Timeout); ``` + diff --git a/src/Snippets/Core/Snippets.cs b/src/Snippets/Core/Snippets.cs new file mode 100644 index 00000000000..526835b52b6 --- /dev/null +++ b/src/Snippets/Core/Snippets.cs @@ -0,0 +1,143 @@ +using Polly; +using Polly.Retry; + +namespace Snippets.Core; + +internal static class Snippets +{ + public static void NonGenericPipeline() + { + #region create-non-generic-pipeline + + ResiliencePipeline pipeline = new ResiliencePipelineBuilder() + .AddRetry(new()) + .AddCircuitBreaker(new()) + .AddTimeout(TimeSpan.FromSeconds(1)) + .Build(); + + #endregion + } + + public static void GenericPipeline() + { + #region create-generic-pipeline + + ResiliencePipeline pipeline = new ResiliencePipelineBuilder() + .AddRetry(new()) + .AddCircuitBreaker(new()) + .AddTimeout(TimeSpan.FromSeconds(1)) + .Build(); + + #endregion + } + + #region on-retry-args + + public readonly struct OnRetryArguments + { + public OnRetryArguments(ResilienceContext context, Outcome outcome, int attemptNumber) + { + Context = context; + Outcome = outcome; + AttemptNumber = attemptNumber; + } + + public ResilienceContext Context { get; } // Include the Context property + + public Outcome Outcome { get; } // Includes the outcome associated with the event + + public int AttemptNumber { get; } + } + + #endregion + + #region on-timeout-args + + public readonly struct OnTimeoutArguments + { + public OnTimeoutArguments(ResilienceContext context, TimeSpan timeout) + { + Context = context; + Timeout = timeout; + } + + public ResilienceContext Context { get; } // Include the Context property + + public TimeSpan Timeout { get; } // Additional event-related properties + } + + #endregion + + #region add-my-custom-strategy + + public static TBuilder AddMyCustomStrategy(this TBuilder builder, MyCustomStrategyOptions options) + where TBuilder : ResiliencePipelineBuilderBase + { + return builder.AddStrategy(context => new MyCustomStrategy(), options); + } + + public class MyCustomStrategyOptions : ResilienceStrategyOptions + { + public MyCustomStrategyOptions() + { + Name = "MyCustomStrategy"; + } + } + + #endregion + + #region my-custom-strategy + + internal class MyCustomStrategy : ResilienceStrategy + { + protected override async ValueTask> ExecuteCore( + Func>> callback, + ResilienceContext context, + TState state) + { + // Perform actions before execution + + var outcome = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); + + // Perform actions after execution + + return outcome; + } + } + + #endregion + + public static void DelegateUsage() + { + #region delegate-usage + + new ResiliencePipelineBuilder() + .AddRetry(new RetryStrategyOptions + { + // Non-Generic predicate for multiple result types + ShouldHandle = args => args.Outcome switch + { + { Exception: InvalidOperationException } => PredicateResult.True(), + { Result: string result } when result == "Failure" => PredicateResult.True(), + { Result: int result } when result == -1 => PredicateResult.True(), + _ => PredicateResult.False() + }, + }) + .Build(); + + new ResiliencePipelineBuilder() + .AddRetry(new RetryStrategyOptions + { + // Generic predicate for a single result type + ShouldHandle = args => args.Outcome switch + { + { Exception: InvalidOperationException } => PredicateResult.True(), + { Result: { } result } when result == "Failure" => PredicateResult.True(), + _ => PredicateResult.False() + }, + }) + .Build(); + + #endregion; + } +} diff --git a/src/Snippets/Extensions/Snippets.cs b/src/Snippets/Extensions/Snippets.cs new file mode 100644 index 00000000000..41a8dd1327b --- /dev/null +++ b/src/Snippets/Extensions/Snippets.cs @@ -0,0 +1,111 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Polly; +using Polly.Registry; +using Polly.Telemetry; + +namespace Snippets.Extensions; + +internal static class Snippets +{ + public static void ConfigureTelemetry() + { + #region configure-telemetry + + var telemetryOptions = new TelemetryOptions + { + // Configure logging + LoggerFactory = LoggerFactory.Create(builder => builder.AddConsole()) + }; + + // Configure enrichers + telemetryOptions.MeteringEnrichers.Add(new MyMeteringEnricher()); + + // Configure telemetry listeners + telemetryOptions.TelemetryListeners.Add(new MyTelemetryListener()); + + var builder = new ResiliencePipelineBuilder() + .AddTimeout(TimeSpan.FromSeconds(1)) + .ConfigureTelemetry(telemetryOptions) // This method enables telemetry in the builder + .Build(); + + #endregion + } + + public static async Task AddResiliencePipeline() + { + #region add-resilience-pipeline + + var services = new ServiceCollection(); + + // Define a resilience pipeline + services.AddResiliencePipeline( + "my-key", + builder => builder.AddTimeout(TimeSpan.FromSeconds(10))); + + // Define a resilience pipeline with custom options + services + .Configure(options => options.Timeout = TimeSpan.FromSeconds(10)) + .AddResiliencePipeline( + "my-timeout", + (builder, context) => + { + var myOptions = context.GetOptions(); + + builder.AddTimeout(myOptions.Timeout); + }); + + // Resolve the resilience pipeline + var serviceProvider = services.BuildServiceProvider(); + var pipelineProvider = serviceProvider.GetRequiredService>(); + var pipeline = pipelineProvider.GetPipeline("my-key"); + + // Use it + await pipeline.ExecuteAsync(async cancellation => await Task.Delay(100, cancellation)); + + #endregion + } + + public static void AddResiliencePipelineWithTelemetry() + { + #region add-resilience-pipeline-with-telemetry + + var serviceCollection = new ServiceCollection() + .AddLogging(builder => builder.AddConsole()) + .AddResiliencePipeline("my-strategy", builder => builder.AddTimeout(TimeSpan.FromSeconds(1))) + .Configure(options => + { + // Configure enrichers + options.MeteringEnrichers.Add(new MyMeteringEnricher()); + + // Configure telemetry listeners + options.TelemetryListeners.Add(new MyTelemetryListener()); + }); + #endregion + } + + private class MyTimeoutOptions + { + public TimeSpan Timeout { get; set; } + } + + #region telemetry-listeners + + internal class MyTelemetryListener : TelemetryListener + { + public override void Write(in TelemetryEventArguments args) + { + Console.WriteLine($"Telemetry event occurred: {args.Event.EventName}"); + } + } + + internal class MyMeteringEnricher : MeteringEnricher + { + public override void Enrich(in EnrichmentContext context) + { + context.Tags.Add(new("my-custom-tag", "custom-value")); + } + } + + #endregion +} diff --git a/src/Snippets/README.md b/src/Snippets/README.md new file mode 100644 index 00000000000..b9acd526644 --- /dev/null +++ b/src/Snippets/README.md @@ -0,0 +1,7 @@ +# Snippets + +Various code-snippets used in the Polly documentation. Run the following command in the root directory to update the snippets in the docs: + +```powershell +dotnet mdsnippets +``` diff --git a/src/Snippets/RateLimiting/Snippets.cs b/src/Snippets/RateLimiting/Snippets.cs new file mode 100644 index 00000000000..0cd2c9b0a66 --- /dev/null +++ b/src/Snippets/RateLimiting/Snippets.cs @@ -0,0 +1,53 @@ +using System.Threading.RateLimiting; +using Polly; +using Polly.RateLimiting; + +namespace Snippets.RateLimiting; + +internal static class Snippets +{ + public static void Usage() + { + #region rate-limiter-usage + + ResiliencePipelineBuilder builder = new ResiliencePipelineBuilder(); + + // Convenience extension method for ConcurrencyLimiter + builder.AddConcurrencyLimiter(permitLimit: 10, queueLimit: 10); + + // Convenience extension method for ConcurrencyLimiter with callback + builder.AddConcurrencyLimiter( + new ConcurrencyLimiterOptions + { + PermitLimit = 10, + QueueLimit = 10 + }); + + // Convenience extension method with custom limiter creation + builder.AddRateLimiter( + new ConcurrencyLimiter(new ConcurrencyLimiterOptions + { + PermitLimit = 10, + QueueLimit = 10 + })); + + // Add rate limiter using the RateLimiterStrategyOptions + var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions + { + PermitLimit = 10, + QueueLimit = 10 + }); + + builder.AddRateLimiter(new RateLimiterStrategyOptions + { + RateLimiter = args => limiter.AcquireAsync(1, args.Context.CancellationToken), + OnRejected = _ => + { + Console.WriteLine("Rate limiter rejected!"); + return default; + } + }); + + #endregion + } +} diff --git a/src/Snippets/Snippets.csproj b/src/Snippets/Snippets.csproj new file mode 100644 index 00000000000..cedda64e529 --- /dev/null +++ b/src/Snippets/Snippets.csproj @@ -0,0 +1,26 @@ + + + + Library + net7.0 + enable + enable + Library + false + false + $(NoWarn);SA1123;SA1515;CA2000;CA2007;CA1303;IDE0021 + + + + + + + + + + + + + + + diff --git a/src/Snippets/Testing/Snippets.cs b/src/Snippets/Testing/Snippets.cs new file mode 100644 index 00000000000..11e2a6539fb --- /dev/null +++ b/src/Snippets/Testing/Snippets.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.Logging.Abstractions; +using Polly; +using Polly.Retry; +using Polly.Testing; +using Polly.Timeout; +using Xunit; + +namespace Snippets.Testing; + +internal static class Snippets +{ + public static void GetPipelineDescriptor() + { + #region get-pipeline-descriptor + + // Build your resilience pipeline. + ResiliencePipeline pipeline = new ResiliencePipelineBuilder() + .AddRetry(new RetryStrategyOptions + { + MaxRetryAttempts = 4 + }) + .AddTimeout(TimeSpan.FromSeconds(1)) + .ConfigureTelemetry(NullLoggerFactory.Instance) + .Build(); + + // Retrieve inner strategies. + ResiliencePipelineDescriptor descriptor = pipeline.GetPipelineDescriptor(); + + // Assert the composition. + Assert.Equal(2, descriptor.Strategies.Count); + + var retryOptions = Assert.IsType(descriptor.Strategies[0].Options); + Assert.Equal(4, retryOptions.MaxRetryAttempts); + + var timeoutOptions = Assert.IsType(descriptor.Strategies[0].Options); + Assert.Equal(TimeSpan.FromSeconds(1), timeoutOptions.Timeout); + + #endregion + } +}