Client and handler have different lifetimes
CreateClient returns a new HttpClient, while the factory can reuse a handler with its connection pool. Disposing a factory-created client does not immediately destroy the shared handler. This separation permits resource reuse without requiring one client instance retained throughout the application.
A typed client captured by a singleton can outlive intended handler renewal and impede reaction to DNS changes. A singleton can receive IHttpClientFactory and request named clients when needed. Another documented strategy uses connections with configured PooledConnectionLifetime; merely increasing an old client’s timeout is insufficient.
The handler scope is not the incoming request
The factory creates a separate DI scope per handler. That scope differs from the incoming HTTP request scope and can outlive it. A scoped service injected into the handler must not be interpreted as the current identity of each request passing through the application.
Retaining a user or token in reused handler state can carry that value across requests. Keep operation-specific data within a boundary supplying it for the corresponding send without retaining shared state. Naming a client after a remote service does not automatically create per-user identity isolation.
The deadline includes every attempt
The resilience handler distinguishes total and per-attempt timeout. Total timeout bounds execution including retries; attempt timeout bounds one call. Backoff and additional attempts consume time observed by the caller. Three ten-second attempts do not necessarily fit a twenty-five-second overall deadline.
The example client has an eight-second total limit, two seconds per attempt and at most one retry. These illustrative choices must still fit the complete operation deadline with margin for remaining work. Circuit breaking and concurrency controls address other conditions; they do not replace defining that budget.
A retry can repeat the effect
The standard handler can retry all methods unless policy is changed. A shipment-creating POST may have committed at the server before its response was lost. Client timeout does not prove remote rollback. More attempts can increase effects without increasing certainty about which attempt caused them.
DisableFor selects methods without retries and DisableForUnsafeHttpMethods disables them for the documented unsafe-method set. Safe and idempotent are different concepts: the decision must consider the actual contract. With an uncertain outcome, use the original identity and available lookup or reconciliation policy before assuming a new operation is acceptable.
Make output caching available and enable it
AddOutputCache and UseOutputCache make the mechanism available. Storage still needs a base policy or endpoint opt-in such as CacheOutput. Under the default policy, only 200 responses to GET or HEAD are stored, excluding authenticated requests and responses with Set-Cookie. Do not attribute those conditions to every custom policy without inspecting it.
The example uses a public anonymous endpoint with fixed values. If the application has authentication and authorization, caching middleware must follow the documented order after those stages. A cached response must not bypass an access decision or expose data belonging to an earlier identity.
Keys, lifetime and eviction
An entry should only be shared when differences between requests do not change the stored representation. If price and label vary by currency and culture, omitting currency from a custom key can serve EUR to a USD request. A short TTL limits error duration but does not make that sharing correct.
Tag catalog selects entries for EvictByTagAsync when relevant data changes. A database change and eviction do not automatically become one transaction; failures between them must be observed. Fill locking mitigates stampedes but neither evicts entries nor guarantees freshness by itself.
Two readings of the example
The first reading concerns HTTP configuration: supplier is a named client with explicit limits and retry policy. The .example domain is illustrative and no endpoint calls it in this program. This configuration proves neither connectivity, supplier authentication nor absence of duplicate effects.
The second reading follows /public-price: EUR/pt produces 100 and a Portuguese label; USD/en produces 110 and an English label. The key distinguishes both parameters and lifetime is thirty seconds. Values are fixed for teaching, not real prices. Assessments ask you to select outcomes and diagnose policy changes without executing requests or code.
// Program.cs: ASP.NET Core 10.0 Web SDK, net10.0, ImplicitUsings enabled.
// Microsoft.Extensions.Http.Resilience 10.x available.
// The named client is configured for illustration; no endpoint calls it here.
// Price values and labels are a fixed public teaching model.
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Http.Resilience;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient("supplier", client =>
client.BaseAddress = new Uri("https://supplier.example/"))
.AddStandardResilienceHandler(options =>
{
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(8);
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(2);
options.Retry.MaxRetryAttempts = 1;
options.Retry.Delay = TimeSpan.FromMilliseconds(200);
options.Retry.DisableForUnsafeHttpMethods();
});
builder.Services.AddOutputCache();
var app = builder.Build();
app.UseOutputCache();
app.MapGet("/public-price", (string currency, string culture) =>
{
if (currency is not ("EUR" or "USD") || culture is not ("pt" or "en"))
return Results.BadRequest();
var amount = currency == "EUR" ? 100 : 110;
var label = culture == "pt" ? "Preço de exemplo" : "Example price";
return Results.Ok(new { currency, culture, amount, label });
}).CacheOutput(policy => policy
.SetVaryByQuery("currency", "culture")
.Expire(TimeSpan.FromSeconds(30))
.Tag("catalog"));
app.Run();
The model produces different prices for EUR and USD and different labels for pt and en. The key retains both dimensions. The supplier client has an eight-second total limit and a two-second attempt limit; the endpoint does not call it and configuration proves no connectivity.
Common pitfalls
Capturing a typed client indefinitely; retaining identity in reused handlers; retrying POST after timeout without knowing the effect; confusing attempt timeout with an overall deadline; omitting currency from the key; treating locking as eviction.
Related topics: Persistence and concurrency · Authentication and authorization · Health, observability and diagnosis
Reuse infrastructure without incorrectly sharing identity. Bound the set of attempts, establish effect semantics and share a cache entry only across requests with equivalent representations.
Reference: Factory clients and handler lifetimes · ASP.NET Core 10.0 / .NET 10 / C# 14