Laminar 0.1.3
dotnet add package Laminar --version 0.1.3
NuGet\Install-Package Laminar -Version 0.1.3
<PackageReference Include="Laminar" Version="0.1.3" />
<PackageVersion Include="Laminar" Version="0.1.3" />
<PackageReference Include="Laminar" />
paket add Laminar --version 0.1.3
#r "nuget: Laminar, 0.1.3"
#:package Laminar@0.1.3
#addin nuget:?package=Laminar&version=0.1.3
#tool nuget:?package=Laminar&version=0.1.3
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 | Versions 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. |
-
net10.0
- System.Threading.RateLimiting (>= 10.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.