Class Command<TCtx>
- Namespace
- PatternKit.Behavioral.Command
- Assembly
- PatternKit.Core.dll
Command Pattern (allocation-light)
Encapsulates a unit of work (an action) plus an optional inverse (undo) so it can be executed, queued, composed, retried, or reversed in a uniform way. This implementation focuses on very low allocation overhead while still supporting synchronous and asynchronous (ValueTask) execution and macro (composite) commands.
public sealed class Command<TCtx>
Type Parameters
TCtxContext type the command operates on.
- Inheritance
-
Command<TCtx>
- Inherited Members
Remarks
When should I use this?
- You need to encapsulate business operations so callers do not depend on concrete implementation details.
- You want an optional undo step (optimistic UI, reversible batch / maintenance tasks, editor actions).
- You want to compose several smaller operations into an ordered macro (with automatic reverse-order undo).
- You plan to queue, schedule, audit, retry, or log operations uniformly.
- You need both sync and async handlers without separate abstractions.
When NOT to use: If you simply need to call a method once with no intent to compose or undo, introducing a command adds unnecessary indirection.
Thread safety: A Command<TCtx> instance is immutable after Build() and therefore reusable across threads. Thread safety of the underlying action depends on the provided delegates and the TCtx contents.
Performance notes: Delegates are captured once; execution avoids allocations for the fast path when underlying work completes synchronously. Undo is optional; if you do not configure it, HasUndo is false and TryUndo(in TCtx, out ValueTask) returns false without allocations.
Value semantics of in TCtx: Passing the context by readonly reference avoids copying large structs. For mutable operations prefer reference types or ensure struct methods mutate internal state safely.
Related patterns: See also Composite (macro commands), Memento (for more complex state restoration), and Strategy (for pluggable behavior without undo).
Failure handling: Exceptions thrown inside Do or Undo propagate to the caller. Macro commands stop on first failure; previously executed sub-commands are not automatically undone unless you explicitly call the macro's undo afterward. If you need transactional semantics, wrap macro execution in a try/catch and invoke undo only when appropriate.
Examples
1. Basic synchronous command with undo
public sealed class Counter { public int Value; }
var counter = new Counter();
var increment = Command<Counter>.Create()
.Do(c => c.Value++)
.Undo(c => c.Value--)
.Build();
await increment.Execute(in counter); // Value: 1
if (increment.TryUndo(in counter, out var undoTask))
await undoTask; // Value: 0
2. Asynchronous command (I/O) with cancellation
var save = Command<string>.Create()
.Do(async (in string path, CancellationToken ct) => {
using var fs = File.Create(path);
await fs.WriteAsync(new byte[]{1,2,3}, ct);
})
.Build();
await save.Execute(in filePath, cancellationToken);
3. Macro (composite) command with conditional stage and reverse-order undo
record BuildCtx(List<string> Log);
var compile = Command<BuildCtx>.Create().Do(c => c.Log.Add("compile")).Undo(c => c.Log.Add("undo-compile")).Build();
var test = Command<BuildCtx>.Create().Do(c => c.Log.Add("test")).Undo(c => c.Log.Add("undo-test")).Build();
var pack = Command<BuildCtx>.Create().Do(c => c.Log.Add("pack")).Build(); // no undo
bool runTests = true;
var pipeline = Command<BuildCtx>.Macro()
.Add(compile)
.AddIf(runTests, test)
.Add(pack)
.Build();
var ctx = new BuildCtx(new List<string>());
await pipeline.Execute(in ctx); // Log: compile, test, pack
if (pipeline.TryUndo(in ctx, out var undoVt))
await undoVt; // Log adds: undo-test, undo-compile (reverse, skipping pack)
4. Optimistic UI action with optional undo
// Immediately show item in UI, but support undo if server rejects later.
var addItem = Command<List<string>>.Create()
.Do(list => list.Add("draft"))
.Undo(list => list.Remove("draft"))
.Build();
Properties
HasUndo
Indicates whether this command has an undo handler.
public bool HasUndo { get; }
Property Value
Methods
Create()
Start building a new command. Provide a Do delegate (required) and optionally an Undo delegate.
public static Command<TCtx>.Builder Create()
Returns
Execute(in TCtx)
Execute with None.
public ValueTask Execute(in TCtx ctx)
Parameters
ctxTCtx
Returns
Execute(in TCtx, CancellationToken)
Execute the command logic. Throws if the underlying delegate throws.
public ValueTask Execute(in TCtx ctx, CancellationToken ct)
Parameters
ctxTCtxContext value.
ctCancellationTokenCancellation token.
Returns
Macro()
Begin a macro (composite) command definition. Sub-commands execute in registration order; undo runs in reverse order and skips those without undo handlers.
public static Command<TCtx>.MacroBuilder Macro()
Returns
- Command<TCtx>.MacroBuilder
TryUndo(in TCtx, CancellationToken, out ValueTask)
Attempt to undo. Returns false if no undo handler was configured.
The returned ValueTask must be awaited if true.
public bool TryUndo(in TCtx ctx, CancellationToken ct, out ValueTask undoTask)
Parameters
ctxTCtxContext.
ctCancellationTokenCancellation token.
undoTaskValueTaskUndo value task (valid only when result is true).
Returns
- bool
trueif an undo handler exists; otherwise false.
TryUndo(in TCtx, out ValueTask)
Convenience overload using None.
public bool TryUndo(in TCtx ctx, out ValueTask undoTask)
Parameters
ctxTCtxundoTaskValueTask