Laminar 0.1.3

dotnet add package Laminar --version 0.1.3
                    
NuGet\Install-Package Laminar -Version 0.1.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Laminar" Version="0.1.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Laminar" Version="0.1.3" />
                    
Directory.Packages.props
<PackageReference Include="Laminar" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Laminar --version 0.1.3
                    
#r "nuget: Laminar, 0.1.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Laminar@0.1.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Laminar&version=0.1.3
                    
Install as a Cake Addin
#tool nuget:?package=Laminar&version=0.1.3
                    
Install as a Cake Tool

Laminar

Small, operation-first resilience for .NET 10. Decorate delegates with rate limiting, retry, cooperative timeout, circuit breaking, or your own pattern. No HTTP dependency, pipelines, or registries.

Usage

using System.Threading.RateLimiting;
using Laminar;

using var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
{
    TokenLimit = 10,
    TokensPerPeriod = 2,
    ReplenishmentPeriod = TimeSpan.FromSeconds(1),
    AutoReplenishment = true,
    QueueLimit = 5,
    QueueProcessingOrder = QueueProcessingOrder.OldestFirst
});

Func<string> hello = () => "Hello, world!";
var limited = hello.RateLimitedBy(limiter);

string result = await limited(CancellationToken.None);

Every pattern adapts Action, Func<T>, and Task- or ValueTask-returning delegates automatically. Async delegates may accept a cancellation token. Invoke the returned delegate with await guarded(cancellationToken). Delegates with other arguments can be bound through a lambda.

For immediate retry execution, import the static facade:

using Laminar;
using static Laminar.Retry;

await Retryable(() => client.SendEmailAsync(Message));

Retryable executes immediately and returns ValueTask or ValueTask<T> to await completion. It accepts Task- and Task<T>-returning delegates, including inline async lambdas, with or without a cancellation-token parameter. Results are inferred automatically:

string result = await Retryable(async () => await ReadAsync());

await Retryable(
    token => client.SendEmailAsync(Message, token),
    options: new RetryOptions { MaxRetries = 3, Delay = TimeSpan.FromSeconds(2) },
    cancellationToken: cancellationToken);

Options are optional and use the same defaults and retry behavior as the Retryable extension. The optional caller token defaults to CancellationToken.None and is forwarded to cancellation-aware delegates. Unlike this immediate static facade, operation.Retryable(options) decorates a reusable delegate without executing it; invoke that delegate with await retried(cancellationToken) each time you need it.

The other patterns have matching immediate-execution facades:

using static Laminar.Timeouts;
using static Laminar.RateLimit;
using static Laminar.CircuitBreaker;

string result = await TimedOutAfter(
    token => ReadAsync(token), TimeSpan.FromSeconds(5),
    cancellationToken: cancellationToken);

await RateLimitedBy(() => SendAsync(), limiter, permitCount: 2);
await CircuitBrokenBy(() => SendAsync(), breaker);

Each facade accepts Task- and Task<T>-returning delegates, with or without a cancellation-token parameter, and returns ValueTask or ValueTask<T>. TimedOutAfter requires an explicit timeout and optionally accepts a TimeProvider. Timeout remains cooperative: it waits for the operation to finish before rejecting a late result. Timeouts avoids a name collision with .NET's System.Threading.Timeout.

Pass an existing limiter or breaker to RateLimitedBy or CircuitBrokenBy. Repeated calls use that same instance's state; the facades neither create nor dispose these shared dependencies. You own the limiter's lifetime. The existing delegate extensions remain the way to compose reusable operations.

For immediate, one-line execution with an existing resilience pattern, use ExecuteAsync:

var retry = new RetryResilience(new RetryOptions
{
    MaxRetries = 3,
    Delay = TimeSpan.FromSeconds(2),
    BackoffFactor = 2
});

string result = await retry.ExecuteAsync(
    () => Task.FromResult("Hello, world!"));

ExecuteAsync accepts Task- and Task<T>-returning delegates, with or without a cancellation-token parameter. An optional caller token is passed as the second argument and forwarded to cancellation-aware delegates. The helpers return ValueTask or ValueTask<T> and use the same adapters and resilience behavior as ResilientWith. For reusable decorated delegates, keep using ResilientWith.

Patterns

Func<CancellationToken, Task<string>> operation =
    cancellationToken => Task.FromResult("Hello, world!");

var breaker = new CircuitBreakerResilience(new CircuitBreakerOptions
{
    FailureThreshold = 5,
    BreakDuration = TimeSpan.FromSeconds(30)
});

var resilient = operation
    .RateLimitedBy(limiter)
    .Retryable(new RetryOptions
    {
        MaxRetries = 3,
        Delay = TimeSpan.FromMilliseconds(100),
        ShouldHandle = exception => exception is IOException
    })
    .TimedOutAfter(TimeSpan.FromSeconds(5))
    .CircuitBrokenBy(breaker);

string result = await resilient(cancellationToken);
Pattern Behavior
RateLimitedBy Uses any .NET RateLimiter; queues according to its options, then throws RateLimitRejectedException on rejection. Optional permitCount defaults to 1.
Retryable Defaults to 3 retries with no delay. Set Delay for fixed delays, or also set BackoffFactor for exponential backoff. Cancellation and resilience rejections are excluded by default; customize ShouldHandle to select failures.
TimedOutAfter Cancels the operation's token at the deadline and throws TimeoutRejectedException. Caller cancellation remains cancellation.
CircuitBrokenBy Opens after consecutive handled failures, throws CircuitOpenException while open, and admits one probe after the break duration. A successful probe closes it.

The last decorator is outermost: operation.RateLimitedBy(limiter).Retryable() acquires a permit for every attempt; operation.Retryable().RateLimitedBy(limiter) acquires once for the whole retry sequence.

Retry delays start at Delay and are multiplied by BackoffFactor for each subsequent retry. The factor defaults to 1 (fixed delay) and must be finite and at least 1. For example, this retries after 2, 4, and 8 seconds:

var retried = operation.Retryable(new RetryOptions
{
    MaxRetries = 3,
    Delay = TimeSpan.FromSeconds(2),
    BackoffFactor = 2
});

Growing delays are capped at .NET's maximum timer duration (4,294,967,294 milliseconds). A zero delay stays immediate regardless of the factor. Every invocation starts its own backoff sequence, and delays honor caller cancellation.

Reuse a limiter or breaker to share its state across operations. You own the limiter's lifetime. Limits are in-process. Retry only operations safe to repeat. Timeout is cooperative: code that ignores cancellation cannot be forcibly stopped; the wrapper waits for completion and rejects a late result. Synchronous adapters run inline, and delegates without a token cannot observe cancellation while running.

Custom patterns

Implement IOperationResilience and apply it with ResilientWith.

public sealed class CustomResilience : IOperationResilience
{
    public Func<CancellationToken, ValueTask<T>> Apply<T>(
        Func<CancellationToken, ValueTask<T>> operation)
        => cancellationToken =>
        {
            cancellationToken.ThrowIfCancellationRequested();
            return operation(cancellationToken);
        };
}

var decorated = operation.ResilientWith(new CustomResilience());

Release notes

See the changelog for changes in every release, including prereleases.

Contributing

Issues and pull requests are welcome. For larger changes, open an issue first to discuss the approach. Keep contributions focused, follow the existing code style, and include tests and documentation updates for behavior changes. Before submitting a pull request, ensure the solution builds, tests pass, and formatting is clean.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.3 106 11/1/2026
0.1.2 116 10/31/2026
0.1.1 105 10/31/2026
0.1.0 103 10/18/2026