Dependency Injection
The Kevlar.Extensions.DependencyInjection package registers named shields with Microsoft.Extensions.DependencyInjection and exposes them through a registry and as keyed services.
dotnet add package Kevlar.Extensions.DependencyInjection
Registering shields
// A shield instance, under a name:
services.AddShield("github",
Shield.Timeout(TimeSpan.FromSeconds(10)).Retry(3));
// A typed shield, built from the service provider (loggers, config, etc.):
services.AddShield<HttpResponseMessage>("downstream",
sp => HttpShield.WhenTransient().Retry(3).WithName("downstream"));
Because shields are immutable and thread-safe, each named shield is a singleton — which is exactly what you want: every consumer of "github" shares the same instance, and therefore the same circuit breaker state, rate-limit bucket and concurrency limit slots.
Consuming via the registry
public sealed class GitHubClient(IKevlarRegistry registry)
{
private readonly Shield _shield = registry.GetShield("github");
public Task<User> GetUserAsync(string id, CancellationToken ct) =>
_shield.ExecuteAsync(ct2 => FetchUserAsync(id, ct2), ct).AsTask();
}
Typed shields come back through GetShield<T>(name):
var httpShield = registry.GetShield<HttpResponseMessage>("downstream");
GetShield throws a KeyNotFoundException (with an actionable message) for unknown names; TryGetShield(name, out var shield) / TryGetShield<T>(...) are the non-throwing forms.
Registry semantics worth knowing:
- Shields are keyed by name + result type — an untyped
"api"shield and aShield<HttpResponseMessage>named"api"coexist independently. - Last registration for a given name wins, matching standard DI override behaviour.
- Ordinary factory registrations run lazily on first resolve and the result is cached — so every consumer shares one instance (and its strategy state), exactly like instance registrations.
AddShieldregisters theIKevlarRegistryfor you (AddKevlar()exists if you ever need just the registry).
Binding shields from configuration
AddShield(name, IConfiguration) builds a shield from a configuration section, so retry counts, timeouts and breaker thresholds are tunable per environment without a redeploy:
// appsettings.json
{
"Resilience": {
"GitHub": {
"Timeout": "00:00:30",
"Retry": { "MaxRetries": 5, "Backoff": "Exponential", "MaxDelay": "00:00:10" },
"CircuitBreaker": { "FailureRatio": 0.5, "BreakDuration": "00:00:15" },
"AttemptTimeout": "00:00:05"
}
}
}
services.AddShield("github", builder.Configuration.GetSection("Resilience:GitHub"));
The schema is ShieldDefinition: optional Timeout, Retry, CircuitBreaker, RateLimit, ConcurrencyLimit and AttemptTimeout sections, chained in that fixed order (outermost first). Only the sections you declare are added, and the defaults inside each section match the fluent API's.
Reloading configuration atomically
AddShield(name, IConfiguration) intentionally binds once, when first resolved. Use
AddReloadingShield when future configuration reloads must affect new operations:
services.AddReloadingShield(
"github",
builder.Configuration.GetSection("Resilience:GitHub"),
error => logger.LogError(error, "Rejected GitHub shield configuration"));
Consume the keyed provider and read Current once per operation:
public sealed class GitHubClient(
[FromKeyedServices("github")] IShieldProvider provider)
{
public Task<User> GetUserAsync(string id, CancellationToken ct)
{
Shield snapshot = provider.Current;
return snapshot.ExecuteAsync(
ct2 => FetchUserAsync(id, ct2),
ct).AsTask();
}
}
Registry consumers can call registry.GetShield("github") once per operation to obtain the
current snapshot. A keyed Shield resolved from DI is also one immutable snapshot; it does not
change after a reload. Every ordinary AddShield registration exposes an IShieldProvider too,
but its Current snapshot remains fixed.
On a valid change, Kevlar builds the entire replacement before one atomic publish. Operations
already using the prior snapshot finish on it. Invalid configuration keeps the last known-good
snapshot and invokes the optional failure callback; callback exceptions are contained so later
reloads remain active. Each successful replacement starts with fresh circuit-breaker,
rate-limiter, and concurrency-limiter state. The provider performs no binding, locking, or
allocation while reading Current; all rebuild work occurs on configuration change. Disposing
the service provider removes the change-token subscription.
Consuming as a keyed service
Named shields are also registered as keyed services, so you can skip the registry entirely:
public sealed class GitHubClient([FromKeyedServices("github")] Shield shield)
{
public Shield Resilience { get; } = shield;
}
Naming shields
WithName stamps a name onto the shield itself, which then shows up as KevlarContext.ShieldName in custom strategies and callbacks — useful for logging and metrics:
Shield.Retry(3).WithName("github");
AddShield("github", …) registers under that DI name either way; WithName is about observability inside the pipeline.
For HttpClient pipelines specifically, see the HTTP integration — it builds on this package's registration model.