BranchBuilder<TPred, THandler>
A tiny, reusable builder for collecting predicate/handler pairs (plus an optional default) and projecting them into any concrete “strategy-like” product. It’s the core used by ActionStrategy, Strategy, and AsyncStrategy.
Why it exists
Lots of “first-match-wins” constructs look the same: you register ordered predicate/handler pairs, optionally set a default, then build an immutable thing. BranchBuilder captures that pattern so you can:
- Avoid re-implementing the same plumbing.
- Keep allocations minimal (lists while building, single
ToArray()onBuild()). - Project the collected data into any product type via a projector function.
Mental model
- Registration order matters. The
ith predicate corresponds to theith handler. - Default is optional. If you don’t set one, a fallback you supply at build time is used, and you get a
hasDefault=falseflag. - Build is a snapshot. Each call copies to arrays; later calls don’t mutate earlier products.
API at a glance
var b = BranchBuilder<TPred, THandler>.Create();
b.Add(TPred predicate, THandler handler); // append a pair (order preserved)
b.Default(THandler handler); // set/replace default
TProduct product = b.Build(
fallbackDefault: THandler, // used if no Default() was configured
projector: (TPred[] preds,
THandler[] handlers,
bool hasDefault,
THandler @default) => /* construct product */
);
Threading & immutability
- Builders are not thread-safe.
- Arrays passed to your projector are fresh snapshots. Treat them as immutable in your product.
Minimal examples
1) Build a simple classifier (sync)
// Shapes
delegate bool Pred(in int x);
delegate string Handler(in int x);
// Predicates/handlers
static bool IsEven(in int x) => (x & 1) == 0;
static bool IsPositive(in int x) => x > 0;
static string HandleEven(in int _) => "even";
static string HandlePositive(in int _) => "pos";
static string Fallback(in int _) => "other";
sealed record Classifier(Pred[] Preds, Handler[] Handlers, bool HasDefault, Handler Default)
{
public string Execute(in int x)
{
for (var i = 0; i < Preds.Length; i++)
if (Preds[i](in x)) return Handlers[i](in x);
return Default(in x);
}
}
var classifier =
BranchBuilder<Pred, Handler>.Create()
.Add(IsEven, HandleEven)
.Add(IsPositive, HandlePositive)
.Build(fallbackDefault: Fallback,
projector: (p, h, hasDef, def) => new Classifier(p, h, hasDef, def));
classifier.Execute(2); // "even"
classifier.Execute(1); // "pos"
classifier.Execute(-1); // "other" (fallback)
2) Swap in a real default (not fallback)
static string RealDefault(in int _) => "default";
var withRealDefault =
BranchBuilder<Pred, Handler>.Create()
.Add(IsEven, HandleEven)
.Default(RealDefault)
.Build(Fallback, (p, h, hasDef, def) => new Classifier(p, h, hasDef, def));
// withRealDefault.HasDefault == true; withRealDefault.Default == RealDefault
3) What the built-in strategies do
All of these are thin wrappers over BranchBuilder:
ActionStrategy<TIn>→ predicates + action handlers (void).Strategy<TIn,TOut>→ predicates + result handlers (TOut).AsyncStrategy<TIn,TOut>→ async predicates/handlers (ValueTask).
Each supplies a sensible fallback default to Build(...) and a projector that constructs the immutable strategy.
Usage patterns & tips
- Replace defaults: calling
Default(...)multiple times replaces the previous one (“last wins”). - Conditional registration: gate calls to
.Add(...)with your ownifor feature flags. (The conditional DSL lives inTryStrategy;BranchBuilderstays simple.) - Multiple products from one builder: you can call
Build(...)more than once. Each build snapshots current pairs and default. - Interop with
inparameters: Usinginin your delegate shapes keeps handlers low-overhead for structs.
Gotchas
- No validation of shapes.
TPred/THandlerare just types; ensure they’re the right delegates for your projector. - Default semantics: If you never call
Default(...), your projector receiveshasDefault=falseand the fallback handler as@default. Use the flag to distinguish “user configured” vs “library fallback”.
Reference (public API)
public sealed class BranchBuilder<TPred, THandler>
{
public static BranchBuilder<TPred, THandler> Create();
public BranchBuilder<TPred, THandler> Add(TPred predicate, THandler handler);
public BranchBuilder<TPred, THandler> Default(THandler handler);
public TProduct Build<TProduct>(
THandler fallbackDefault,
Func<TPred[], THandler[], bool, THandler, TProduct> projector);
}
See also
- ActionStrategy – first-match actions.
- Strategy – first-match handlers that return values.
- AsyncStrategy – async first-match strategy.
- ActionChain / ResultChain – chain style (middleware) alternatives.