Table of Contents

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

TCtx

Context 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

bool

Methods

Create()

Start building a new command. Provide a Do delegate (required) and optionally an Undo delegate.

public static Command<TCtx>.Builder Create()

Returns

Command<TCtx>.Builder

Execute(in TCtx)

Execute with None.

public ValueTask Execute(in TCtx ctx)

Parameters

ctx TCtx

Returns

ValueTask

Execute(in TCtx, CancellationToken)

Execute the command logic. Throws if the underlying delegate throws.

public ValueTask Execute(in TCtx ctx, CancellationToken ct)

Parameters

ctx TCtx

Context value.

ct CancellationToken

Cancellation token.

Returns

ValueTask

ValueTask enabling allocation-free sync completion.

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

ctx TCtx

Context.

ct CancellationToken

Cancellation token.

undoTask ValueTask

Undo value task (valid only when result is true).

Returns

bool

true if 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

ctx TCtx
undoTask ValueTask

Returns

bool