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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,8 +446,8 @@ var cachedRemoteProxy = Proxy<int, string>.Create(id => remoteProxy.Execute(id))
---

## 📚 Patterns Table
| 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](docs/patterns/structural/decorator/decorator.md) • [Facade](docs/patterns/structural/facade/facade.md) • [Flyweight](docs/patterns/structural/flyweight/index.md) • [Proxy](docs/patterns/structural/proxy/index.md) |
| **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) • [ReplayableSequence](docs/patterns/behavioral/iterator/replayablesequence.md) • [WindowSequence](docs/patterns/behavioral/iterator/windowsequence.md) • [Command](docs/patterns/behavioral/command/command.md) • [Mediator](docs/patterns/behavioral/mediator/mediator.md) • [Memento](docs/patterns/behavioral/memento/memento.md) • [Observer](docs/patterns/behavioral/observer/observer.md) • [State](docs/patterns/behavioral/state/state.md) ✓ • Template Method (planned) • Visitor (planned) |
| Category | Patterns |
| -------------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **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](docs/patterns/structural/decorator/decorator.md) • [Facade](docs/patterns/structural/facade/facade.md) • [Flyweight](docs/patterns/structural/flyweight/index.md) • [Proxy](docs/patterns/structural/proxy/index.md) |
| **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) • [ReplayableSequence](docs/patterns/behavioral/iterator/replayablesequence.md) • [WindowSequence](docs/patterns/behavioral/iterator/windowsequence.md) • [Command](docs/patterns/behavioral/command/command.md) • [Mediator](docs/patterns/behavioral/mediator/mediator.md) • [Memento](docs/patterns/behavioral/memento/memento.md) • [Observer](docs/patterns/behavioral/observer/observer.md) • [State](docs/patterns/behavioral/state/state.md) • [Template Method](docs/patterns/behavioral/template/template.md) • [Visitor](docs/patterns/behavioral/visitor/visitor.md) |
99 changes: 99 additions & 0 deletions docs/examples/api-exception-mapping-visitor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Visitor — API Exception Mapping (ASP.NET Core)

Map exceptions to HTTP responses with a result visitor. Centralize error policy without scattering `catch` blocks across controllers.

---

## Goal

- Convert exceptions to `ProblemDetails` or `IResult`.
- Keep mappings in one place; add specialized cases without touching controllers.
- Share a single, immutable visitor via DI.

---

## Exception Types

```csharp
public sealed class NotFoundException(string resource, string id) : Exception($"{resource} '{id}' not found");
public sealed class ValidationException(IDictionary<string, string[]> errors) : Exception("Validation failed");
public sealed class ForbiddenException(string? reason = null) : Exception(reason ?? "Forbidden");
```

---

## Visitor Registration

```csharp
// Program.cs or composition root
builder.Services.AddSingleton<Visitor<Exception, IResult>>(_ =>
Visitor<Exception, IResult>
.Create()
.On<NotFoundException>(ex => Results.Problem(
statusCode: StatusCodes.Status404NotFound,
title: "Not Found",
detail: ex.Message))
.On<ValidationException>(ex => Results.ValidationProblem(
errors: ex.Errors.ToDictionary(kv => kv.Key, kv => kv.Value)))
.On<ForbiddenException>(ex => Results.Problem(
statusCode: StatusCodes.Status403Forbidden,
title: "Forbidden",
detail: ex.Message))
.Default(ex => Results.Problem(
statusCode: StatusCodes.Status500InternalServerError,
title: "Server Error",
detail: builder.Environment.IsDevelopment() ? ex.ToString() : ex.Message))
.Build());
```

---

## Middleware

```csharp
public sealed class ExceptionMappingMiddleware(
RequestDelegate next,
Visitor<Exception, IResult> mapper,
ILogger<ExceptionMappingMiddleware> log)
{
public async Task Invoke(HttpContext ctx)
{
try
{
await next(ctx);
}
catch (Exception ex)
{
log.LogError(ex, "Unhandled exception");
var result = mapper.Visit(ex);
await result.ExecuteAsync(ctx);
}
}
}

// Program.cs
app.UseMiddleware<ExceptionMappingMiddleware>();
```

---

## Why This Works Well

- The mapping is explicit and ordered; specific exceptions come first.
- A default keeps APIs resilient to unknown errors.
- The visitor instance is immutable and safe to reuse across requests.

---

## Tests (sketch)

```csharp
[Fact]
public void Maps_NotFound_To_404()
{
var v = BuildMapper();
var res = v.Visit(new NotFoundException("Order", "123"));
// Assert res is Problem with 404...
}
```

76 changes: 76 additions & 0 deletions docs/examples/event-processor-visitor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Visitor — Event Processing (Orchestration)

Use action and async visitors to route domain events to handlers. Keep orchestration logic discoverable and easy to extend.

---

## Domain Events

```csharp
abstract record Event(DateTimeOffset At);
record OrderPlaced(string OrderId, decimal Total, DateTimeOffset At) : Event(At);
record PaymentCaptured(string OrderId, string TxId, DateTimeOffset At) : Event(At);
record ShipmentScheduled(string OrderId, string Tracking, DateTimeOffset At) : Event(At);
record AuditLog(string Message, DateTimeOffset At) : Event(At);
```

---

## Async Orchestrator

```csharp
public interface IEmail { Task SendAsync(string to, string subject, CancellationToken ct); }
public interface IAccounting { Task RecordAsync(string orderId, decimal total, CancellationToken ct); }
public interface IShipping { Task ScheduleAsync(string orderId, string tracking, CancellationToken ct); }

public static class EventOrchestrator
{
public static AsyncActionVisitor<Event> Build(IEmail email, IAccounting acct, IShipping ship)
=> AsyncActionVisitor<Event>
.Create()
.On<OrderPlaced>(async (e, ct) =>
{
await acct.RecordAsync(e.OrderId, e.Total, ct);
await email.SendAsync("ops@example.com", $"Order {e.OrderId} placed", ct);
})
.On<PaymentCaptured>(async (e, ct) =>
await email.SendAsync("ops@example.com", $"Payment captured {e.TxId}", ct))
.On<ShipmentScheduled>(async (e, ct) =>
await ship.ScheduleAsync(e.OrderId, e.Tracking, ct))
.Default((e, _) => { /* ignore or metric */ return default; })
.Build();
}
```

Usage
```csharp
var orchestrator = EventOrchestrator.Build(email, accounting, shipping);
foreach (var e in events) await orchestrator.VisitAsync(e, ct);
```

---

## Synchronous Projection (Result Visitor)

```csharp
public static class EventProjection
{
public static Visitor<Event, string> BuildSummary()
=> Visitor<Event, string>
.Create()
.On<OrderPlaced>(e => $"Placed:{e.OrderId}:{e.Total:C}")
.On<PaymentCaptured>(e => $"Paid:{e.OrderId}:{e.TxId}")
.On<ShipmentScheduled>(e => $"Ship:{e.OrderId}:{e.Tracking}")
.Default(e => $"Audit:{e.At:O}")
.Build();
}
```

---

## Testing Tips

- Cover each event type and the default path.
- Assert side effects by using test doubles or counters.
- Include cancellation tests to ensure `ct` is respected.

65 changes: 65 additions & 0 deletions docs/examples/message-router-visitor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Visitor — Message Router (Background Worker)

Dispatch messages pulled from a queue to processors keyed by runtime type. Scale out by sharing a single immutable visitor across workers.

---

## Messages

```csharp
abstract record Message(string Id);
record UserCreated(string Id, string Email) : Message(Id);
record OrderSubmitted(string Id, string OrderId) : Message(Id);
record InventoryLow(string Id, string Sku, int Qty) : Message(Id);
```

---

## Router (Async Action Visitor)

```csharp
public interface IMessageHandlers
{
Task On(UserCreated m, CancellationToken ct);
Task On(OrderSubmitted m, CancellationToken ct);
Task On(InventoryLow m, CancellationToken ct);
Task OnUnknown(Message m, CancellationToken ct);
}

public static class MessageRouter
{
public static AsyncActionVisitor<Message> Build(IMessageHandlers h)
=> AsyncActionVisitor<Message>
.Create()
.On<UserCreated>(h.On)
.On<OrderSubmitted>(h.On)
.On<InventoryLow>(h.On)
.Default(h.OnUnknown)
.Build();
}
```

---

## Worker Loop

```csharp
public sealed class Worker(AsyncActionVisitor<Message> router, ILogger<Worker> log)
{
public async Task RunAsync(IAsyncEnumerable<Message> stream, CancellationToken ct)
{
await foreach (var m in stream.WithCancellation(ct))
{
try { await router.VisitAsync(m, ct); }
catch (OperationCanceledException) when (ct.IsCancellationRequested) { }
catch (Exception ex) { log.LogError(ex, "Message failed: {Id}", m.Id); }
}
}
}
```

Operational notes
- Handlers receive a `CancellationToken` for graceful shutdown.
- Add metrics/logging to the default branch to analyze unknown message types.
- For high volume, prefer minimal allocations in handlers.

Loading
Loading