diff --git a/.github/wordlist.txt b/.github/wordlist.txt index fe5f2dacbd8..d199af4bbdd 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -12,6 +12,7 @@ circuitbreaker comparer contrib deserialization +dependencyinjection dotnet dotnetrocks durations @@ -21,6 +22,8 @@ extensibility flurl fs hangfire +httpclient +httpclientfactory interop jetbrains jitter @@ -50,6 +53,7 @@ rebase rebased rebasing resharper +restsharp rethrow rethrows retryable @@ -73,6 +77,7 @@ ui unhandled uwp valuetask +vnext waitandretry wpf xunit diff --git a/Directory.Packages.props b/Directory.Packages.props index c570377cf69..f335137a768 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,6 +11,7 @@ + @@ -37,7 +38,9 @@ + + diff --git a/docs/community/http-client-integrations.md b/docs/community/http-client-integrations.md new file mode 100644 index 00000000000..57843f628cc --- /dev/null +++ b/docs/community/http-client-integrations.md @@ -0,0 +1,203 @@ +# HTTP client integration samples + +Transient failures are inevitable for HTTP based communication as well. It is not a surprise that many developers use Polly with an HTTP client to make there applications more robust. + +Here we have collected some popular HTTP client libraries and show how to integrate them with Polly. + +## Setting the stage + +In the examples below we will register HTTP clients into a Dependency Injection container. + +The same resilience strategy will be used each time to keep the samples focused on the HTTP client library integration. + + +```cs +private static ValueTask HandleTransientHttpError(Outcome outcome) => +outcome switch +{ + { Exception: HttpRequestException } => PredicateResult.True(), + { Result.StatusCode: HttpStatusCode.RequestTimeout } => PredicateResult.True(), + { Result.StatusCode: >= HttpStatusCode.InternalServerError } => PredicateResult.True(), + _ => PredicateResult.False() +}; + +private static RetryStrategyOptions GetRetryOptions() => +new() +{ + ShouldHandle = args => HandleTransientHttpError(args.Outcome), + MaxRetryAttempts = 3, + BackoffType = DelayBackoffType.Exponential, + Delay = TimeSpan.FromSeconds(2) +}; +``` + + +Here we create a strategy which will retry the HTTP request if the status code is either `408`, greater than or equal to `500`, or an `HttpRequestException` is thrown. + +The `HandleTransientHttpError` method is equivalent to the [`HttpPolicyExtensions.HandleTransientHttpError`](https://github.com/App-vNext/Polly.Extensions.Http/blob/93b91c4359f436bda37f870c4453f25555b9bfd8/src/Polly.Extensions.Http/HttpPolicyExtensions.cs) method in the [App-vNext/Polly.Extensions.Http](https://github.com/App-vNext/Polly.Extensions.Http) repository. + +## With HttpClient + +We use the [`AddResilienceHandler`](https://learn.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.resiliencehttpclientbuilderextensions.addresiliencehandler) method to register our resilience strategy with the built-in [`HttpClient`](https://learn.microsoft.com/dotnet/api/system.net.http.httpclient). + + +```cs +var services = new ServiceCollection(); + +// Register a named HttpClient and decorate with a resilience pipeline +services.AddHttpClient(HttpClientName) + .ConfigureHttpClient(client => client.BaseAddress = BaseAddress) + .AddResilienceHandler("httpclient_based_pipeline", + builder => builder.AddRetry(GetRetryOptions())); + +using var provider = services.BuildServiceProvider(); + +// Resolve the named HttpClient +var httpClientFactory = provider.GetRequiredService(); +var httpClient = httpClientFactory.CreateClient(HttpClientName); + +// Use the HttpClient by making a request +var response = await httpClient.GetAsync("/408"); +``` + + +> [!NOTE] +> The following packages are required to the above example: +> +> - [Microsoft.Extensions.DependencyInjection](https://www.nuget.org/packages/microsoft.extensions.dependencyinjection): Required for the dependency injection functionality +> - [Microsoft.Extensions.Http.Resilience](https://www.nuget.org/packages/Microsoft.Extensions.Http.Resilience): Required for the `AddResilienceHandler` extension + +### Further reading for HttpClient + +- [Build resilient HTTP apps: Key development patterns](https://learn.microsoft.com/dotnet/core/resilience/http-resilience) +- [Building resilient cloud services with .NET 8](https://devblogs.microsoft.com/dotnet/building-resilient-cloud-services-with-dotnet-8/) + +## With Flurl + +[Flurl](https://flurl.dev/) is a URL builder and HTTP client library for .NET. + +The named `HttpClient` registration and its decoration with our resilience strategy are the same as the built-in `HttpClient`. + +Here we create a `FlurlClient` which uses the decorated, named `HttpClient` for HTTP requests. + + +```cs +var services = new ServiceCollection(); + +// Register a named HttpClient and decorate with a resilience pipeline +services.AddHttpClient(HttpClientName) + .ConfigureHttpClient(client => client.BaseAddress = BaseAddress) + .AddResilienceHandler("flurl_based_pipeline", + builder => builder.AddRetry(GetRetryOptions())); + +using var provider = services.BuildServiceProvider(); + +// Resolve the named HttpClient and create a new FlurlClient +var httpClientFactory = provider.GetRequiredService(); +var flurlClient = new FlurlClient(httpClientFactory.CreateClient(HttpClientName)); + +// Use the FlurlClient by making a request +var response = await flurlClient.Request("/408").GetAsync(); +``` + + +> [!NOTE] +> The following packages are required to the above example: +> +> - [Microsoft.Extensions.DependencyInjection](https://www.nuget.org/packages/microsoft.extensions.dependencyinjection): Required for the dependency injection functionality +> - [Microsoft.Extensions.Http.Resilience](https://www.nuget.org/packages/Microsoft.Extensions.Http.Resilience): Required for the `AddResilienceHandler` extension +> - [Flurl.Http](https://www.nuget.org/packages/Flurl.Http/): Required for the `FlurlClient` + +### Further reading for Flurl + +- [Flurl home page](https://flurl.dev/) + +## With Refit + +[Refit](https://github.com/reactiveui/refit) is an automatic type-safe REST library for .NET. + +First let's define the API interface: + + +```cs +public interface IHttpStatusApi +{ + [Get("/408")] + Task GetRequestTimeoutEndpointAsync(); +} +``` + + +Then use the `AddRefitClient` method to register the interface as a typed `HttpClient`. Finally we call `AddResilienceHandler` to decorate the underlying `HttpClient` with our resilience strategy. + + +```cs +var services = new ServiceCollection(); + +// Register a Refit generated typed HttpClient and decorate with a resilience pipeline +services.AddRefitClient() + .ConfigureHttpClient(client => client.BaseAddress = BaseAddress) + .AddResilienceHandler("refit_based_pipeline", + builder => builder.AddRetry(GetRetryOptions())); + +// Resolve the typed HttpClient +using var provider = services.BuildServiceProvider(); +var apiClient = provider.GetRequiredService(); + +// Use the Refit generated typed HttpClient by making a request +var response = await apiClient.GetRequestTimeoutEndpointAsync(); +``` + + +> [!NOTE] +> The following packages are required to the above example: +> +> - [Microsoft.Extensions.DependencyInjection](https://www.nuget.org/packages/microsoft.extensions.dependencyinjection): Required for the dependency injection functionality +> - [Refit.HttpClientFactory](https://www.nuget.org/packages/Refit.HttpClientFactory): Required for the `AddRefitClient` extension + +### Further readings for Refit + +- [Using ASP.NET Core 2.1's HttpClientFactory with Refit's REST library](https://www.hanselman.com/blog/using-aspnet-core-21s-httpclientfactory-with-refits-rest-library) +- [Refit in .NET: Building Robust API Clients in C#](https://www.milanjovanovic.tech/blog/refit-in-dotnet-building-robust-api-clients-in-csharp) +- [Understand Refit in .NET Core](https://medium.com/@jaimin_99136/understand-the-refit-in-net-core-ba0097c5e620) + +## With RestSharp + +[RestSharp](https://restsharp.dev/) is a simple REST and HTTP API Client for .NET. + +The named `HttpClient` registration and its decoration with our resilience strategy are the same as the built-in `HttpClient`. + +Here we create a `RestClient` which uses the decorated, named `HttpClient` for HTTP requests. + + +```cs +var services = new ServiceCollection(); + +// Register a named HttpClient and decorate with a resilience pipeline +services.AddHttpClient(HttpClientName) + .ConfigureHttpClient(client => client.BaseAddress = BaseAddress) + .AddResilienceHandler("restsharp_based_pipeline", + builder => builder.AddRetry(GetRetryOptions())); + +using var provider = services.BuildServiceProvider(); + +// Resolve the named HttpClient and create a RestClient +var httpClientFactory = provider.GetRequiredService(); +var restClient = new RestClient(httpClientFactory.CreateClient(HttpClientName)); + +// Use the RestClient by making a request +var request = new RestRequest("/408", Method.Get); +var response = await restClient.ExecuteAsync(request); +``` + + +> [!NOTE] +> The following packages are required to the above example: +> +> - [Microsoft.Extensions.DependencyInjection](https://www.nuget.org/packages/microsoft.extensions.dependencyinjection): Required for the dependency injection functionality +> - [Microsoft.Extensions.Http.Resilience](https://www.nuget.org/packages/Microsoft.Extensions.Http.Resilience): Required for the `AddResilienceHandler` extension +> - [RestSharp](https://www.nuget.org/packages/RestSharp): Required for the `RestClient`, `RestRequest`, `RestResponse`, etc. types + +### Further reading for RestSharp + +- [RestSharp home page](https://restsharp.dev/) diff --git a/docs/community/toc.yml b/docs/community/toc.yml index 0d91ca86bf3..dd539a2fcd2 100644 --- a/docs/community/toc.yml +++ b/docs/community/toc.yml @@ -8,3 +8,5 @@ href: git-workflow.md - name: Cheat sheets href: cheat-sheets.md +- name: HTTP client integration samples + href: http-client-integrations.md diff --git a/src/Snippets/Docs/HttpClientIntegrations.cs b/src/Snippets/Docs/HttpClientIntegrations.cs new file mode 100644 index 00000000000..7f23eaa7193 --- /dev/null +++ b/src/Snippets/Docs/HttpClientIntegrations.cs @@ -0,0 +1,132 @@ +using System.Net; +using System.Net.Http; +using Flurl.Http; +using Microsoft.Extensions.DependencyInjection; +using Polly.Retry; +using Refit; +using RestSharp; + +namespace Snippets.Docs; + +internal static class HttpClientIntegrations +{ + private const string HttpClientName = "httpclient"; + private static readonly Uri BaseAddress = new("https://httpstat.us/"); + + #region http-client-integrations-handle-transient-errors + private static ValueTask HandleTransientHttpError(Outcome outcome) => + outcome switch + { + { Exception: HttpRequestException } => PredicateResult.True(), + { Result.StatusCode: HttpStatusCode.RequestTimeout } => PredicateResult.True(), + { Result.StatusCode: >= HttpStatusCode.InternalServerError } => PredicateResult.True(), + _ => PredicateResult.False() + }; + + private static RetryStrategyOptions GetRetryOptions() => + new() + { + ShouldHandle = args => HandleTransientHttpError(args.Outcome), + MaxRetryAttempts = 3, + BackoffType = DelayBackoffType.Exponential, + Delay = TimeSpan.FromSeconds(2) + }; + #endregion + +#pragma warning disable CA2234 + public static async Task HttpClientExample() + { + #region http-client-integrations-httpclient + var services = new ServiceCollection(); + + // Register a named HttpClient and decorate with a resilience pipeline + services.AddHttpClient(HttpClientName) + .ConfigureHttpClient(client => client.BaseAddress = BaseAddress) + .AddResilienceHandler("httpclient_based_pipeline", + builder => builder.AddRetry(GetRetryOptions())); + + using var provider = services.BuildServiceProvider(); + + // Resolve the named HttpClient + var httpClientFactory = provider.GetRequiredService(); + var httpClient = httpClientFactory.CreateClient(HttpClientName); + + // Use the HttpClient by making a request + var response = await httpClient.GetAsync("/408"); + #endregion + } +#pragma warning restore CA2234 + + public static async Task RefitExample() + { + #region http-client-integrations-refit + var services = new ServiceCollection(); + + // Register a Refit generated typed HttpClient and decorate with a resilience pipeline + services.AddRefitClient() + .ConfigureHttpClient(client => client.BaseAddress = BaseAddress) + .AddResilienceHandler("refit_based_pipeline", + builder => builder.AddRetry(GetRetryOptions())); + + // Resolve the typed HttpClient + using var provider = services.BuildServiceProvider(); + var apiClient = provider.GetRequiredService(); + + // Use the Refit generated typed HttpClient by making a request + var response = await apiClient.GetRequestTimeoutEndpointAsync(); + #endregion + } + + public static async Task FlurlExample() + { + #region http-client-integrations-flurl + var services = new ServiceCollection(); + + // Register a named HttpClient and decorate with a resilience pipeline + services.AddHttpClient(HttpClientName) + .ConfigureHttpClient(client => client.BaseAddress = BaseAddress) + .AddResilienceHandler("flurl_based_pipeline", + builder => builder.AddRetry(GetRetryOptions())); + + using var provider = services.BuildServiceProvider(); + + // Resolve the named HttpClient and create a new FlurlClient + var httpClientFactory = provider.GetRequiredService(); + var flurlClient = new FlurlClient(httpClientFactory.CreateClient(HttpClientName)); + + // Use the FlurlClient by making a request + var response = await flurlClient.Request("/408").GetAsync(); + #endregion + } + + public static async Task RestSharpExample() + { + #region http-client-integrations-restsharp + var services = new ServiceCollection(); + + // Register a named HttpClient and decorate with a resilience pipeline + services.AddHttpClient(HttpClientName) + .ConfigureHttpClient(client => client.BaseAddress = BaseAddress) + .AddResilienceHandler("restsharp_based_pipeline", + builder => builder.AddRetry(GetRetryOptions())); + + using var provider = services.BuildServiceProvider(); + + // Resolve the named HttpClient and create a RestClient + var httpClientFactory = provider.GetRequiredService(); + var restClient = new RestClient(httpClientFactory.CreateClient(HttpClientName)); + + // Use the RestClient by making a request + var request = new RestRequest("/408", Method.Get); + var response = await restClient.ExecuteAsync(request); + #endregion + } +} + +#region http-client-integrations-refit-interface +public interface IHttpStatusApi +{ + [Get("/408")] + Task GetRequestTimeoutEndpointAsync(); +} +#endregion diff --git a/src/Snippets/Snippets.csproj b/src/Snippets/Snippets.csproj index a5432022f89..78f29eae35e 100644 --- a/src/Snippets/Snippets.csproj +++ b/src/Snippets/Snippets.csproj @@ -22,6 +22,9 @@ + + +