Result Chain (Behavioral.Chain.ResultChain)
A first–match-wins chain that can produce a value.
Each handler receives (in TIn input, out TOut? result, Next next) and can either:
- Produce a result and return
true→ the chain short-circuits, or - Delegate to
next(input, out result)→ the chain continues.
It’s the “router with a return value” sibling of ActionChain<TCtx>.
Why use ResultChain?
Use it when you need ordered rules that return something:
- HTTP‐ish routing →
HttpResponse - Command parsers →
ICommand - Promotion/price/feature selection →
CalculationResult - Fallback/“NotFound” defaults via a terminal tail
If you only need side effects, prefer ActionChain<TCtx>.
If you want a simpler first match mapping (no next), see Strategy/TryStrategy.
Quick example: tiny router
using PatternKit.Behavioral.Chain;
public readonly record struct Request(string Method, string Path);
public readonly record struct Response(int Status, string Body);
var router = ResultChain<Request, Response>.Create()
// GET /health
.When(static (in r) => r.Method == "GET" && r.Path == "/health")
.Then(r => new Response(200, "OK"))
// GET /users/{id}
.When(static (in r) => r.Method == "GET" && r.Path.StartsWith("/users/"))
.Then(r => new Response(200, $"user:{r.Path[7..]}"))
// default / not found
.Finally(static (in _, out Response? res, _) => { res = new(404, "not found"); return true; })
.Build();
var ok1 = router.Execute(in new Request("GET", "/health"), out var res1); // ok1=true, res1=200 OK
var ok2 = router.Execute(in new Request("GET", "/nope"), out var res2); // ok2=true, res2=404
“Sometimes produce, sometimes delegate”
Use When(...).Do(handler) when a rule might conditionally produce and otherwise pass control onward:
var chain = ResultChain<int, string>.Create()
.When(static (in x) => x % 2 == 0).Do(static (in x, out string? r, ResultChain<int,string>.Next next) =>
{
// Only handle THE answer; otherwise delegate
if (x == 42) { r = "forty-two"; return true; }
return next(in x, out r);
})
.When(static (in x) => x % 2 == 0).Then(_ => "even") // handles delegated evens
.Finally(static (in _, out string? r, _) => { r = "odd"; return true; })
.Build();
API surface
public sealed class ResultChain<TIn, TOut>
{
public delegate bool Next(in TIn input, out TOut? result);
public delegate bool TryHandler(in TIn input, out TOut? result, Next next);
public delegate bool Predicate(in TIn input);
public bool Execute(in TIn input, out TOut? result);
public sealed class Builder
{
public Builder Use(TryHandler handler);
public WhenBuilder When(Predicate predicate);
public Builder Finally(TryHandler tail); // terminal fallback
public ResultChain<TIn, TOut> Build();
public sealed class WhenBuilder
{
public Builder Do(TryHandler handler); // may produce OR delegate
public Builder Then(Func<TIn, TOut> produce); // produces and stops
}
}
public static Builder Create();
}
Semantics
Order is preserved. First matching producer wins.
Then(Func<TIn,TOut>): if predicate is true → produce result, returntrue.Do(TryHandler): you decide to produce or delegate by callingnext.Finally(TryHandler): runs only if the chain reaches the tail (i.e., nobody produced earlier). Typical use: default/NotFound.Execute(...)returns:truewhen any handler (orFinally) produced a result;resultis set.falsewhen no one produced and noFinallyran;resultisdefault.
Patterns
1) Default/NotFound tail
.Finally(static (in _, out MyResult? r, _) => { r = MyResult.NotFound; return true; })
2) Multi-stage processing
Chain “can I handle this?” steps. Each step may produce, or else delegate:
.When(static (in x) => IsFastPath(x)).Do(static (in x, out R? r, var next) =>
{
if (TryFast(x, out r)) return true;
return next(in x, out r); // let later handlers try
})
.When(static (in x) => IsSlowPath(x)).Then(SlowCompute)
3) Cross-cut logging without duplication
Put it in Finally only if you want it to run when nothing matched.
Otherwise log inside the Then/Do that produced.
Performance & threading
- The chain composes to a single delegate at
Build()time (reverse fold). No allocations duringExecutebesides what your handlers do. - The built chain is immutable and thread-safe. Builders are not thread-safe.
Gotchas & tips
For lambdas passed to
When(...), the parameter isin. Prefer explicit static lambdas to avoid captures and compiler warnings:.When(static (in r) => r.Flag) // good .Then(static r => ...) // Then’s lambda is a normal parameter (no `in`)If you omit
Finallyand nothing produces,Executereturnsfalseandresultisdefault.
Tests
See PatternKit.Tests/Behavioral/Chain/ResultChainTests.cs for executable specs covering:
- First match wins; fallback via
Finally - No tail →
Executereturnsfalse When.Do→ produce vs delegate- Registration order guarantees
- Tail runs only when no earlier producer
These tests use TinyBDD so the assertions read like documentation.