Skip to content

Commit e36c495

Browse files
IEvangelistgeeknoidmartincostellomartintmkgewarren
authored
Resiliency in .NET content (#37270)
* Initial bits * Trying to explore the resilience libs * Updates to resiliency * More updates * Updates to docs * More fixes and updates * Fix heading * More HTTP bits * Apply suggestions from code review Co-authored-by: Martin Taillefer <geeknoid@users.noreply.github.com> * More tweaks * Added image * Additions * More updates * Apply suggestions from code review Co-authored-by: Martin Costello <martin@martincostello.com> Co-authored-by: martintmk <103487740+martintmk@users.noreply.github.com> * Clean up and updates * Add context * Retitle a bit * A quick fix * Resilience in TOC * Updates to headings * Fix TOC entry * Added a bit more detail * Update diagram * Updates to HTTP resiliency content * Add more configuration bits * From peer feedback * Clean up * Apply suggestions from code review Co-authored-by: Martin Costello <martin@martincostello.com> * Update image * Apply suggestions from code review Co-authored-by: martintmk <103487740+martintmk@users.noreply.github.com> * Updates from peer review * Major updates to the resilience bits * A bit closer * The id's should work * There we go * code bits for resilience * Update source with markers * Apply suggestions from code review Co-authored-by: martintmk <103487740+martintmk@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Co-authored-by: martintmk <103487740+martintmk@users.noreply.github.com> Co-authored-by: Martin Costello <martin@martincostello.com> * Address all feedback * More clean up, final touches --------- Co-authored-by: Martin Taillefer <geeknoid@users.noreply.github.com> Co-authored-by: Martin Costello <martin@martincostello.com> Co-authored-by: martintmk <103487740+martintmk@users.noreply.github.com> Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com>
1 parent c82d027 commit e36c495

17 files changed

+710
-0
lines changed

docs/core/resilience/http-resilience.md

Lines changed: 223 additions & 0 deletions
Large diffs are not rendered by default.

docs/core/resilience/index.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
---
2+
title: Introduction to resilient app development
3+
description: Learn about resiliency as it relates to .NET and how to build a resilience pipeline.
4+
author: IEvangelist
5+
ms.author: dapine
6+
ms.date: 10/20/2023
7+
---
8+
9+
# Introduction to resilient app development
10+
11+
Resiliency is the ability of an app to recover from failures and continue to function. In the context of .NET programming, resilience is achieved by designing apps that can handle failures gracefully and recover quickly. To help build resilient apps in .NET, the following two packages are available on NuGet:
12+
13+
| NuGet package | Description |
14+
|--|--|
15+
| [📦 Microsoft.Extensions.Resilience](https://www.nuget.org/packages/Microsoft.Extensions.Resilience) | This NuGet package provides mechanisms to harden apps against transient failures. |
16+
| [📦 Microsoft.Extensions.Http.Resilience](https://www.nuget.org/packages/Microsoft.Extensions.Http.Resilience) | This NuGet package provides resilience mechanisms specifically for the <xref:System.Net.Http.HttpClient> class. |
17+
18+
These two NuGet packages are built on top of [Polly](https://github.com/App-vNext/Polly), which is a popular open-source project. Polly is a .NET resilience and transient fault-handling library that allows developers to express strategies such as retry, circuit breaker, timeout, bulkhead isolation, rate-limiting, fallback, and hedging in a fluent and thread-safe manner.
19+
20+
> [!IMPORTANT]
21+
> The [Microsoft.Extensions.Http.Polly](https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly) NuGet package is deprecated. Use either of the aforementioned packages instead.
22+
23+
## Get started
24+
25+
To get started with resilience in .NET, install the [Microsoft.Extensions.Resilience](https://www.nuget.org/packages/Microsoft.Extensions.Resilience) NuGet package.
26+
27+
### [.NET CLI](#tab/dotnet-cli)
28+
29+
```dotnetcli
30+
dotnet add package Microsoft.Extensions.Resilience --version 8.0.0
31+
```
32+
33+
### [PackageReference](#tab/package-reference)
34+
35+
```xml
36+
<PackageReference Include="Microsoft.Extensions.Resilience" Version="8.0.0" />
37+
```
38+
39+
---
40+
41+
For more information, see [dotnet add package](../tools/dotnet-add-package.md) or [Manage package dependencies in .NET applications](../tools/dependencies.md).
42+
43+
## Build a resilience pipeline
44+
45+
To use resilience, you must first build a pipeline of resilience-based strategies. Each configured strategy executes in order of configuration. In other words, order is important. The entry point is an extension method on the <xref:Microsoft.Extensions.DependencyInjection.IServiceCollection> type, named `AddResiliencePipeline`. This method takes an identifier of the pipeline and delegate that configures the pipeline. The delegate is passed an instance of `ResiliencePipelineBuilder`, which is used to add resilience strategies to the pipeline.
46+
47+
Consider the following string-based `key` example:
48+
49+
:::code language="csharp" source="snippets/resilience/Program.cs" id="setup":::
50+
51+
The preceding code:
52+
53+
- Creates a new `ServiceCollection` instance.
54+
- Defines a `key` to identify the pipeline.
55+
- Adds a resilience pipeline to the `ServiceCollection` instance.
56+
- Configures the pipeline with a retry strategy, circuit breaker strategy, and timeout strategy.
57+
58+
Each pipeline is configured for a given `key`, and each `key` is used to identify its corresponding `ResiliencePipeline` when getting the pipeline from the provider. The `key` is a generic type parameter of the `AddResiliencePipeline` method.
59+
60+
### Resilience pipeline builder extensions
61+
62+
To add a strategy to the pipeline, call any of the available `Add*` extension methods on the `ResiliencePipelineBuilder` instance.
63+
64+
- `AddRetry`: Try again if something fails, which is useful when the problem is temporary and might go away.
65+
- `AddCircuitBreaker`: Stop trying if something is broken or busy, which benefits you by avoiding wasted time and making things worse.
66+
- `AddTimeout`: Give up if something takes too long, which can improve performance by freeing up resources.
67+
- `AddRateLimiter`: Limit how many requests you make or accept, which enables you to control load.
68+
- `AddFallback`: Do something else when experiencing failures, which improves user experience.
69+
- `AddHedging`: Do more than one thing at the same time and take the fastest one, which can improve responsiveness.
70+
71+
For more information, see [Resilience strategies](https://www.pollydocs.org/strategies).
72+
73+
## Add enrichment
74+
75+
Enrichment is the automatic augmentation of telemetry with well-known state, in the form of name/value pairs. For example, an app might emit a log that includes the _operation_ and _result code_ as columns to represent the outcome of some operation. In this situation and depending on peripheral context, enrichment adds _Cluster name_, _Process name_, _Region_, _Tenant ID_, and more to the log as it's sent to the telemetry backend. When enrichment is added, the app code doesn't need to do anything extra to benefit from enriched metrics.
76+
77+
Imagine 1,000 globally distributed service instances generating logs and metrics. When you encounter an issue on your service dashboard, it's crucial to quickly identify the problematic region or data center. Enrichment ensures that metric records contain the necessary information to pinpoint failures in distributed systems. Without enrichment, the burden falls on the app code to internally manage this state, integrate it into the logging process, and manually transmit it. Enrichment simplifies this process, seamlessly handling it without affecting the app's logic.
78+
79+
### Add resilience enrichment
80+
81+
In addition to registering a resilience pipeline, you can also register resilience enrichment. To add enrichment, call the `AddResilienceEnricher` extensions method on the `IServiceCollection` instance.
82+
83+
:::code language="csharp" source="snippets/resilience/Program.cs" id="enricher":::
84+
85+
By calling the `AddResilienceEnricher` extension method, you're adding dimensions on top of the default ones that are built into the underlying Polly library. The following enrichment dimensions are added:
86+
87+
- Exception enrichment based on the <xref:Microsoft.Extensions.Diagnostics.ExceptionSummarization.IExceptionSummarizer>, which provides a mechanism to summarize exceptions for use in telemetry.
88+
- Result enrichment based on the <xref:Microsoft.Extensions.Resilience.FailureResultContext>, which captures the dimensions metered for transient fault failures.
89+
- Request metadata enrichment based on <xref:Microsoft.Extensions.Http.Telemetry.RequestMetadata>, which holds the request metadata for telemetry.
90+
91+
For more information, see [Polly: Telemetry metrics](https://www.pollydocs.org/advanced/telemetry#metrics).
92+
93+
## Use resilience pipeline
94+
95+
To use a configured resilience pipeline, you must get the pipeline from a `ResiliencePipelineProvider<TKey>`. When you added the pipeline earlier, the `key` was of type `string`, so you must get the pipeline from the `ResiliencePipelineProvider<string>`.
96+
97+
:::code language="csharp" source="snippets/resilience/Program.cs" id="pipeline":::
98+
99+
The preceding code:
100+
101+
- Builds a service provider from the `ServiceCollection` instance.
102+
- Gets the `ResiliencePipelineProvider<string>` from the service provider.
103+
- Retrieves the `ResiliencePipeline` from the `ResiliencePipelineProvider<string>`.
104+
105+
## Execute resilience pipeline
106+
107+
To use the resilience pipeline, call any of the available `Execute*` methods on the `ResiliencePipeline` instance. For example, consider an example call to `ExecuteAsync` method:
108+
109+
:::code language="csharp" source="snippets/resilience/Program.cs" id="execute":::
110+
111+
The preceding code executes the delegate within the `ExecuteAsync` method. When there are failures, the configured strategies are executed. For example, if the `RetryStrategy` is configured to retry three times, the delegate is executed three times before the failure is propagated.
112+
113+
## Next steps
114+
115+
> [!div class="nextstepaction"]
116+
> [Build resilient HTTP apps: Key development patterns](http-resilience.md)
156 KB
Loading
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
namespace Http.Resilience.Example;
2+
3+
public record class Comment(
4+
int PostId, int Id, string Name, string Email, string Body);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System.Net.Http.Json;
2+
3+
namespace Http.Resilience.Example;
4+
5+
/// <summary>
6+
/// An example client service, that relies on the <see cref="HttpClient"/> instance.
7+
/// </summary>
8+
/// <param name="client">The given <see cref="HttpClient"/> instance.</param>
9+
internal sealed class ExampleClient(HttpClient client)
10+
{
11+
/// <summary>
12+
/// Returns an <see cref="IAsyncEnumerable{T}"/> of <see cref="Comment"/>s.
13+
/// </summary>
14+
public IAsyncEnumerable<Comment?> GetCommentsAsync()
15+
{
16+
return client.GetFromJsonAsAsyncEnumerable<Comment>("/comments");
17+
}
18+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System.Net;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Http.Resilience;
4+
using Polly;
5+
6+
internal partial class Program
7+
{
8+
private static void WithCustomHandler(IHttpClientBuilder httpClientBuilder)
9+
{
10+
// <custom>
11+
httpClientBuilder.AddResilienceHandler(
12+
"CustomPipeline",
13+
static builder =>
14+
{
15+
// See: https://www.pollydocs.org/strategies/retry.html
16+
builder.AddRetry(new HttpRetryStrategyOptions
17+
{
18+
// Customize and configure the retry logic.
19+
BackoffType = DelayBackoffType.Exponential,
20+
MaxRetryAttempts = 5,
21+
UseJitter = true
22+
});
23+
24+
// See: https://www.pollydocs.org/strategies/circuit-breaker.html
25+
builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
26+
{
27+
// Customize and configure the circuit breaker logic.
28+
SamplingDuration = TimeSpan.FromSeconds(10),
29+
FailureRatio = 0.2,
30+
MinimumThroughput = 3,
31+
ShouldHandle = static args =>
32+
{
33+
return ValueTask.FromResult(args is
34+
{
35+
Outcome.Result.StatusCode:
36+
HttpStatusCode.RequestTimeout or
37+
HttpStatusCode.TooManyRequests
38+
});
39+
}
40+
});
41+
42+
// See: https://www.pollydocs.org/strategies/timeout.html
43+
builder.AddTimeout(TimeSpan.FromSeconds(5));
44+
});
45+
// </custom>
46+
}
47+
48+
private static void WithAdvancedCustomHandler(IHttpClientBuilder httpClientBuilder)
49+
{
50+
// <advanced>
51+
httpClientBuilder.AddResilienceHandler(
52+
"AdvancedPipeline",
53+
static (ResiliencePipelineBuilder<HttpResponseMessage> builder,
54+
ResilienceHandlerContext context) =>
55+
{
56+
// Enable reloads whenever the named options change
57+
context.EnableReloads<HttpRetryStrategyOptions>("RetryOptions");
58+
59+
// Retrieve the named options
60+
var retryOptions =
61+
context.GetOptions<HttpRetryStrategyOptions>("RetryOptions");
62+
63+
// Add retries using the resolved options
64+
builder.AddRetry(retryOptions);
65+
});
66+
// </advanced>
67+
}
68+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.Http.Resilience;
3+
4+
internal partial class Program
5+
{
6+
private static void WithStandardHedgingHandler(IHttpClientBuilder httpClientBuilder)
7+
{
8+
// <standard>
9+
httpClientBuilder.AddStandardHedgingHandler();
10+
// </standard>
11+
}
12+
13+
private static void WithConfiguredStandardHedgingHandler(IHttpClientBuilder httpClientBuilder)
14+
{
15+
// <ordered>
16+
httpClientBuilder.AddStandardHedgingHandler(static (IRoutingStrategyBuilder builder) =>
17+
{
18+
// Hedging allows sending multiple concurrent requests
19+
builder.ConfigureOrderedGroups(static options =>
20+
{
21+
options.Groups.Add(new UriEndpointGroup()
22+
{
23+
Endpoints =
24+
{
25+
// Imagine a scenario where 3% of the requests are
26+
// sent to the experimental endpoint.
27+
new() { Uri = new("https://example.net/api/experimental"), Weight = 3 },
28+
new() { Uri = new("https://example.net/api/stable"), Weight = 97 }
29+
}
30+
});
31+
});
32+
});
33+
// </ordered>
34+
35+
// <weighted>
36+
httpClientBuilder.AddStandardHedgingHandler(static (IRoutingStrategyBuilder builder) =>
37+
{
38+
// Hedging allows sending multiple concurrent requests
39+
builder.ConfigureWeightedGroups(static options =>
40+
{
41+
options.SelectionMode = WeightedGroupSelectionMode.EveryAttempt;
42+
43+
options.Groups.Add(new WeightedUriEndpointGroup()
44+
{
45+
Endpoints =
46+
{
47+
// Imagine A/B testing
48+
new() { Uri = new("https://example.net/api/a"), Weight = 33 },
49+
new() { Uri = new("https://example.net/api/b"), Weight = 33 },
50+
new() { Uri = new("https://example.net/api/c"), Weight = 33 }
51+
}
52+
});
53+
});
54+
});
55+
// </weighted>
56+
}
57+
}
58+
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Polly;
3+
4+
internal partial class Program
5+
{
6+
private static void WithStandardHandler(IHttpClientBuilder httpClientBuilder)
7+
{
8+
// <standard>
9+
httpClientBuilder.AddStandardResilienceHandler();
10+
// </standard>
11+
}
12+
13+
private static void WithConfiguredStandardHandler(IHttpClientBuilder httpClientBuilder)
14+
{
15+
// <configure>
16+
httpClientBuilder.AddStandardResilienceHandler(static options =>
17+
{
18+
options.Retry.BackoffType = DelayBackoffType.Linear;
19+
});
20+
// </configure>
21+
}
22+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Microsoft.Extensions.Configuration;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Hosting;
4+
using Microsoft.Extensions.Http.Resilience;
5+
6+
internal partial class Program
7+
{
8+
private static void ConfigureRetryOptions(HostApplicationBuilder builder)
9+
{
10+
// <options>
11+
var section = builder.Configuration.GetSection("RetryOptions");
12+
13+
builder.Services.Configure<HttpRetryStrategyOptions>(section);
14+
// </options>
15+
}
16+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
3+
namespace Http.Resilience.Example;
4+
5+
internal partial class Program
6+
{
7+
private static void CreateServiceCollection()
8+
{
9+
// <services>
10+
var services = new ServiceCollection();
11+
12+
var httpClientBuilder = services.AddHttpClient<ExampleClient>(
13+
configureClient: static client =>
14+
{
15+
client.BaseAddress = new("https://jsonplaceholder.typicode.com");
16+
});
17+
// </services>
18+
}
19+
}

0 commit comments

Comments
 (0)