Derniers tips / Mémos
Modélise nativement « exactement l'un de ces types », avec un switch exhaustif vérifié par le compilateur.
Fin des state machines générées par le compilateur : moins d'allocations et des stack traces enfin lisibles.
Recherche sémantique directement dans SQL Server 2025, sans base vectorielle séparée, en LINQ typé.
Framework UI cross-platform pour .NET : Windows, macOS, Linux, iOS, Android et WebAssembly.
Découvrez les nouvelles APIs asynchrones pour la compression ZIP et les améliorations de performance dans .NET 10
Microsoft.Extensions.Resilience : pipelines de résilience intégrés à .NET
Ce package fournit un modèle standardisé pour définir, composer et observer des stratégies de résilience (basées sur Polly) directement via l'injection de dépendances .NET.
L'objectif est de réduire le code infrastructure répétitif et d'offrir une configuration centralisée, observable et testable pour vos appels externes et internes.
Installation
# Paquets principaux
dotnet add package Microsoft.Extensions.Resilience
# Intégration HttpClient
dotnet add package Microsoft.Extensions.Http.ResilienceLe premier paquet expose le modèle de pipeline; le second ajoute des helpers spécifiques à HttpClientFactory (AddResilienceHandler).
Concepts clés
Un pipeline est une séquence ordonnée de stratégies. On l'enregistre avec AddResiliencePipeline<TContext>(). On l'exécute via IResiliencePipelineProvider. Chaque stratégie (retry, timeout, circuit breaker, hedging...) est modulaire.
// Program.cs (ou dans un module d'enregistrement de services)
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Resilience;
using Polly; // (stratégies sous-jacentes)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddResiliencePipeline<string>(
pipelineName: "standard-pipeline",
configure: (pipelineBuilder, context) =>
{
// 1. Retry exponentiel
pipelineBuilder.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(200),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
ShouldHandle = new PredicateBuilder().Handle<Exception>()
});
// 2. Timeout global
pipelineBuilder.AddTimeout(TimeSpan.FromSeconds(5));
// 3. Circuit breaker
pipelineBuilder.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(15)
});
});
var app = builder.Build();
app.MapGet("/work", async (IResiliencePipelineProvider<string> provider) =>
{
var pipeline = provider.GetPipeline("standard-pipeline");
return await pipeline.ExecuteAsync(async token =>
{
// Opération potentiellement fragile
await Task.Delay(100, token);
return Results.Ok("OK");
}, context: "partition-A"); // partition optionnelle
});
app.Run();Stratégies intégrées
| Stratégie | Quand l'utiliser ? | Exemple |
|---|---|---|
| Retry | Erreurs transitoires (5xx, IOException) | AddRetry(...) |
| Timeout | Limiter la durée totale d'une opération | AddTimeout(5s) |
| CircuitBreaker | Empêcher l'avalanche quand la cible tombe | AddCircuitBreaker(...) |
| Hedging | Réduire la latence p95/p99 via appels parallèles contrôlés | AddHedging(...) |
| Fallback | Fournir une réponse de secours | AddFallback(...) |
| RateLimiter / Bulkhead | Limiter concurrence ou débit | AddConcurrencyLimiter(...) |
| Chaos | Tests de robustesse (fault injection) | AddChaosLatency(...) |
Intégration HttpClient
AddResilienceHandler enchaîne les stratégies côté pipeline HTTP. Les stratégies s'appliquent avant l'envoi effectif (SocketsHttpHandler).
builder.Services.AddHttpClient("catalog")
.AddResilienceHandler("http-standard", (builder, context) =>
{
builder.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(250),
BackoffType = DelayBackoffType.Exponential,
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.HandleResult(r => (int)r.StatusCode >= 500)
.Handle<HttpRequestException>()
});
builder.AddTimeout(TimeSpan.FromSeconds(4));
builder.AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
{
FailureRatio = 0.2,
SamplingDuration = TimeSpan.FromSeconds(20),
MinimumThroughput = 20,
BreakDuration = TimeSpan.FromSeconds(10)
});
});
// Utilisation via IHttpClientFactory
app.MapGet("/products", async (IHttpClientFactory factory) =>
{
var client = factory.CreateClient("catalog");
var response = await client.GetAsync("https://example.com/api/products");
return Results.Text($"Status: {response.StatusCode}");
});Hedging (latence optimisée)
La stratégie lance des requêtes alternatives si la première tarde ou échoue selon un prédicat. À manier avec parcimonie pour ne pas surcharger la cible.
builder.Services.AddResiliencePipeline<HttpResponseMessage>(
"search-hedged",
(pb, ctx) =>
{
pb.AddHedging(new HedgingStrategyOptions<HttpResponseMessage>
{
MaxHedgedAttempts = 3,
Delay = TimeSpan.FromMilliseconds(150),
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.HandleResult(r => (int)r.StatusCode >= 500 || r.StatusCode == System.Net.HttpStatusCode.RequestTimeout),
OnHedging = args =>
{
Console.WriteLine($"Hedge attempt #{args.AttemptNumber}");
return default;
}
});
pb.AddTimeout(TimeSpan.FromSeconds(2));
});
app.MapGet("/search", async (IResiliencePipelineProvider<HttpResponseMessage> provider) =>
{
var pipeline = provider.GetPipeline("search-hedged");
var result = await pipeline.ExecuteAsync(async token =>
{
using var http = new HttpClient();
return await http.GetAsync("https://example.com/api/search?q=test", token);
});
return Results.Text(result.StatusCode.ToString());
});Partitionnement (context key)
Vous pouvez passer un 'context' lors de ExecuteAsync pour différencier instrumentation, quotas ou limites par client/locataire.
builder.Services.AddResiliencePipeline<string>("per-customer", (pb, ctx) =>
{
pb.AddRetry(new RetryStrategyOptions { MaxRetryAttempts = 2, Delay = TimeSpan.FromMilliseconds(100) });
});
app.MapGet("/customer/{id}", async (string id, IResiliencePipelineProvider<string> provider) =>
{
var pipeline = provider.GetPipeline("per-customer");
return await pipeline.ExecuteAsync(async token =>
{
// Logique dépendant du client
await Task.Delay(50, token);
return Results.Ok(new { Customer = id });
}, context: id); // le context devient la clé de partition
});Un autre exemple concret sans HTTP
Création d'un pipeline autonome dans une classe utilitaire, puis utilisation dans un repository pour entourer un appel SQL. Remplacez ADO.NET par EF Core ou Dapper si vous préférez.
using System.Data;
using Microsoft.Data.SqlClient;
using Polly;
public sealed class DatabaseResilience
{
private readonly ResiliencePipeline _pipeline;
public DatabaseResilience()
{
_pipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(200),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
ShouldHandle = new PredicateBuilder()
.Handle<SqlException>()
.Handle<TimeoutException>()
})
.AddTimeout(TimeSpan.FromSeconds(3))
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.3,
SamplingDuration = TimeSpan.FromSeconds(20),
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(10)
})
.Build();
}
public async Task<T?> ExecuteAsync<T>(Func<CancellationToken, Task<T?>> dbCall, CancellationToken ct = default)
{
var context = ResilienceContextPool.Shared.Get(ct);
try
{
return await _pipeline.ExecuteAsync(async (ctx) =>
{
var result = await dbCall(ctx.CancellationToken);
return result;
}, context);
}
finally
{
ResilienceContextPool.Shared.Return(context);
}
}
}
// Usage dans un repository "classique"
public sealed class ProductRepository
{
private readonly string _connString;
private readonly DatabaseResilience _resilience = new();
public ProductRepository(string connectionString)
=> _connString = connectionString;
public Task<string?> GetNameAsync(int id, CancellationToken ct = default)
=> _resilience.ExecuteAsync<string?>(async token =>
{
await using var conn = new SqlConnection(_connString);
await conn.OpenAsync(token);
await using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT TOP(1) Name FROM Products WHERE Id = @id";
var p = cmd.CreateParameter();
p.ParameterName = "@id"; p.Value = id; cmd.Parameters.Add(p);
var result = await cmd.ExecuteScalarAsync(token);
return result as string;
}, ct);
}
// Remarque: vous pouvez remplacer ADO.NET par Dapper/EF Core; l'important est que l'appel
// DB soit exécuté dans la lambda du pipeline.Observabilité
Les pipelines exposent des événements (logs, métriques) exploitables par OpenTelemetry. Chaque tentative, ouverture/fermeture de circuit, timeout ou hedging peut être tracé.
// Activer la télémétrie (ex: OpenTelemetry + logs)
builder.Services.AddLogging();
builder.Services.AddOpenTelemetry().WithMetrics(m => { /* ... */ }).WithTracing(t => { /* ... */ });
// Les events d'exécution des stratégies sont émis automatiquement (compteurs, logs...)Configuration via appsettings
Les options peuvent être bindées depuis la configuration. Vous pouvez créer un binder custom ou un wrapper pour charger dynamiquement des options par environnement.
{
"Resilience": {
"Pipelines": {
"standard-pipeline": {
"Retry": { "MaxRetryAttempts": 3, "Delay": "00:00:00.200", "UseJitter": true },
"Timeout": { "Timeout": "00:00:05" },
"CircuitBreaker": { "FailureRatio": 0.5, "SamplingDuration": "00:00:30", "BreakDuration": "00:00:15", "MinimumThroughput": 10 }
}
}
}
}Dans Program.cs : récupérez IConfiguration et appliquez les valeurs lors du build des pipelines.
Bonnes pratiques
- Ne pas multiplier les retries empilés (éviter cascades).
- Isoler les stratégies par type d'appel (lecture, écriture).
- Définir des timeouts explicites (pas uniquement default).
- Surveiller métriques (taux d'échec, ouvertures de circuits).
- Limiter Hedging et Chaos aux environnements de test ou à quelques endpoints.
- Utiliser partitioning pour différencier clients sensibles.
- Documenter les paramètres (durées, seuils) dans le repo.
En résumé
Microsoft.Extensions.Resilience fournit une surcouche native .NET pour construire des pipelines de résilience cohérents, testables et observables.
Il standardise l'usage des stratégies Polly et simplifie la maintenance à grande échelle dans des architectures distribuées.