AsyncTemplate<TContext, TResult>
A fluent, allocation-light async Template: define a fixed workflow (before → step → after), add async/sync hooks, opt into synchronization, and choose throwing or non-throwing execution.
What it is
- Async skeleton with three phases: Before (0..n), Step (1), After (0..n)
- Non-throwing path via
TryExecuteAsync(context)returning(ok, result, error) - Optional per-instance synchronization via
SemaphoreSlim - Immutable and thread-safe after
Build()
TL;DR
using PatternKit.Behavioral.Template;
var tpl = AsyncTemplate<int, string>
.Create(async (id, ct) =>
{
await Task.Delay(15, ct);
if (id < 0) throw new InvalidOperationException("invalid id");
return $"VAL-{id}";
})
.Before((id, ct) => { Console.WriteLine($"[BeforeAsync] {id}"); return default; })
.After((id, res, ct) => { Console.WriteLine($"[AfterAsync] {id} -> {res}"); return default; })
.OnError((id, err, ct) => { Console.WriteLine($"[ErrorAsync] {id}: {err}"); return default; })
.Synchronized() // optional
.Build();
var (ok, result, error) = await tpl.TryExecuteAsync(42);
Console.WriteLine(ok ? $"OK: {result}" : $"ERR: {error}");
API shape
var tpl = AsyncTemplate<TContext, TResult>
.Create(static (TContext ctx, CancellationToken ct) => /* ValueTask<TResult> */)
.Before(static (TContext ctx, CancellationToken ct) => /* ValueTask */) // 0..n (async)
.Before(static (TContext ctx) => { /* side-effect */ }) // 0..n (sync overload)
.After(static (TContext ctx, TResult res, CancellationToken ct) => /* ValueTask */) // 0..n (async)
.After(static (TContext ctx, TResult res) => { /* side-effect */ }) // 0..n (sync overload)
.OnError(static (TContext ctx, string error, CancellationToken ct) => /* ValueTask */) // 0..n (async)
.OnError(static (TContext ctx, string error) => { /* observe */ }) // 0..n (sync overload)
.Synchronized() // optional
.Build();
// Throws on failure
TResult result = await tpl.ExecuteAsync(context, ct);
// Non-throwing; returns tuple (ok, result?, error?)
(bool ok, TResult? result, string? error) = await tpl.TryExecuteAsync(context, ct);
Notes
- Multiple hooks compose; registration order is invocation order.
- OnError hooks run only when TryExecuteAsync catches an exception.
- Synchronized() uses an async mutex; keep the critical section small.
Testing (TinyBDD-style)
using PatternKit.Behavioral.Template;
using TinyBDD;
using TinyBDD.Xunit;
var tpl = AsyncTemplate<string, int>
.Create(async (ctx, ct) => { await Task.Yield(); return ctx.Length; })
.Before((ctx, ct) => { Console.WriteLine($"before:{ctx}"); return default; })
.After((ctx, res, ct) => { Console.WriteLine($"after:{ctx}:{res}"); return default; })
.Build();
var r = await tpl.ExecuteAsync("abc"); // 3
Design notes
- No reflection/LINQ in the hot path; simple async delegate calls and an optional async lock.
- Immutable after Build() so instances can be safely shared across threads.
- Sync and async hooks both supported; they are adapted internally to async.
Gotchas
- ExecuteAsync throws; OnError hooks are not invoked by ExecuteAsync.
- TryExecuteAsync captures ex.Message as error; result is default when failing.
- Synchronized serializes executions; prefer idempotent, short steps.
See also
- Subclassing: TemplateMethod<TContext, TResult>
- Synchronous fluent: Template<TContext, TResult>
- Demos: Template Method Demo, Template Method Async Demo