Table of Contents

Command Pattern API Reference

Complete API documentation for the Command pattern in PatternKit.

Namespace

using PatternKit.Behavioral.Command;

Command<TCtx>

Encapsulates an action with optional undo capability.

public sealed class Command<TCtx>

Type Parameters

Parameter Description
TCtx The context type the command operates on

Delegates

Exec

public delegate ValueTask Exec(in TCtx ctx, CancellationToken ct);

Asynchronous execution delegate. Return default for synchronous completion.

Properties

Property Type Description
HasUndo bool Indicates whether this command has an undo handler

Methods

Method Returns Description
Execute(in TCtx ctx, CancellationToken ct) ValueTask Executes the command
Execute(in TCtx ctx) ValueTask Executes with default cancellation token
TryUndo(in TCtx ctx, CancellationToken ct, out ValueTask undoTask) bool Attempts to undo; returns false if no undo
TryUndo(in TCtx ctx, out ValueTask undoTask) bool Attempts to undo with default token

Static Methods

Method Returns Description
Create() Builder Creates a new command builder
Macro() MacroBuilder Creates a macro (composite) command builder

Example

var command = Command<Counter>.Create()
    .Do(c => c.Value++)
    .Undo(c => c.Value--)
    .Build();

await command.Execute(counter);

if (command.TryUndo(counter, out var undoTask))
    await undoTask;

Command<TCtx>.Builder

Fluent builder for configuring a command.

public sealed class Builder

Methods

Method Returns Description
Do(Exec handler) Builder Sets the async Do handler (required)
Do(Action<TCtx> handler) Builder Sets a sync Do handler
Undo(Exec handler) Builder Sets the async Undo handler (optional)
Undo(Action<TCtx> handler) Builder Sets a sync Undo handler
Build() Command<TCtx> Builds the immutable command

Exceptions

Method Exception Condition
Build InvalidOperationException No Do handler was provided

Example

// Async command
var asyncCmd = Command<FileContext>.Create()
    .Do(async (in FileContext ctx, CancellationToken ct) =>
    {
        await File.WriteAllTextAsync(ctx.Path, ctx.Content, ct);
    })
    .Undo(async (in FileContext ctx, CancellationToken ct) =>
    {
        File.Delete(ctx.Path);
    })
    .Build();

// Sync command
var syncCmd = Command<Counter>.Create()
    .Do(c => c.Value++)
    .Undo(c => c.Value--)
    .Build();

Command<TCtx>.MacroBuilder

Builder for composite commands that execute in sequence.

public sealed class MacroBuilder

Methods

Method Returns Description
Add(Command<TCtx> cmd) MacroBuilder Adds a sub-command
AddIf(bool condition, Command<TCtx> cmd) MacroBuilder Adds a sub-command conditionally
Build() Command<TCtx> Builds the macro command

Execution Semantics

  • Execute: Runs sub-commands in registration order
  • Undo: Runs undo in reverse order, skipping commands without undo
  • Failure: Stops on first exception; does NOT auto-undo previous commands

Example

var compile = Command<BuildCtx>.Create()
    .Do(c => c.Steps.Add("compile"))
    .Undo(c => c.Steps.Remove("compile"))
    .Build();

var test = Command<BuildCtx>.Create()
    .Do(c => c.Steps.Add("test"))
    .Undo(c => c.Steps.Remove("test"))
    .Build();

var package = Command<BuildCtx>.Create()
    .Do(c => c.Steps.Add("package"))
    .Build(); // No undo

var pipeline = Command<BuildCtx>.Macro()
    .Add(compile)
    .AddIf(runTests, test)
    .Add(package)
    .Build();

// Execute: compile → test (if runTests) → package
await pipeline.Execute(context);

// Undo: test → compile (reverse order, skips package)
if (pipeline.TryUndo(context, out var undo))
    await undo;

Thread Safety

Component Thread-Safe
Builder No - use from single thread
MacroBuilder No - use from single thread
Command<TCtx> Yes - immutable after build

Performance Characteristics

Aspect Description
Execute (sync) Allocation-free via ValueTask
Execute (async) Single allocation for state machine
Macro (sync) Allocation-free when all complete synchronously
Macro (async) One allocation per async step

Complete Example

using PatternKit.Behavioral.Command;

// Context type
public class DocumentContext
{
    public List<string> Lines { get; } = new();
    public string? LastDeleted { get; set; }
}

// Create commands
var insertLine = Command<DocumentContext>.Create()
    .Do(ctx => ctx.Lines.Add("New line"))
    .Undo(ctx => ctx.Lines.RemoveAt(ctx.Lines.Count - 1))
    .Build();

var deleteLine = Command<DocumentContext>.Create()
    .Do(ctx =>
    {
        if (ctx.Lines.Count > 0)
        {
            ctx.LastDeleted = ctx.Lines[^1];
            ctx.Lines.RemoveAt(ctx.Lines.Count - 1);
        }
    })
    .Undo(ctx =>
    {
        if (ctx.LastDeleted != null)
        {
            ctx.Lines.Add(ctx.LastDeleted);
            ctx.LastDeleted = null;
        }
    })
    .Build();

// Use commands
var doc = new DocumentContext();

await insertLine.Execute(doc);  // Lines: ["New line"]
await insertLine.Execute(doc);  // Lines: ["New line", "New line"]
await deleteLine.Execute(doc);  // Lines: ["New line"], LastDeleted: "New line"

if (deleteLine.TryUndo(doc, out var undo))
    await undo;                 // Lines: ["New line", "New line"]

// Macro example
var batch = Command<DocumentContext>.Macro()
    .Add(insertLine)
    .Add(insertLine)
    .Add(insertLine)
    .Build();

await batch.Execute(doc);      // Adds 3 lines

if (batch.TryUndo(doc, out var batchUndo))
    await batchUndo;           // Removes 3 lines (reverse order)

See Also