diff --git a/README.md b/README.md
index ef539fa..8188c0c 100644
--- a/README.md
+++ b/README.md
@@ -68,11 +68,11 @@ if (parse.Execute("123", out var n))
PatternKit will grow to cover **Creational**, **Structural**, and **Behavioral** patterns with fluent, discoverable APIs:
-| Category | Patterns ✓ = implemented |
-| -------------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| **Creational** | [Factory](docs/patterns/creational/factory/factory.md) ✓ • [Composer](docs/patterns/creational/builder/composer.md) ✓ • [ChainBuilder](docs/patterns/creational/builder/chainbuilder.md) ✓ • [BranchBuilder](docs/patterns/creational/builder/chainbuilder.md) ✓ • [MutableBuilder](docs/patterns/creational/builder/mutablebuilder.md) ✓ • [Prototype](docs/patterns/creational/prototype/prototype.md) ✓ • [Singleton](docs/patterns/creational/singleton/singleton.md) ✓ |
-| **Structural** | [Adapter](docs/patterns/structural/adapter/fluent-adapter.md) ✓ • [Bridge](docs/patterns/structural/bridge/bridge.md) ✓ • [Composite](docs/patterns/structural/composite/composite.md) ✓ • Decorator (planned) • Facade (planned) • Flyweight (planned) • Proxy (planned) |
-| **Behavioral** | [Strategy](docs/patterns/behavioral/strategy/strategy.md) ✓ • [TryStrategy](docs/patterns/behavioral/strategy/trystrategy.md) ✓ • [ActionStrategy](docs/patterns/behavioral/strategy/actionstrategy.md) ✓ • [ActionChain](docs/patterns/behavioral/chain/actionchain.md) ✓ • [ResultChain](docs/patterns/behavioral/chain/resultchain.md) ✓ • Command (planned) • Iterator (planned) • Mediator (planned) • Memento (planned) • Observer (planned) • State (planned) • Template Method (planned) • Visitor (planned) |
+| Category | Patterns ✓ = implemented |
+| -------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| **Creational** | [Factory](docs/patterns/creational/factory/factory.md) ✓ • [Composer](docs/patterns/creational/builder/composer.md) ✓ • [ChainBuilder](docs/patterns/creational/builder/chainbuilder.md) ✓ • [BranchBuilder](docs/patterns/creational/builder/chainbuilder.md) ✓ • [MutableBuilder](docs/patterns/creational/builder/mutablebuilder.md) ✓ • [Prototype](docs/patterns/creational/prototype/prototype.md) ✓ • [Singleton](docs/patterns/creational/singleton/singleton.md) ✓ |
+| **Structural** | [Adapter](docs/patterns/structural/adapter/fluent-adapter.md) ✓ • [Bridge](docs/patterns/structural/bridge/bridge.md) ✓ • [Composite](docs/patterns/structural/composite/composite.md) ✓ • Decorator (planned) • Facade (planned) • Flyweight (planned) • Proxy (planned) |
+| **Behavioral** | [Strategy](docs/patterns/behavioral/strategy/strategy.md) ✓ • [TryStrategy](docs/patterns/behavioral/strategy/trystrategy.md) ✓ • [ActionStrategy](docs/patterns/behavioral/strategy/actionstrategy.md) ✓ • [ActionChain](docs/patterns/behavioral/chain/actionchain.md) ✓ • [ResultChain](docs/patterns/behavioral/chain/resultchain.md) ✓ • [Command](docs/patterns/behavioral/command/command.md) ✓ • Iterator (planned) • [Mediator](docs/behavioral/mediator/mediator.md) ✓ • Memento (planned) • Observer (planned) • State (planned) • Template Method (planned) • Visitor (planned) |
Each pattern will ship with:
diff --git a/docs/index.md b/docs/index.md
index eeb0475..79b2b0a 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -60,15 +60,16 @@ if (parser.Execute("123", out var value))
## 📚 Available Patterns
-PatternKit will ultimately support the full spectrum of **creational**, **structural**, and **behavioral** patterns:
+PatternKit will grow to cover **Creational**, **Structural**, and **Behavioral** patterns with fluent, discoverable APIs:
-| Category | Patterns ✓ = Implemented |
-| -------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| **Creational** | Factory (planned) • [Builder](xref:PatternKit.Creational.Builder) (planned) • Prototype (planned) • Singleton (planned) |
-| **Structural** | Adapter (planned) • Bridge (planned) • Composite (planned) • Decorator (planned) • Facade (planned) • Flyweight (planned) • Proxy (planned) |
-| **Behavioral** | [Strategy](xref:PatternKit.Behavioral.Strategy.Strategy`2) ✓ • [TryStrategy](xref:PatternKit.Behavioral.Strategy.TryStrategy`2) ✓ • Chain of Responsibility (planned) • Command (planned) • Iterator (planned) • Mediator (planned) • Memento (planned) • Observer (planned) • State (planned) • Template Method (planned) • Visitor (planned) |
+| Category | Patterns ✓ = implemented |
+| -------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| **Creational** | [Factory](patterns/creational/factory/factory.md) ✓ • [Composer](patterns/creational/builder/composer.md) ✓ • [ChainBuilder](patterns/creational/builder/chainbuilder.md) ✓ • [BranchBuilder](patterns/creational/builder/chainbuilder.md) ✓ • [MutableBuilder](patterns/creational/builder/mutablebuilder.md) ✓ • [Prototype](patterns/creational/prototype/prototype.md) ✓ • [Singleton](patterns/creational/singleton/singleton.md) ✓ |
+| **Structural** | [Adapter](patterns/structural/adapter/fluent-adapter.md) ✓ • [Bridge](patterns/structural/bridge/bridge.md) ✓ • [Composite](patterns/structural/composite/composite.md) ✓ • Decorator (planned) • Facade (planned) • Flyweight (planned) • Proxy (planned) |
+| **Behavioral** | [Strategy](patterns/behavioral/strategy/strategy.md) ✓ • [TryStrategy](patterns/behavioral/strategy/trystrategy.md) ✓ • [ActionStrategy](patterns/behavioral/strategy/actionstrategy.md) ✓ • [ActionChain](patterns/behavioral/chain/actionchain.md) ✓ • [ResultChain](patterns/behavioral/chain/resultchain.md) ✓ • [Command](patterns/behavioral/command/command.md) ✓ • Iterator (planned) • [Mediator](behavioral/mediator/mediator.md) ✓ • Memento (planned) • Observer (planned) • State (planned) • Template Method (planned) • Visitor (planned) |
+
+Each pattern will ship with:
-Each pattern ships with:
* **Fluent API**: readable and composable (`.When(...)`, `.Then(...)`, `.Finally(...)`)
* **Strongly-typed handlers** using `in` parameters
diff --git a/src/PatternKit.Core/Behavioral/Command/Command.cs b/src/PatternKit.Core/Behavioral/Command/Command.cs
index 9d57cba..3d1ffef 100644
--- a/src/PatternKit.Core/Behavioral/Command/Command.cs
+++ b/src/PatternKit.Core/Behavioral/Command/Command.cs
@@ -3,12 +3,94 @@
namespace PatternKit.Behavioral.Command;
///
-/// Allocation-light command that executes an action (and optional undo) over an input context.
-/// Supports sync/async via ValueTask and can be composed into a macro with ordered execution and reverse undo.
+/// 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
+/// () execution and macro (composite) commands.
///
+///
+/// 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 instance is immutable after and therefore reusable across threads. Thread safety of the underlying action depends on the provided delegates and the 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, is false and 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
+/// .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
+/// .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
+/// Log);
+/// var compile = Command.Create().Do(c => c.Log.Add("compile")).Undo(c => c.Log.Add("undo-compile")).Build();
+/// var test = Command.Create().Do(c => c.Log.Add("test")).Undo(c => c.Log.Add("undo-test")).Build();
+/// var pack = Command.Create().Do(c => c.Log.Add("pack")).Build(); // no undo
+/// bool runTests = true;
+/// var pipeline = Command.Macro()
+/// .Add(compile)
+/// .AddIf(runTests, test)
+/// .Add(pack)
+/// .Build();
+/// var ctx = new BuildCtx(new List());
+/// 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
+/// >.Create()
+/// .Do(list => list.Add("draft"))
+/// .Undo(list => list.Remove("draft"))
+/// .Build();
+/// ]]>
+///
+///
+/// Context type the command operates on.
public sealed class Command
{
- /// Asynchronous execution delegate for a command.
+ ///
+ /// Asynchronous (or synchronous) execution delegate signature.
+ /// Return default / completed for synchronous completion.
+ ///
+ /// Context (readonly reference).
+ /// Cancellation token.
public delegate ValueTask Exec(in TCtx ctx, CancellationToken ct);
private readonly Exec _do;
@@ -16,12 +98,25 @@ public sealed class Command
private Command(Exec @do, Exec? undo) => (_do, _undo) = (@do, undo);
- /// Execute the command against .
+ ///
+ /// Execute the command logic. Throws if the underlying delegate throws.
+ ///
+ /// Context value.
+ /// Cancellation token.
+ /// enabling allocation-free sync completion.
public ValueTask Execute(in TCtx ctx, CancellationToken ct) => _do(in ctx, ct);
+ /// Execute with .
public ValueTask Execute(in TCtx ctx) => _do(in ctx, CancellationToken.None);
- /// Attempt to undo the command; returns false when no undo was configured.
+ ///
+ /// Attempt to undo. Returns false if no undo handler was configured.
+ /// The returned must be awaited if true.
+ ///
+ /// Context.
+ /// Cancellation token.
+ /// Undo value task (valid only when result is true).
+ /// true if an undo handler exists; otherwise false.
public bool TryUndo(in TCtx ctx, CancellationToken ct, out ValueTask undoTask)
{
if (_undo is null)
@@ -29,87 +124,91 @@ public bool TryUndo(in TCtx ctx, CancellationToken ct, out ValueTask undoTask)
undoTask = default;
return false;
}
-
undoTask = _undo(in ctx, ct);
return true;
}
+ /// Convenience overload using .
public bool TryUndo(in TCtx ctx, out ValueTask undoTask) => TryUndo(in ctx, default, out undoTask);
- /// Whether this command has an undo handler.
+ /// Indicates whether this command has an undo handler.
public bool HasUndo => _undo is not null;
// ---- Builder ----
+ ///
+ /// Start building a new command. Provide a Do delegate (required) and optionally an Undo delegate.
+ ///
public static Builder Create() => new();
- /// Fluent builder for .
+ ///
+ /// Fluent builder for . Not thread-safe; configure on one thread then call once.
+ ///
public sealed class Builder
{
private Exec? _do;
private Exec? _undo;
- /// Set the required Do handler.
+ /// Set the required asynchronous (or synchronous) Do handler.
public Builder Do(Exec @do)
{
_do = @do;
return this;
}
- /// Set sync Do handler (adapter).
+ /// Set a synchronous Do handler (adapter wrapped in a ).
public Builder Do(Action @do)
{
- _do = (in c, _) =>
- {
- @do(c);
- return default;
- };
+ _do = (in c, _) => { @do(c); return default; };
return this;
}
- /// Set optional Undo handler.
+ /// Set the optional asynchronous Undo handler.
public Builder Undo(Exec undo)
{
_undo = undo;
return this;
}
- /// Set sync Undo handler (adapter).
+ /// Set a synchronous Undo handler (adapter).
public Builder Undo(Action undo)
{
- _undo = (in c, _) =>
- {
- undo(c);
- return default;
- };
+ _undo = (in c, _) => { undo(c); return default; };
return this;
}
- /// Build an immutable command.
+ /// Build an immutable command instance. Throws if no Do handler was supplied.
public Command Build()
=> new(_do ?? throw new InvalidOperationException("Command requires Do handler."), _undo);
}
// ---- Macro composition ----
+ ///
+ /// 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 MacroBuilder Macro() => new();
- /// Builder for a macro command that runs sub-commands in order and undoes in reverse.
+ ///
+ /// Builder for a macro command that runs sub-commands in order and undoes in reverse. Supports conditional inclusion via .
+ ///
public sealed class MacroBuilder
{
private readonly ChainBuilder> _chain = ChainBuilder>.Create();
+ /// Add a sub-command to the macro.
public MacroBuilder Add(Command cmd)
{
_chain.Add(cmd);
return this;
}
+ /// Add a sub-command only when is true.
public MacroBuilder AddIf(bool condition, Command cmd)
{
_chain.AddIf(condition, cmd);
return this;
}
- /// Build a macro command.
+ /// Finalize and build a macro command.
public Command Build()
{
var items = _chain.Build(static cmds => cmds);
@@ -121,12 +220,12 @@ ValueTask Do(in TCtx ctx, CancellationToken ct)
for (var i = 0; i < items.Length; i++)
{
var vt = items[i].Execute(in ctx, ct);
- if (!vt.IsCompletedSuccessfully)
- {
- // Slow path: await then continue; copy ctx to avoid capturing 'in' in async state machine
- var copy = ctx;
- return AwaitNext(i, vt, copy, ct, items);
- }
+ if (vt.IsCompletedSuccessfully)
+ continue;
+
+ // Not completed successfully: enter slow path (await then continue); copy ctx to avoid capturing 'in' in async state machine
+ var copy = ctx;
+ return AwaitNext(i, vt, copy, ct, items);
}
return default;
@@ -150,14 +249,11 @@ ValueTask Undo(in TCtx ctx, CancellationToken ct)
{
for (var i = items.Length - 1; i >= 0; i--)
{
- if (items[i].TryUndo(in ctx, ct, out var vt))
- {
- if (!vt.IsCompletedSuccessfully)
- {
- var copy = ctx;
- return AwaitUndo(i, vt, copy, ct, items);
- }
- }
+ if (!items[i].TryUndo(in ctx, ct, out var vt) || vt.IsCompletedSuccessfully)
+ continue;
+
+ var copy = ctx;
+ return AwaitUndo(i, vt, copy, ct, items);
}
return default;
@@ -167,11 +263,11 @@ static async ValueTask AwaitUndo(int startIndex, ValueTask pending, TCtx localCt
await pending.ConfigureAwait(false);
for (var j = startIndex - 1; j >= 0; j--)
{
- if (itemsArr[j].TryUndo(in localCtx, ct2, out var t))
- {
- if (!t.IsCompletedSuccessfully)
- await t.ConfigureAwait(false);
- }
+ if (!itemsArr[j].TryUndo(in localCtx, ct2, out var t) ||
+ t.IsCompletedSuccessfully)
+ continue;
+
+ await t.ConfigureAwait(false);
}
}
}
diff --git a/src/PatternKit.Core/Behavioral/Mediator/Mediator.cs b/src/PatternKit.Core/Behavioral/Mediator/Mediator.cs
index d4cf25d..4c63329 100644
--- a/src/PatternKit.Core/Behavioral/Mediator/Mediator.cs
+++ b/src/PatternKit.Core/Behavioral/Mediator/Mediator.cs
@@ -1,5 +1,6 @@
using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;
+using PatternKit.Common;
namespace PatternKit.Behavioral.Mediator;
@@ -110,10 +111,10 @@ private Mediator(
/// Cancellation token.
/// The handler produced response.
/// Thrown if no handler exists or the result cannot be cast to .
- public ValueTask Send(in TRequest request, CancellationToken ct = default)
+ public ValueTask Send(in TRequest request, CancellationToken ct = default)
=> Core(request, ct);
- private async ValueTask Core(TRequest request, CancellationToken ct)
+ private async ValueTask Core(TRequest request, CancellationToken ct)
{
if (!_commands.TryGetValue(typeof(TRequest), out var handler))
throw new InvalidOperationException($"No command handler registered for request type '{typeof(TRequest)}'.");
@@ -135,10 +136,13 @@ private async ValueTask Core(TRequest request, C
foreach (var b in _post)
await b(in boxed, obj, ct).ConfigureAwait(false);
- if (obj is TResponse r) return r;
- if (obj is null) return default(TResponse)!;
- throw new InvalidOperationException(
- $"Handler returned incompatible result for '{typeof(TRequest)}' -> expected '{typeof(TResponse)}', got '{obj.GetType()}'.");
+ return obj switch
+ {
+ TResponse r => r,
+ null => default,
+ _ => throw new InvalidOperationException(
+ $"Handler returned incompatible result for '{typeof(TRequest)}' -> expected '{typeof(TResponse)}', got '{obj.GetType()}'.")
+ };
}
///
@@ -212,9 +216,12 @@ private async IAsyncEnumerable StreamCore(
await foreach (var item in seq.ConfigureAwait(false).WithCancellation(ct))
{
- if (item is TItem ti) yield return ti;
- else if (item is null && default(TItem) is null) yield return default!;
- else throw new InvalidOperationException($"Stream item type mismatch. Expected '{typeof(TItem)}' but got '{item?.GetType()}'.");
+ yield return item switch
+ {
+ TItem ti => ti,
+ null when default(TItem) is null => default!,
+ _ => throw new InvalidOperationException($"Stream item type mismatch. Expected '{typeof(TItem)}' but got '{item?.GetType()}'.")
+ };
}
// After stream completes
@@ -222,10 +229,9 @@ private async IAsyncEnumerable StreamCore(
await b(in boxed, null, ct).ConfigureAwait(false);
}
- private readonly struct BoxedAsyncEnumerable
+ private readonly struct BoxedAsyncEnumerable(IAsyncEnumerable