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
73 changes: 71 additions & 2 deletions docs/chain-of-responsibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Ordered handlers process a shared context. Each handler can short-circuit (skip
| `HandlerPipeline<TContext>` | Immutable pipeline — `InvokeAsync`, `InvokeTracedAsync` |
| `HandlerPipelineTrace` | Per-invocation step list |
| `HandlerPipelineStep` | Index, handler display name, `HandlerPipelineStepStatus` |
| `HandlerPipelineStepStatus` | `Completed` / `ShortCircuited` / `NotReached` |
| `HandlerPipelineStepStatus` | `Completed` / `ShortCircuited` / `NotReached` / `Skipped` / `Failed` |

```csharp
var pipeline = new HandlerPipelineBuilder<RequestContext>()
Expand Down Expand Up @@ -50,6 +50,8 @@ foreach (var step in trace.Steps)
| **Completed** | Handler ran and called `next` |
| **ShortCircuited** | Handler ran but did **not** call `next` |
| **NotReached** | Handler never ran (an earlier handler short-circuited) |
| **Skipped** | Handler guard returned `false` — handler not invoked, pipeline continues |
| **Failed** | Handler (or its guard) threw an exception — captured in trace, then re-thrown |

`InvokeTracedAsync` does not change execution order or short-circuit rules — it only returns a trace. Delegate handlers appear as `"<delegate>"`.

Expand All @@ -67,7 +69,74 @@ public sealed class AuthorizationHandler : IRequestHandler { ... }

## Diagnostics

DP005, DP008–DP009, DP024 (Info + CodeFix for missing `[HandlerOrder]`).
DP005, DP008–DP009, DP024 (Info + CodeFix for missing `[HandlerOrder]`). DP050–DP052 for guard method validation.

## Guard predicates

Conditionally skip handlers at runtime using guard predicates. When a guard returns `false`, the handler is skipped and the pipeline continues to the next handler.

### Source generator

Use the `Guard` property on `[HandlerOrder]` to reference a static guard method:

```csharp
[HandlerOrder<RequestContext>(1, Guard = nameof(CanHandle))]
public sealed class AuthorizationHandler : IHandler<RequestContext>
{
private static bool CanHandle(RequestContext ctx) => ctx.IsAuthenticated;
}
```

The generator validates the guard method signature (DP050–DP052).

### Traced invocation

When using `InvokeTracedAsync`, skipped handlers appear with status `Skipped`:

```csharp
var trace = await pipeline.InvokeTracedAsync(context, cancellationToken);

foreach (var step in trace.Steps)
{
Console.WriteLine($"{step.Index}: {step.Name} → {step.Status}");
}
```

## Exception observability

Capture handler exceptions in traces and receive notifications for logging or metrics without changing handler implementations.

### Trace fields

`HandlerPipelineTrace` provides:

- `FailedHandlerIndex` — Zero-based index of the failing handler, or `-1` when no handler failed
- `Exception` — The exception thrown by the failing handler, or `null` when no handler failed

```csharp
var trace = await pipeline.InvokeTracedAsync(context, cancellationToken);

if (trace.FailedHandlerIndex >= 0)
{
Console.WriteLine($"Handler {trace.FailedHandlerIndex} failed: {trace.Exception?.Message}");
}
```

### Observers

Implement `IHandlerExceptionObserver<TContext>` to receive callbacks when handlers throw:

```csharp
public sealed class LoggingExceptionObserver<TContext> : IHandlerExceptionObserver<TContext>
{
public void OnHandlerException(TContext context, int handlerIndex, string handlerName, Exception exception)
=> Console.WriteLine($"Handler {handlerName} (index {handlerIndex}) failed: {exception.Message}");
}

var trace = await pipeline.InvokeTracedAsync(context, new LoggingExceptionObserver<RequestContext>(), cancellationToken);
```

Pass the observer to `InvokeTracedAsync` as the second parameter. The observer is notified before the exception is re-thrown.

## Sample

Expand Down
36 changes: 35 additions & 1 deletion docs/composite.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,41 @@ Multi-root catalogs: `BuildRoot()` throws `CompositeAssemblyException` at runtim

## Diagnostics

DP010–DP015.
DP010–DP015. DP040 for unregistered DI nodes. DP041 for visitor coverage.

## DI integration

When the `DesignPatterns.Extensions.DependencyInjection` package is referenced, the source generator emits a `RegisterDi(IServiceCollection, ServiceLifetime)` method for each contract. This registers all composite parts with the DI container and enables `BuildRoot(IServiceProvider)` to resolve nodes from the container.

```csharp
MenuNodeCompositeCatalog.RegisterDi(services);

var provider = services.BuildServiceProvider();
var root = MenuNodeCompositeCatalog.BuildRoot(provider);
```

Parts must be registered in the container before calling `BuildRoot(IServiceProvider)`. If a part is not registered, the generator reports **DP040** at compile time.

## Visitor generation

The source generator can optionally emit a visitor interface and `AcceptVisitor` extension methods for traversing composite trees with type-safe double dispatch.

For a contract `IMenuNode`, the generator emits:

- `IMenuNodeVisitor` — visitor interface with `Visit` methods for each concrete node type
- `AcceptVisitor<TVisitor>(this IMenuNode, TVisitor)` extension methods

```csharp
public class MenuPrinter : IMenuNodeVisitor
{
public void Visit(HomeMenu node) => Console.WriteLine($"Home: {node.Title}");
public void Visit(SettingsMenu node) => Console.WriteLine($"Settings: {node.Title}");
}

root.AcceptVisitor(new MenuPrinter());
```

If a node type is added but the visitor interface is not updated, the generator reports **DP041**.

## Sample

Expand Down
40 changes: 39 additions & 1 deletion docs/decorator.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Stack cross-cutting behaviors around a core service without subclass explosion.
## Runtime

- `IDecorator<T>` — decorator contract
- `IAsyncDecorator<T>` — async decorator contract (`DecorateAsync` with `CancellationToken`)
- `DecoratorStackBuilder<T>` — ordered composition; optional `Add(..., Func<bool>)` skips a decorator when the predicate is false at **build** time

### Conditional registration
Expand Down Expand Up @@ -43,7 +44,44 @@ Lower order wraps closer to the core; outer decorators run first on the way in.

## Diagnostics

DP016–DP019.
DP016–DP019. DP042 for async signature validation. DP043 for DI resolvability.

## DI integration

When the `DesignPatterns.Extensions.DependencyInjection` package is referenced, the source generator emits a `RegisterDi(IServiceCollection, ServiceLifetime)` method for each service contract. This registers all decorators with the DI container and enables `Build(IServiceProvider, core)` to resolve decorators from the container.

```csharp
PaymentServiceDecoratorStack.RegisterDi(services);

var provider = services.BuildServiceProvider();
var core = new PaymentService();
var service = PaymentServiceDecoratorStack.Build(provider, core);
```

Decorators must be registered in the container before calling `Build(IServiceProvider, core)`. The core service itself is not registered automatically — you provide it explicitly to `Build`.

## Async variant

For asynchronous decoration scenarios, implement `IAsyncDecorator<T>` instead of `IDecorator<T>`. The async decorator uses `DecorateAsync` instead of `Decorate`:

```csharp
public interface IAsyncDecorator<TService>
{
ValueTask<TService> DecorateAsync(TService inner, CancellationToken cancellationToken = default);
}

[Decorator<IPaymentService>(10)]
public sealed class AsyncCachingPaymentDecorator : IPaymentService, IAsyncDecorator<IPaymentService>
{
public async ValueTask<IPaymentService> DecorateAsync(IPaymentService inner, CancellationToken ct = default)
{
await InitializeCacheAsync(ct);
return this;
}
}
```

The source generator validates async decorator signatures (DP042–DP043). Use `IAsyncDecorator<T>` when decorator initialization requires async work (loading configuration, warming caches, establishing connections). For simple synchronous wrapping, `IDecorator<T>` is sufficient.

## Sample

Expand Down
57 changes: 55 additions & 2 deletions docs/dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,52 @@ Extension methods such as:

- `AddStrategyRegistry<TKey, TStrategy>(...)`
- `AddFactoryRegistry<TKey, TProduct>(...)`
- `AddAsyncFactoryRegistry<TKey, TProduct>(...)`
- `AddPooledFactoryRegistry<TKey, TProduct>(..., poolSize)`
- `AddHandlerPipeline<TContext>(...)`
- `AddTransitionTable<TState, TTrigger>(...)`
- `AddStateMachine<TState, TTrigger>(...)`

### Async factory registry

```csharp
services.AddAsyncFactoryRegistry<string, IProduct>(builder =>
{
builder.Register("standard", ct => new ValueTask<IProduct>(new StandardProduct()));
});
```

### Pooled factory registry

```csharp
services.AddPooledFactoryRegistry<string, IProduct>(builder =>
{
builder.Register("buffer", () => new BufferProduct());
}, poolSize: 16);
```

### State machine

```csharp
services.AddTransitionTable<OrderStatus, OrderTrigger>();
services.AddStateMachine<OrderStatus, OrderTrigger>();
```

Use `ServiceLifetime.Transient` for `AddStateMachine` when each consumer needs its own state tracking.

## Generated `RegisterDi`

When the DI package (or its MSBuild targets) is referenced, Strategy / Factory / Handler generators can emit:
When the DI package (or its MSBuild targets) is referenced, Strategy / Factory / Handler / State / Composite / Decorator / EventAggregator generators emit:

```csharp
PaymentStrategyRegistry.RegisterDi(services);
OrderStatusStateMachine.RegisterDi(services);
MenuNodeCompositeCatalog.RegisterDi(services);
PaymentServiceDecoratorStack.RegisterDi(services);
OrderPlacedEventHandlerRegistry.RegisterDi(services);
```

Handlers and strategies resolve from DI; registries register as singletons by default (see sample for transient factory behavior).
Registries register as singletons by default. **Factory** registries default to `Transient` for implementation types (matching factory semantics — each `Create` returns a new instance).

## Project setup

Expand All @@ -37,3 +72,21 @@ Set `DesignPatternsSampleKind=DependencyInjection` in sample projects for the sh
## Note on meta package

The core **`Skymly.DesignPatterns`** NuGet meta package does **not** include the DI extension; reference `DesignPatterns.Extensions.DependencyInjection` from the main repo (sibling project) until a separate package is published.

## Autofac integration

The **`DesignPatterns.Extensions.Autofac`** package provides Autofac integration symmetric to the MSDI `RegisterDi` pattern. When referenced, the source generator emits `RegisterAutofac(ContainerBuilder)` and `Create(ILifetimeScope)` methods for Strategy, Factory, Handler, and State registries.

```csharp
var builder = new ContainerBuilder();
PaymentStrategyRegistry.RegisterAutofac(builder);

using var container = builder.Build();
var registry = container.Resolve<IStrategyRegistry<string, IPaymentStrategy>>();
```

`RegisterAutofac` accepts optional parameters:
- `sharing` — `InstanceSharing.Shared` (default, `SingleInstance()`) or `InstanceSharing.None` (`InstancePerDependency()`)
- `serviceKey` — optional key for keyed registration

The Autofac extension can be referenced alongside `DesignPatterns.Extensions.DependencyInjection` — both `RegisterDi` and `RegisterAutofac` methods are generated. The Autofac extension is **not** included in the meta package; reference it separately.
68 changes: 68 additions & 0 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,74 @@ See [Plugin assemblies sample](https://github.com/Skymly/DesignPatterns.Samples/

See [State transition table](./state-transition-table.md).

## State guard (DP032, DP034–DP035) {#dp032}

| ID | Severity | When |
|----|----------|------|
| **DP032** | Error | `[Transition(Guard = nameof(Method))]` guard method not found on holder class |
| **DP034** | Error | Guard method is not `static` (defensive — C# compiler rejects instance methods on static classes first) |
| **DP035** | Error | Guard method has wrong signature (must be `static bool Method(TState, TTrigger)`) |

## State literal edge (DP036) {#dp036}

| ID | Severity | When |
|----|----------|------|
| **DP036** | Info | `TryTransition` called with literal `(state, trigger)` arguments that do not match any declared `[Transition]` edge |

## State entry/exit actions (DP037–DP039) {#dp037}

| ID | Severity | When |
|----|----------|------|
| **DP037** | Error | `[Transition(OnEnter/OnExit = nameof(Method))]` action method not found on holder class |
| **DP038** | Error | Action method is not `static` (unreachable in practice — CS0708 fires first; retained for completeness) |
| **DP039** | Error | Action method has wrong signature (must be `static void Method(TState, TState, TTrigger)` or `static ValueTask Method(TState, TState, TTrigger, CancellationToken)`) |

## Composite DI + Visitor (DP040–DP041) {#dp040}

| ID | Severity | When |
|----|----------|------|
| **DP040** | Error | `BuildRoot(IServiceProvider)` called but a composite node is not registered in the container |
| **DP041** | Error | Generated `I{Contract}NodeVisitor` does not cover all node types (visitor missing a `Visit` overload) |

## Decorator DI + async (DP042–DP043) {#dp042}

| ID | Severity | When |
|----|----------|------|
| **DP042** | Error | Async decorator method signature is incorrect (`DecorateAsync` must return `ValueTask<TService>`) |
| **DP043** | Error | Async decorator cannot be resolved from DI when using `Build(IServiceProvider, core)` |

## EventAggregator (DP044–DP046) {#dp044}

| ID | Severity | When |
|----|----------|------|
| **DP044** | Info | Type implements `IEventHandler<T>` but is not marked with `[RegisterEventHandler]` |
| **DP045** | Error | Duplicate `[RegisterEventHandler]` on the same handler for the same event type |
| **DP046** | Error | Type marked with `[RegisterEventHandler<T>]` but does not implement `IEventHandler<T>` |

## Strategy guard (DP047–DP049) {#dp047}

| ID | Severity | When |
|----|----------|------|
| **DP047** | Error | `[RegisterStrategy(Guard = nameof(Method))]` guard method not found on strategy class |
| **DP048** | Error | Guard method is not `static` |
| **DP049** | Error | Guard method has wrong signature (must be `static bool Method(TKey)`) |

## Handler guard (DP050–DP052) {#dp050}

| ID | Severity | When |
|----|----------|------|
| **DP050** | Error | `[HandlerOrder(Guard = nameof(Method))]` guard method not found on handler class |
| **DP051** | Error | Guard method is not `static` |
| **DP052** | Error | Guard method has wrong signature (must be `static bool Method(TContext)`) |

## Factory async + pooling (DP053–DP055) {#dp053}

| ID | Severity | When |
|----|----------|------|
| **DP053** | Error | `[RegisterFactory(IsAsync = true)]` but factory does not implement `IAsyncFactory<T>` |
| **DP054** | Error | `PoolSize` is negative (must be ≥ 0; 0 disables pooling) |
| **DP055** | Warning | `PoolSize` > 1024 (may cause excessive memory usage) |

## Code fixes

`DesignPatterns.CodeFixes` ships inside the **`Skymly.DesignPatterns`** meta package (`analyzers/dotnet/cs`). Selected diagnostics offer one-click fixes in the IDE (requires C# Workspaces):
Expand Down
Loading
Loading