Outbound integration resilience: Polly retry/circuit-breaker wrapper
Every outbound HTTP call to an external partner/integration goes through one shared Polly-backed policy rather than a hand-rolled retry loop per integration. Built 2026-08-17.
OutboundIntegrationResilience.AddOutboundIntegrationResilience()
The one extension method every current and future outbound-integration
HttpClient registration chains onto (Infrastructure/Http/ OutboundIntegrationResilience.cs). Wraps Microsoft.Extensions.Http. Resilience's own AddStandardResilienceHandler() (rate limiter →
total-request timeout → retry → circuit breaker → per-attempt timeout)
rather than a hand-rolled Polly v8 pipeline, tuning only the circuit breaker
(FailureRatio=0.5, MinimumThroughput=5, SamplingDuration/
BreakDuration=30s) down from the library's high-traffic-assuming defaults
to something a low-volume client (occasional SSO logins, occasional webhook
fires) can actually trip.
:::caution Circuit isolation is per client name, not per real-world partner
Each named/typed client gets its own independent circuit breaker, matching
IHttpClientFactory's per-client handler lifetime — but "AutomationWebhook"
fires to a different URL per Scenario and they all share one circuit
breaker, so a failing partner can trip every other, healthy partner behind
that same name for the 30s break duration. Accepted for today's low webhook
volume / single live SSO provider — revisit (key per-target-host instead) if
concurrent multi-partner volume becomes real.
:::
Current call sites
OidcIdentityProvider's typed client (SSO discovery/token exchange) and the
"AutomationWebhook" named client (CallWebhookAutomationAction's partner
POSTs) — both registered in Infrastructure/DependencyInjection.cs. Add a
new outbound integration by chaining this same extension; never
re-implement retry/circuit-breaker logic per integration.
GraphEmailTransportSender's Microsoft Graph SDK calls are deliberately
excluded — the Graph SDK manages its own retry/backoff internally, and
wrapping it in a second, independent resilience layer would double-retry
rather than add coverage.
Idempotency, deliberately not solved here
The default retry predicate retries POSTs too (both current integrations are
POST) — accepted as the same at-least-once shape as this codebase's other
documented at-least-once windows (inbound-email redelivery, automation-engine
ack timing). See Idempotency-Key Pattern for the
matching guard on the receiving side. Do not add
DisableForUnsafeHttpMethods() here without reconsidering this tradeoff
first.
:::note A retired DI-ordering landmine
AddStandardResilienceHandler() registers a keyed ServiceDescriptor that
used to silently break the classic Microsoft.ApplicationInsights 2.22.0
SDK's own service-collection scan unless telemetry registration ran before
AddInfrastructure(). The constraint no longer applies — the 2026-08-25
migration to the Azure Monitor OpenTelemetry Distro (see
Distributed Tracing) retired the 2.x
SDK entirely. Worth recognizing the failure mode (WebApplicationFactory
tests dying with "No service for type 'X' has been registered", silent
everywhere else) if a similarly narrow DI-ordering constraint shows up again
with some other library.
:::
Reference files
OutboundIntegrationResilience.cs, OutboundIntegrationResilienceTests.cs,
DependencyInjection.cs (OidcIdentityProvider/"AutomationWebhook"
registrations).