8 min readStart here

Wiring it up

Integrate NResilience into ASP.NET Core using Dependency Injection, configuration binding with hot reload, and health checks.

getting-startedaspnetcoredependency-injection

TL;DR: Moving from manual policy creation to DI. Use AddResilience to bind policies to configuration with hot-reload support and integrate with ASP.NET Core health checks.

The gap between a snippet and an app

In the first few posts, we looked at policies as values. We created them with with expressions and ran them through RunAsync. For a small tool or a unit test, static readonly fields are plenty.

But in a real ASP.NET Core or worker app, you don't want your resilience settings hard-coded. You want them in appsettings.json so you can tune a timeout or a retry count without a full recompile and redeploy. You also want those policies available via Dependency Injection so your services don't have to track down static fields.

Registering the roster

NResilience uses a "roster" pattern. Instead of injecting ten different policies into a constructor, you inject one service: IResiliencePolicies.

This service acts as a named registry. You ask it for a policy by name, and it gives you the current version of that policy.

To get started, use AddResilience() in your Program.cs.

var builder = WebApplication.CreateBuilder(args);

// Register a single policy by name
builder.Services.AddResilience("payments", Resilience.Http with
{
    Deadline = TimeSpan.FromSeconds(10)
});

// Or register every child of a config section as a policy, keyed by name
builder.Services.AddResilience(builder.Configuration.GetSection("Resilience"));

builder.Services.AddControllers();

Now, any service in your app can resolve the roster and grab a policy:

public class PaymentService(IResiliencePolicies policies, HttpClient http)
{
    public async Task ProcessPayment(Payment req)
    {
        // Resolve on every call, not into a field: a captured policy is a snapshot,
        // and a configuration reload will never reach it.
        var policy = policies["payments"];

        await policy.RunAsync(async attempt =>
        {
            await http.PostAsJsonAsync("/pay", req);
        });
    }
}

Binding to JSON and hot reload

Hard-coding the with expression in Program.cs is better than a static field, but still requires a restart. The real power comes from binding to configuration.

If you use builder.Services.AddResilience(builder.Configuration.GetSection("Resilience")), the library looks for a section in your JSON that looks like this:

{
  "Resilience": {
    "payments": {
      "Preset": "Http",
      "Deadline": "00:00:10",
      "Attempts": 3,
      "Backoff": {
        "TransientBase": "00:00:00.200",
        "Factor": 2.0
      }
    },
    "inventory": {
      "Preset": "Http",
      "Deadline": "00:00:02",
      "Attempts": 2
    }
  }
}

Because this uses IOptionsMonitor under the hood, these settings support hot reload. If you change the Deadline in your JSON file and save it, the next call to policies.Get("payments") returns a new policy with the updated value.

The state survives the reload

Here is the part that usually trips people up when building their own config systems: if you swap the policy instance, do you lose the state?

If you have a circuit breaker (which we'll cover in the "open the breaker" post) or a retry budget (the "don't be the storm" post), those have memory. They track how many failures have happened or how many tokens are left.

In NResilience, that state is pinned to the registration name ("payments"), not the policy object itself. When you reload the JSON, the library swaps the definition of the policy, but the state of the breaker and budget stays put. Your app doesn't "forget" that the payments API is currently down just because you tweaked a timeout value.

Visibility via Health Checks

If a circuit breaker opens, your app is still "healthy" in the sense that the process is running and the port is open. But it's "degraded" because it's actively refusing calls to a dependency.

You can expose this state to your orchestrator (like Kubernetes) using the built-in health check integration.

builder.Services.AddHealthChecks()
    .AddResilienceHealthCheck();

This registers a check that scans every breaker and retry budget in the process.

By default, an open breaker reports HealthStatus.Degraded rather than Unhealthy. This is a deliberate choice. An Unhealthy status often triggers a pod restart in Kubernetes. Restarting the process doesn't fix a failing downstream API - it just clears the breaker's memory and lets the storm hit the dependency again. Degraded tells your monitoring system that something is wrong without triggering a useless restart loop.

You can also watch specific policies or tune the thresholds:

builder.Services.AddHealthChecks().AddResilienceHealthCheck(o =>
{
    o.BudgetThreshold = 0.75; // Report degraded if a budget is 75% used
    o.Watch("payments", PaymentsBreaker); // A breaker DI doesn't own
});

Draining on shutdown

AddResilience also subscribes to IHostApplicationLifetime.ApplicationStopping. When the host starts shutting down, no call starts another attempt, no hedge is armed, and every deadline clamps to what is left of the grace period.

The reason is the same one that makes Degraded the right health status. A pod gets about 30 seconds after SIGTERM, and the load balancer stops routing to it almost immediately. A call that is three attempts into a retry sequence when that happens is holding a connection and delaying the rollout to send a request nobody will read the answer to. So it stops, with the failure it already has, as StopReason.Draining.

In-flight attempts are left alone. Cancelling work that might still complete is the host's job, and the host already has the token for it.

Nothing to turn on. Tune it if your shutdown timeout is not the default 30 seconds:

builder.Services.AddResilienceDraining(o => o.Grace = TimeSpan.FromSeconds(20));

DrainOnShutdown = false turns it off, for a worker whose outbound calls have to finish during shutdown. And if you are not using the hosting package at all, Draining.Begin() in the core package is the same latch - Draining.IsDraining is what a readiness probe reads.

Summary

You've now moved from raw policy values to a production-ready DI setup. You have a central roster, configuration that can change without restarts, and a way to tell the rest of your stack when your dependencies are struggling.

The next few posts move into the "rules of the call" - how NResilience actually decides when to retry, when to time out, and how it handles cancellation.

Retry the right things covers the Classifier and the four verdicts that determine whether to retry, how long to wait, and if the dependency is unhealthy.