Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ PatternKit will grow to cover **Creational**, **Structural**, and **Behavioral**

| Category | Patterns ✓ = implemented |
| -------------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Creational** | [Factory](docs/patterns/creational/factory/factory.md) ✓ • Builder ✓ • [Prototype](docs/patterns/creational/prototype/prototype.md) ✓ • [Singleton](docs/patterns/creational/singleton/singleton.md) ✓ |
| **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) |

Expand Down
103 changes: 103 additions & 0 deletions docs/examples/mediator-demo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Replacing MediatR with PatternKit.Mediator — DI Scanning + Behaviors (PoC)

This demo shows how to wire PatternKit’s allocation-light Mediator into a typical .NET app with DI scanning, request/notification/stream handlers, and pipeline behaviors — near-parity with MediatR.

What it demonstrates
- DI scanning for handlers and pipeline behaviors via a single extension
- Send (request/response), Publish (fan-out notifications), and Stream (IAsyncEnumerable)
- Open-generic pipeline behaviors (e.g., LoggingBehavior<TRequest,TResponse>)
- Zero-alloc fast path via ValueTask and in parameters

Where to look
- Code: src/PatternKit.Examples/MediatorDemo/
- Abstractions.cs: MediatR-like abstractions and IServiceCollection.AddPatternKitMediator
- Demo.cs: sample commands, notifications, stream requests, handlers, and a logging behavior
- Tests: test/PatternKit.Examples.Tests/MediatorDemo/MediatorDemoTests.cs (TinyBDD scenarios)

Quick start
```csharp
using Microsoft.Extensions.DependencyInjection;
using PatternKit.Examples.MediatorDemo;

var services = new ServiceCollection();
services.AddSingleton<IMediatorDemoSink, MediatorDemoSink>();
// Scan current assembly (or provide others)
services.AddPatternKitMediator(typeof(PingHandler).Assembly);
var sp = services.BuildServiceProvider();

var mediator = sp.GetRequiredService<IAppMediator>();

// Send (request/response)
var pong = await mediator.Send(new PingCmd(7)); // "pong:7"
var sum = await mediator.Send(new SumCmd(2, 3)); // 5

// Publish (fan-out notifications)
await mediator.Publish(new UserCreated("Ada")); // both handlers run

// Stream (IAsyncEnumerable), netstandard2.1+/netcoreapp3.0+
await foreach (var i in mediator.Stream(new CountUpCmd(3, 4)))
Console.WriteLine(i); // 3, 4, 5, 6
```

Handlers
```csharp
public readonly record struct PingCmd(int Value) : ICommand<string>;
public sealed class PingHandler : ICommandHandler<PingCmd, string>
{
public ValueTask<string> Handle(PingCmd r, CancellationToken ct) => new($"pong:{r.Value}");
}

public sealed record UserCreated(string Name) : INotification;
public sealed class AuditLogHandler : INotificationHandler<UserCreated>
{
public ValueTask Handle(UserCreated n, CancellationToken ct) { /* log */ return default; }
}

public readonly record struct CountUpCmd(int From, int Count) : IStreamRequest<int>;
public sealed class CountUpHandler : IStreamRequestHandler<CountUpCmd, int>
{
public async IAsyncEnumerable<int> Handle(CountUpCmd r, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct)
{ for (int i = 0; i < r.Count; i++) { await Task.Yield(); yield return r.From + i; } }
}
```

Pipeline behaviors
```csharp
public sealed class LoggingBehavior<TRequest,TResponse>(IMediatorDemoSink sink) : IPipelineBehavior<TRequest,TResponse>
where TRequest : ICommand<TResponse>
{
public async ValueTask<TResponse> Handle(TRequest req, CancellationToken ct, Func<TRequest, CancellationToken, ValueTask<TResponse>> next)
{
sink.Log.Add($"before:{typeof(TRequest).Name}");
var res = await next(req, ct);
sink.Log.Add($"after:{typeof(TRequest).Name}:{res}");
return res;
}
}
```

DI scanning
- Call services.AddPatternKitMediator(assemblies) to scan types implementing:
- ICommandHandler<TRequest,TResponse>
- INotificationHandler<TNotification>
- IStreamRequestHandler<TRequest,TItem>
- IPipelineBehavior<TRequest,TResponse> (open generic supported)
- Handlers/behaviors are registered transient; IMediator facade (IAppMediator) is scoped.

Near-parity with MediatR
- Send/Publish/Stream APIs via IAppMediator
- Pipeline behaviors (open-generic) with before/after orchestration
- DI scanning for automatic wire-up

Differences
- Uses PatternKit.Behavioral.Mediator under the hood (ValueTask everywhere, in parameters, minimal allocations)
- The IAppMediator is an example facade; you can adapt names/signatures to match your project.

Run the demo tests
```bash
# From the repo root
dotnet build PatternKit.slnx -c Debug
dotnet test PatternKit.slnx -c Debug
```
Tests include send/publish/stream and behavior ordering assertions.

92 changes: 92 additions & 0 deletions docs/patterns/behavioral/command/command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Command — Command<TCtx>

A minimal, allocation-light Command pattern:

- Encapsulates an action (Do) and optional Undo over a context TCtx
- Executes synchronously or asynchronously via ValueTask
- Composes multiple commands into a macro that executes in order and undoes in reverse
- Thread-safe after Build(); uses in parameters for structs

---

## Quick start

```csharp
using PatternKit.Behavioral.Command;

// Single command
var cmd = Command<MyCtx>.Create()
.Do(static (in MyCtx c, CancellationToken _) => { Console.WriteLine($"hello {c.Name}"); return default; })
.Undo(static (in MyCtx c, CancellationToken _) => { Console.WriteLine($"undo {c.Name}"); return default; })
.Build();

var ctx = new MyCtx("Ada");
await cmd.Execute(ctx); // hello Ada
await cmd.TryUndo(ctx, out var t); // t completes; prints "undo Ada"

// Macro
var a = Command<MyCtx>.Create().Do(static (in MyCtx c, _) => { Console.Write("A"); return default; }).Build();
var b = Command<MyCtx>.Create().Do(static (in MyCtx c, _) => { Console.Write("B"); return default; }).Build();

var macro = Command<MyCtx>.Macro().Add(a).Add(b).Build();
await macro.Execute(ctx); // prints AB
```

```csharp
public readonly record struct MyCtx(string Name);
```

---

## API (at a glance)

```csharp
public sealed class Command<TCtx>
{
public delegate ValueTask Exec(in TCtx ctx, CancellationToken ct);

public static Builder Create();
public ValueTask Execute(in TCtx ctx, CancellationToken ct);
public ValueTask Execute(in TCtx ctx); // ct = default

public bool TryUndo(in TCtx ctx, CancellationToken ct, out ValueTask undoTask);
public bool TryUndo(in TCtx ctx, out ValueTask undoTask); // ct = default
public bool HasUndo { get; }

public sealed class Builder
{
public Builder Do(Exec handler); // required
public Builder Do(Action<TCtx> handler); // sync adapter
public Builder Undo(Exec handler); // optional
public Builder Undo(Action<TCtx> handler); // sync adapter
public Command<TCtx> Build();
}

public static MacroBuilder Macro();

public sealed class MacroBuilder
{
public MacroBuilder Add(Command<TCtx> cmd);
public MacroBuilder AddIf(bool condition, Command<TCtx> cmd);
public Command<TCtx> Build(); // executes in order; Undo runs in reverse
}
}
```

### Design notes

- Uses in parameters to avoid copies of struct contexts.
- ValueTask everywhere keeps the sync-fast path allocation-free.
- Macro execution is optimized for the fast path: it returns immediately if all steps complete synchronously; otherwise it awaits only when needed.

### Error behavior

- Execute/Undo propagate exceptions from user handlers.
- TryUndo returns false if no Undo was configured.

---

## Testing

See PatternKit.Tests/Behavioral/Command/CommandTests.cs for TinyBDD scenarios covering Do/Undo and macro ordering.

126 changes: 126 additions & 0 deletions docs/patterns/behavioral/mediator/mediator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Mediator — Mediator

An allocation-light mediator that coordinates messages between loosely coupled components.
It supports:

- Commands (request/response) with ValueTask
- Notifications (fan-out) with ValueTask
- Streaming commands via IAsyncEnumerable<T> (netstandard2.1+/netcoreapp3.0+)
- Global behaviors: Pre, Post, and Whole (around) with minimal overhead
- Sync adapters for convenience

The mediator is immutable and thread-safe after Build(). Builders are mutable and not thread-safe.

---

## Quick start

```csharp
using PatternKit.Behavioral.Mediator;

var mediator = Mediator.Create()
// behaviors
.Pre(static (in object req, CancellationToken _) => { Console.WriteLine($"pre:{req.GetType().Name}"); return default; })
.Whole(static (in object req, CancellationToken ct, Mediator.MediatorNext next) => next(in req, ct))
.Post(static (in object req, object? res, CancellationToken _) => { Console.WriteLine($"post:{res}"); return default; })

// command: Ping -> string
.Command<Ping, string>(static (in Ping p, CancellationToken _) => new ValueTask<string>("pong:" + p.Value))

// notification: Note
.Notification<Note>(static (in Note n, CancellationToken _) => { Console.WriteLine(n.Text); return default; })

#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER
// streaming command
.Stream<RangeRequest, int>(static (in RangeRequest r, CancellationToken _) => Range(r.Start, r.Count))
#endif

.Build();

var s = await mediator.Send<Ping, string>(new Ping(5)); // "pong:5"
await mediator.Publish(new Note("hello"));

#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER
await foreach (var i in mediator.Stream<RangeRequest, int>(new RangeRequest(2, 3)))
Console.WriteLine(i); // 2, 3, 4
#endif

public readonly record struct Ping(int Value);
public readonly record struct Note(string Text);
public readonly record struct RangeRequest(int Start, int Count);
#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER
static async IAsyncEnumerable<int> Range(int start, int count)
{
for (int i = 0; i < count; i++) { await Task.Yield(); yield return start + i; }
}
#endif
```

---

## Behaviors

- Pre(in object request, CancellationToken): runs before handler execution.
- Whole(in object request, CancellationToken, MediatorNext next): wraps the handler; can short-circuit or decorate.
- Post(in object request, object? response, CancellationToken): runs after handler completion.

Notifications are not wrapped by Whole behaviors (fire-and-forget fan-out semantics), but Pre/Post still run.

---

## API (at a glance)

```csharp
public sealed class Mediator
{
// Build
public static Mediator.Builder Create();

// Use
public ValueTask<TResponse> Send<TRequest, TResponse>(in TRequest request, CancellationToken ct = default);
public ValueTask Publish<TNotification>(in TNotification notification, CancellationToken ct = default);
#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER
public IAsyncEnumerable<TItem> Stream<TRequest, TItem>(in TRequest request, CancellationToken ct = default);
#endif

public sealed class Builder
{
// Behaviors
public Builder Pre(PreBehavior behavior);
public Builder Post(PostBehavior behavior);
public Builder Whole(WholeBehavior behavior);

// Commands
public Builder Command<TRequest, TResponse>(CommandHandlerTyped<TRequest, TResponse> handler);
public Builder Command<TRequest, TResponse>(SyncCommandHandlerTyped<TRequest, TResponse> handler);

// Notifications
public Builder Notification<TNotification>(NotificationHandlerTyped<TNotification> handler);
public Builder Notification<TNotification>(SyncNotificationHandlerTyped<TNotification> handler);

// Streams (netstandard2.1+/netcoreapp3.0+)
public Builder Stream<TRequest, TItem>(StreamHandlerTyped<TRequest, TItem> handler);

public Mediator Build();
}
}
```

### Design notes

- Typed delegates adapt to object-typed internal handlers once at Build time to keep hot paths tight.
- ValueTask everywhere; sync adapters avoid allocation when the work completes synchronously.
- Streaming requires netstandard2.1+/netcoreapp3.0+; the build conditionally includes the feature.

### Error behavior

- Send throws if no command handler is registered for the request type or if the handler returns an incompatible result type.
- Publish without handlers is a no-op.
- Stream throws if no stream handler is registered for the request type.

---

## Testing

See PatternKit.Tests/Behavioral/Mediator/MediatorTests.cs for TinyBDD scenarios covering Send, Publish, and Stream with behaviors.

4 changes: 4 additions & 0 deletions docs/patterns/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
href: behavioral/strategy/actionstrategy.md
- name: AsyncStrategy
href: behavioral/strategy/asyncstrategy.md
- name: Mediator
href: behavioral/mediator/mediator.md
- name: Command
href: behavioral/command/command.md
- name: Creational
items:
- name: Builder
Expand Down
Loading