Skip to content

Latest commit

 

History

502 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Kevlar

NuGet version NuGet downloads CI License Docs

Fast, allocation-conscious resilience for .NET. Kevlar brings retries, circuit breakers, timeouts, rate limiting, concurrency limiting, hedging and fallbacks together in a fluent API.

Resilience code should explain how a call is protected, not make you decode a framework. With Kevlar, you build an immutable Shield, reuse it, and use it with ordinary delegates.

Documentation · Strategies · API Reference · Benchmarks

Get started

dotnet add package Kevlar

Install all coupled Kevlar.* packages at the same version. NuGet reports partial upgrades with NU1605 or NU1608; see the package lockstep policy.

using Kevlar;

var shield = Shield.Retry(3);

using var client = new HttpClient();
using var response = await shield.ExecuteAsync(
    ct => client.GetAsync("https://example.com", ct));

Retry(3) means three retries after the initial call: up to 4 total attempts. Its default backoff is exponential from 250 ms with factor 2, equal jitter, and a 30-second cap. It retries ordinary exceptions such as HttpRequestException, but treats the TaskCanceledException from HttpClient.Timeout as cancellation. To retry that timeout and HTTP 5xx/429 responses, use HttpShield.WhenTransient() from Kevlar.Extensions.Http. The cancellation token passed to your delegate is important—it is how timeouts and abandoned attempts stop the underlying work.

When you combine strategies, the first strategy is the outermost, just like ASP.NET middleware:

var productionShield = Shield
    .Timeout(TimeSpan.FromSeconds(30))
    .Retry(3)
    .CircuitBreaker(consecutiveFailures: 5, breakDuration: TimeSpan.FromSeconds(30));

That reads in execution order: the 30-second timeout wraps the retries, which wrap the circuit breaker.

Build shields once and reuse them. They are immutable and thread-safe. Reuse also matters for stateful strategies: calls made through the same shield share its circuit breaker and limiter state.

Why Kevlar?

  • The common case stays small. Start with Shield.Retry(3); use options and callbacks when the situation genuinely needs them.
  • Failures can be exceptions or results. Retry an HttpRequestException, an HTTP 500 response, or both, without changing the shape of the pipeline.
  • Composition is explicit. Chain strategies, or combine existing shields with Wrap and Compose. The first strategy is always the outermost.
  • State can be isolated by key. Partitioned shields retain independent breaker, limiter, and queue state per tenant, endpoint, or other bounded key.
  • It is designed for hot paths. Struct outcomes, pooled contexts, state-passing overloads and ValueTask keep overhead and allocations low. Browse the benchmark suite or the published comparative BenchmarkDotNet results.
  • Production concerns are built in. Shields support TimeProvider, describe their own pipeline, publish metrics through the Kevlar meter, and can emit structured ILogger events through Kevlar.Extensions.Logging. Hook exceptions never replace the protected outcome; observe them through KevlarDiagnostics.OnCallbackError, logging, or Kevlar.Testing.TelemetryRecorder. Built-in analyzers catch cancellation and pipeline mistakes at compile time.

Runnable samples

Every sample is a small net8.0;net10.0 application with a --smoke mode used by CI:

Build all samples with dotnet build samples/Samples.slnx -c Release, or follow the command in a sample's README to run one directly.

Choose what counts as failure

Reactive strategies handle ordinary exceptions by default, excluding cancellation, Kevlar's fail-fast rejections, and fatal runtime failures. A handling clause lets you be more precise:

var search = Shield.For<HttpResponseMessage>()
    .When<HttpRequestException>()
    .Or<TimeoutExceededException>()
    .OrResult(response => (int)response.StatusCode is 429 or >= 500)
    .Fallback((outcome, ct) => cache.GetCachedResultsAsync(ct))
    .Retry(3)
    .CircuitBreaker(consecutiveFailures: 5, breakDuration: TimeSpan.FromSeconds(30));

A clause is ambient. It applies to the strategy it is attached to and to every reactive strategy chained after it, until a new clause replaces it, WithDefaultHandling() resets it, or Wrap/Compose seals it. Above, the fallback, the retry and the circuit breaker all react to the same three conditions. Nothing repeats the predicate per strategy:

var api = Shield
    .When<HttpRequestException>()
    .Retry(3)                        // retries HttpRequestException
    .CircuitBreaker(consecutiveFailures: 5, breakDuration: TimeSpan.FromSeconds(30));
    // the breaker inherits the clause above: only HttpRequestException counts toward tripping it

Typed shields keep result handling strongly typed, including callback events and Outcome<T> values.

Compose protection in reading order

The first strategy is the outermost, just like ASP.NET middleware:

var shield = Shield
    .Timeout(TimeSpan.FromSeconds(30))  // total budget
    .Retry(3)                           // retry within that budget
    .CircuitBreaker(consecutiveFailures: 5, breakDuration: TimeSpan.FromSeconds(30))
    .Timeout(TimeSpan.FromSeconds(5));  // budget for each attempt

This rule makes the important questions visible: is a timeout per attempt or for the whole call? Does fallback wrap retry? Are two clients meant to share one circuit? See composition for Wrap, Compose and the state-sharing rules.

HTTP and dependency injection

Kevlar.Extensions.Http provides a ready-to-use HttpClientFactory pipeline. In an ASP.NET Core app using Microsoft.NET.Sdk.Web:

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder();

builder.Services.AddHttpClient("api")
    .AddStandardShield();

The standard shield has a 30-second total timeout, three jittered retries that honour Retry-After, a circuit breaker, and a 10-second timeout per attempt. You can configure every part or supply your own shield. POST, PATCH, and custom methods remain single-attempt unless you explicitly enable replay for operations that are safe to repeat.

Kevlar.Extensions.DependencyInjection adds named, configuration-bound shields and IKevlarRegistry. In a standalone console project, install the concrete Microsoft.Extensions.DependencyInjection package as well; it provides BuildServiceProvider():

using System;
using Kevlar;
using Kevlar.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

services.AddShield("database", Shield
    .Timeout(TimeSpan.FromSeconds(10))
    .Retry(3));

using var serviceProvider = services.BuildServiceProvider();
var registry = serviceProvider.GetRequiredService<IKevlarRegistry>();
var databaseShield = registry.GetShield("database");

Packages

Kevlar is a focused Polly alternative for teams that prefer immutable pipelines, explicit reading-order composition, and allocation-conscious hot paths. It is not API-compatible with Polly; the migration guide maps the concepts side by side.

Package What it adds
Kevlar Core strategies, Shield API, and diagnostics-only analyzers
Kevlar.Chaos Controlled latency, faults, outcomes and custom behaviour
Kevlar.Extensions.DependencyInjection Named and configuration-bound shields for Microsoft DI
Kevlar.Extensions.Http HttpClientFactory integration, request replay and transient-fault handling
Kevlar.Extensions.Logging Structured ILogger events for every built-in strategy
Kevlar.Extensions.Grpc gRPC client resilience for unary and streaming calls
Kevlar.Extensions.RateLimiting Adapters for System.Threading.RateLimiting and custom leases
Kevlar.Testing Pipeline assertions, state snapshots and deterministic time helpers

Requirements, targets and support

Package Target frameworks
Kevlar netstandard2.0; net8.0; net10.0
Kevlar.Chaos netstandard2.0; net8.0; net10.0
Kevlar.Extensions.DependencyInjection netstandard2.0; net8.0; net10.0
Kevlar.Extensions.Grpc netstandard2.0; netstandard2.1; net8.0; net10.0
Kevlar.Extensions.Http netstandard2.0; net8.0; net10.0
Kevlar.Extensions.Logging netstandard2.0; net8.0; net10.0
Kevlar.Extensions.RateLimiting netstandard2.0; net8.0; net10.0
Kevlar.Testing netstandard2.0; net8.0; net10.0

The analyzers bundled with Kevlar run in Visual Studio 2022 17.8 or later and the .NET 8.0.100 SDK or later. Older compiler hosts skip the analyzer assets; the runtime library remains available. Kevlar.Testing supports callback recording on netstandard2.0, but deterministic time and metric capture require .NET 8 or later.

The core package depends on Reservoir [1.4.0, 2.0.0) on every target. Its netstandard2.0 asset also uses Microsoft.Bcl.AsyncInterfaces, Microsoft.Bcl.TimeProvider, and System.Threading.Tasks.Extensions. See the support policy for integration-package dependency floors, package lockstep, and the release support window.

Kevlar is licensed under the MIT License.

Where next?

Community

Read the contribution guide, code of conduct, and release notes. Use the guided issue forms for bugs, feature requests, and questions; report vulnerabilities through GitHub's private security advisory form.

About

Fast, readable resilience for .NET.

Topics

Resources

Code of conduct

Contributing

Stars

13 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages