AsyncStrategy<TIn, TOut>
A first-match-wins, asynchronous strategy: evaluate predicates in order and execute the handler for the first
branch that matches. Handlers return a TOut via ValueTask<TOut>.
Great for things like: async routing/dispatch, picking a storage backend, selecting an algorithm or serializer, or any “try A, else B” flow that needs awaits.
Why use it
- Deterministic control flow: registration order = evaluation order; only the first match runs.
- Async-first: both predicates and handlers can await.
- Explicit defaults: optional fallback handler when nothing matches.
- Immutable & thread-safe after
Build()(safe for concurrent calls). - Allocation-light: uses arrays and
ValueTaskto minimize overhead.
TL;DR example
using PatternKit.Behavioral.Strategy;
var strat = AsyncStrategy<int, string>.Create()
.When((n, ct) => new ValueTask<bool>(n < 0))
.Then((n, ct) => new ValueTask<string>("negative"))
.When((n, ct) => new ValueTask<bool>(n == 0))
.Then((n, ct) => new ValueTask<string>("zero"))
.Default((n, ct) => new ValueTask<string>("positive"))
.Build();
var s1 = await strat.ExecuteAsync(-5); // "negative"
var s2 = await strat.ExecuteAsync(0); // "zero"
var s3 = await strat.ExecuteAsync(7); // "positive"
Building branches
Each branch is a predicate + handler pair:
var s = AsyncStrategy<Request, Response>.Create()
.When((req, ct) => new ValueTask<bool>(req.Path.StartsWith("/admin")))
.Then(async (req, ct) =>
{
await Audit(req, ct);
return await HandleAdmin(req, ct);
})
.When((req, ct) => new ValueTask<bool>(req.Path.StartsWith("/api/")))
.Then((req, ct) => HandleApi(req, ct)) // already returns ValueTask<Response>
.Default((req, ct) => NotFound(req)) // fallback runs if nothing matched
.Build();
First match wins: if multiple predicates are true, only the earliest registered one runs.
Defaults & errors
- If you provide
.Default(handler), it runs when no predicates match. - If you omit a default and nothing matches,
ExecuteAsyncthrowsInvalidOperationExceptionto signal no branch matched.
Cancellation
CancellationToken flows into both predicate and handler:
var s = AsyncStrategy<int, string>.Create()
.When((_, ct) =>
{
ct.ThrowIfCancellationRequested();
return new ValueTask<bool>(true);
})
.Then((_, ct) =>
{
ct.ThrowIfCancellationRequested();
return new ValueTask<string>("ok");
})
.Build();
If the token is canceled, your predicate/handler can throw OperationCanceledException and the call will surface it.
Synchronous adapters (nice for quick wiring)
You don’t have to write ValueTask everywhere:
var s = AsyncStrategy<int, string>.Create()
.When(n => n % 2 == 0) // sync predicate
.Then((_, _) => new ValueTask<string>("even"))
.When((n, _) => new ValueTask<bool>(n >= 0))
.Then((_, _) => new ValueTask<string>("nonneg"))
.Default(_ => "other") // sync default
.Build();
await s.ExecuteAsync(2); // "even"
await s.ExecuteAsync(1); // "nonneg"
await s.ExecuteAsync(-1); // "other"
Testing (TinyBDD style)
[Scenario("First matching async branch runs; default used when none match")]
[Fact]
public async Task FirstMatchAndDefault()
{
var log = new List<string>();
var strat = AsyncStrategy<int, string>.Create()
.When((n, _) => new ValueTask<bool>(n > 0))
.Then((n, _) => { log.Add("pos"); return new ValueTask<string>("+" + n); })
.When((n, _) => new ValueTask<bool>(n < 0))
.Then((n, _) => { log.Add("neg"); return new ValueTask<string>(n.ToString()); })
.Default((_, _) => new ValueTask<string>("zero"))
.Build();
var r1 = await strat.ExecuteAsync(5);
Assert.Equal("+5", r1);
Assert.Equal("pos", string.Join("|", log));
}
Our repository includes comprehensive tests covering: first-match behavior, default vs. throw, sync adapters, order guarantees, and cancellation.
Design notes
- Built on
BranchBuilder: the builder collects(Predicate, Handler)pairs and compiles them into arrays. ValueTaskeverywhere: avoids allocatingTaskfor already-completed operations.- No reflection / no LINQ in the hot path: simple loops over arrays.
Gotchas
- Order matters. Put the most specific predicates first.
- Default is optional. Without it, expect
InvalidOperationExceptionwhen nothing matches. - Predicate/handler exceptions are not swallowed—let them surface or handle them upstream.
See also
- ActionStrategy — first-match actions with no return value.
- Strategy — synchronous result-producing strategy (throws on no match).
- TryStrategy — synchronous, result-producing strategy that can “not match” without throwing.
- BranchBuilder — the generic composition utility AsyncStrategy builds upon.