diff --git a/docs/chain-of-responsibility.md b/docs/chain-of-responsibility.md index b49edfe..c7cae17 100644 --- a/docs/chain-of-responsibility.md +++ b/docs/chain-of-responsibility.md @@ -15,7 +15,7 @@ Ordered handlers process a shared context. Each handler can short-circuit (skip | `HandlerPipeline` | 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() @@ -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 `""`. @@ -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(1, Guard = nameof(CanHandle))] +public sealed class AuthorizationHandler : IHandler +{ + 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` to receive callbacks when handlers throw: + +```csharp +public sealed class LoggingExceptionObserver : IHandlerExceptionObserver +{ + 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(), cancellationToken); +``` + +Pass the observer to `InvokeTracedAsync` as the second parameter. The observer is notified before the exception is re-thrown. ## Sample diff --git a/docs/composite.md b/docs/composite.md index 78e739d..f2b3d63 100644 --- a/docs/composite.md +++ b/docs/composite.md @@ -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(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 diff --git a/docs/decorator.md b/docs/decorator.md index 9909de2..671820e 100644 --- a/docs/decorator.md +++ b/docs/decorator.md @@ -9,6 +9,7 @@ Stack cross-cutting behaviors around a core service without subclass explosion. ## Runtime - `IDecorator` — decorator contract +- `IAsyncDecorator` — async decorator contract (`DecorateAsync` with `CancellationToken`) - `DecoratorStackBuilder` — ordered composition; optional `Add(..., Func)` skips a decorator when the predicate is false at **build** time ### Conditional registration @@ -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` instead of `IDecorator`. The async decorator uses `DecorateAsync` instead of `Decorate`: + +```csharp +public interface IAsyncDecorator +{ + ValueTask DecorateAsync(TService inner, CancellationToken cancellationToken = default); +} + +[Decorator(10)] +public sealed class AsyncCachingPaymentDecorator : IPaymentService, IAsyncDecorator +{ + public async ValueTask DecorateAsync(IPaymentService inner, CancellationToken ct = default) + { + await InitializeCacheAsync(ct); + return this; + } +} +``` + +The source generator validates async decorator signatures (DP042–DP043). Use `IAsyncDecorator` when decorator initialization requires async work (loading configuration, warming caches, establishing connections). For simple synchronous wrapping, `IDecorator` is sufficient. ## Sample diff --git a/docs/dependency-injection.md b/docs/dependency-injection.md index cd1344e..01040b4 100644 --- a/docs/dependency-injection.md +++ b/docs/dependency-injection.md @@ -12,17 +12,52 @@ Extension methods such as: - `AddStrategyRegistry(...)` - `AddFactoryRegistry(...)` +- `AddAsyncFactoryRegistry(...)` +- `AddPooledFactoryRegistry(..., poolSize)` - `AddHandlerPipeline(...)` +- `AddTransitionTable(...)` +- `AddStateMachine(...)` + +### Async factory registry + +```csharp +services.AddAsyncFactoryRegistry(builder => +{ + builder.Register("standard", ct => new ValueTask(new StandardProduct())); +}); +``` + +### Pooled factory registry + +```csharp +services.AddPooledFactoryRegistry(builder => +{ + builder.Register("buffer", () => new BufferProduct()); +}, poolSize: 16); +``` + +### State machine + +```csharp +services.AddTransitionTable(); +services.AddStateMachine(); +``` + +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 @@ -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>(); +``` + +`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. diff --git a/docs/diagnostics.md b/docs/diagnostics.md index 80a961f..e98c417 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -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`) | +| **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` 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]` but does not implement `IEventHandler` | + +## 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` | +| **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): diff --git a/docs/event-aggregator.md b/docs/event-aggregator.md index dea6061..5e965fa 100644 --- a/docs/event-aggregator.md +++ b/docs/event-aggregator.md @@ -4,18 +4,101 @@ Namespace: `DesignPatterns.Behavioral` ## Overview -In-process pub/sub decoupling publishers from subscribers. No source generator — pure runtime API. +In-process pub/sub decoupling publishers from subscribers. Supports a source generator for auto-subscription, configurable error handling, and publish tracing. ## Runtime -- `IEventAggregator` — `Publish`, `Subscribe`, `Unsubscribe` +- `IEventAggregator` — `PublishAsync`, `Subscribe`, `Unsubscribe` - `IEventHandler` — typed handler +- `EventPublishErrorHandling` — `StopOnError` / `ContinueOnError` / `AggregateErrors` - Default implementation: thread-safe in-memory aggregator ```csharp var aggregator = new EventAggregator(); aggregator.Subscribe(new EmailHandler()); -aggregator.Publish(new OrderPlaced(orderId, total)); +await aggregator.PublishAsync(new OrderPlaced(orderId, total)); +``` + +## Source generator + +Mark event handlers with `[RegisterEventHandler]` (generic on .NET 7+, non-generic on netstandard2.0). The generator groups handlers by event type and emits a `{Event}EventHandlerRegistry` static class with subscription helpers. + +```csharp +public record OrderPlacedEvent(string OrderId, decimal Total); + +[RegisterEventHandler] +public sealed class EmailNotificationHandler : IEventHandler +{ + public ValueTask HandleAsync(OrderPlacedEvent evt, CancellationToken ct = default) + { + Console.WriteLine($"Email sent for order {evt.OrderId}"); + return default; + } +} + +var aggregator = new EventAggregator(); +OrderPlacedEventHandlerRegistry.SubscribeAll(aggregator); +await aggregator.PublishAsync(new OrderPlacedEvent("ORD-001", 99.99m)); +``` + +The generated registry provides: + +- `SubscribeAll(IEventAggregator)` — instantiates handlers with public parameterless constructors and subscribes them +- `RegisterDi(IServiceCollection, ServiceLifetime)` — registers handlers in the DI container (when `DesignPatterns.EnableDiIntegration=true`) +- `SubscribeAll(IEventAggregator, IServiceProvider)` — resolves handlers from DI and subscribes them + +### Diagnostics + +| ID | Severity | When | +|----|----------|------| +| **DP044** | Info | Type implements `IEventHandler` 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]` but does not implement `IEventHandler` | + +## Error isolation + +By default, if one handler throws an exception during `PublishAsync`, the exception propagates immediately and subsequent handlers are not called. To isolate handler failures, configure `EventPublishErrorHandling`: + +```csharp +var aggregator = new EventAggregator(errorHandling: EventPublishErrorHandling.ContinueOnError); +``` + +Available modes: + +| Mode | Behavior | +|------|----------| +| `StopOnError` (default) | First exception propagates immediately; remaining handlers skipped | +| `ContinueOnError` | All handlers execute; exceptions captured and available in the trace | +| `AggregateErrors` | All handlers execute; exceptions aggregated into `AggregateException` | + +## Publish tracing + +To observe publication behavior and capture per-handler outcomes, use `PublishTracedAsync`: + +```csharp +var trace = await aggregator.PublishTracedAsync(new OrderPlacedEvent("ORD-001", 99.99m)); + +foreach (var step in trace.Steps) +{ + Console.WriteLine($"{step.Index}: {step.HandlerName} → {step.Status}"); +} +``` + +`EventPublicationTrace` provides: + +- `Steps` — list of `EventPublicationStep` (Index, HandlerName, Status, Exception?) +- `FailedCount` — number of handlers that threw exceptions + +### Observers + +Implement `IEventPublicationObserver` to receive callbacks for side-effects like logging or metrics: + +```csharp +public class PublicationLogger : IEventPublicationObserver +{ + public void OnPublicationCompleted(OrderPlacedEvent evt, EventPublicationTrace trace) + => Console.WriteLine($"Event {typeof(OrderPlacedEvent).Name}: {trace.HandlerCount} handlers, {trace.FailedCount} failed"); +} ``` ## Sample diff --git a/docs/factory-registry.md b/docs/factory-registry.md index 829baed..6003c94 100644 --- a/docs/factory-registry.md +++ b/docs/factory-registry.md @@ -9,20 +9,60 @@ Map keys to product factories — similar to Strategy but for **creating** insta ## Runtime - `IFactoryRegistry` — extends read-only registry with `Create` +- `IAsyncFactoryRegistry` — async variant with `CreateAsync` +- `IPooledFactoryRegistry` — pooled variant with `RentAsync` / `Return` (backed by `ArrayPool`) - `FactoryRegistryBuilder` — fluent manual registration +- `AsyncFactoryRegistryBuilder` — fluent manual async/pooled registration ## Source generator -`[RegisterFactory(typeof(TProduct), "key")]` on factory types; partial static registry holder. +`[RegisterFactory(typeof(TProduct), "key")]` on factory types; partial static registry holder. Optional `IsAsync = true` for async factories and `PoolSize = N` for pooled factories. ```csharp [RegisterFactory(typeof(IProduct), "standard")] public sealed class StandardProductFactory : IProductFactory { ... } + +[RegisterFactory(typeof(IProduct), "premium", IsAsync = true)] +public sealed class PremiumProductFactory : IAsyncFactory { ... } + +[RegisterFactory(typeof(IProduct), "buffer", PoolSize = 16)] +public sealed class BufferProductFactory : IAsyncFactory { ... } +``` + +When `IsAsync = true` or the factory implements `IAsyncFactory`, the generator emits an `IAsyncFactoryRegistry` in addition to the sync registry. When `PoolSize > 0`, the generator emits an `IPooledFactoryRegistry`. + +## Async factory registry + +For asynchronous product creation, use `IAsyncFactoryRegistry`: + +```csharp +var registry = new AsyncFactoryRegistryBuilder() + .Register("premium", ct => CreatePremiumAsync(ct)) + .Build(); + +var product = await registry.CreateAsync("premium"); +``` + +## Pooled factory registry + +For reusable product instances with per-key object pooling, use `IPooledFactoryRegistry`: + +```csharp +var registry = new AsyncFactoryRegistryBuilder() + .Register("buffer", () => new BufferProduct()) + .WithPooling(poolSize: 16) + .Build(); + +var buffer = await registry.RentAsync("buffer"); +// ... use buffer +registry.Return("buffer", buffer); ``` +If a product implements `IResettable`, `Reset()` is called before returning it to the pool. + ## Diagnostics -DP020–DP023, [DP025](./diagnostics.md#registry-key-dp025) (unknown literal keys). See [Registry key conventions](./registry-key-conventions.md). +DP020–DP023, [DP025](./diagnostics.md#registry-key-dp025) (unknown literal keys). DP053–DP055 for async signature and pool size validation. See [Registry key conventions](./registry-key-conventions.md). ## Samples diff --git a/docs/getting-started.md b/docs/getting-started.md index f9335f9..0f79188 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -10,7 +10,7 @@ The meta package is published as **`Skymly.DesignPatterns`** on [nuget.org](https://www.nuget.org/packages/Skymly.DesignPatterns). C# namespaces remain `DesignPatterns.*`. ```xml - + ``` ::: warning Early preview diff --git a/docs/index.md b/docs/index.md index b652da6..533ffe2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,7 +19,7 @@ features: - title: Source generators details: "Attributes like [RegisterStrategy] and [HandlerOrder] emit keys, registries, and pipelines at compile time." - title: Diagnostics - details: DP001–DP025 catch duplicate keys, contract mismatches, missing registrations, and unknown registry keys before runtime. + details: DP001–DP055 catch duplicate keys, contract mismatches, missing registrations, guard signature errors, and unknown registry keys before runtime. - title: Optional DI details: DesignPatterns.Extensions.DependencyInjection adds RegisterDi helpers when you use Microsoft.Extensions.DependencyInjection. --- @@ -27,7 +27,7 @@ features: ## Status ::: warning Early preview -Public APIs, generated code shapes, and diagnostic IDs are **not stable** yet. Install [`Skymly.DesignPatterns`](https://www.nuget.org/packages/Skymly.DesignPatterns) `0.1.0-preview3` from nuget.org, or use a sibling clone / pin a commit until a stability announcement. +Public APIs, generated code shapes, and diagnostic IDs are **not stable** yet. Install [`Skymly.DesignPatterns`](https://www.nuget.org/packages/Skymly.DesignPatterns) `0.2.0-preview2` from nuget.org, or use a sibling clone / pin a commit until a stability announcement. ::: ## Where to read next diff --git a/docs/reference.md b/docs/reference.md index be7371b..477c664 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -12,11 +12,12 @@ | Package ID | Version (preview) | Contents | |------------|-------------------|----------| -| [`Skymly.DesignPatterns`](https://www.nuget.org/packages/Skymly.DesignPatterns) | `0.1.0-preview3` | Meta package — runtime + source generator + analyzers + code fixes | +| [`Skymly.DesignPatterns`](https://www.nuget.org/packages/Skymly.DesignPatterns) | `0.2.0-preview2` | Meta package — runtime + source generator + analyzers + code fixes | | `DesignPatterns.Extensions.DependencyInjection` | — (not on NuGet yet) | MSDI extensions + `RegisterDi` generation | +| `DesignPatterns.Extensions.Autofac` | — (not on NuGet yet) | Autofac extensions + `RegisterAutofac` generation | ```xml - + ``` ::: info Deprecated GitHub-only IDs @@ -29,7 +30,7 @@ Do not use the old GitHub Packages ID `DesignPatterns` (`0.1.0-preview1` / `prev |---------|------| | `DesignPatterns` | Runtime primitives | | `DesignPatterns.SourceGenerators` | Incremental generators | -| `DesignPatterns.Analyzers` / `DesignPatterns.CodeFixes` | DP006, DP023, DP024, DP025 | +| `DesignPatterns.Analyzers` / `DesignPatterns.CodeFixes` | DP006, DP023, DP024, DP025, DP033, DP036, DP044 | | `DesignPatterns.Diagnostics` | DP### ID constants | | `DesignPatterns.Extensions.DependencyInjection` | MSDI + DI targets | | `DesignPatterns.Package` | NuGet meta package (`PackageId=Skymly.DesignPatterns`) | @@ -37,7 +38,7 @@ Do not use the old GitHub Packages ID `DesignPatterns` (`0.1.0-preview1` / `prev ## Diagnostics -See [Diagnostics](./diagnostics.md) (DP001–DP025). +See [Diagnostics](./diagnostics.md) (DP001–DP055). ## Contributor docs diff --git a/docs/state-transition-table.md b/docs/state-transition-table.md index 447d7ce..e6c0c7f 100644 --- a/docs/state-transition-table.md +++ b/docs/state-transition-table.md @@ -6,7 +6,7 @@ Namespace: `DesignPatterns.Behavioral` Model finite state graphs as **(current state, trigger) → next state**. Use a manual builder or compile-time `[StateMachine]` / `[Transition]` attributes to avoid hand-written `switch` blocks and catch invalid edges at build time. -This is **not** a full UML state-machine framework (no hierarchical states, history, persistence, or entry/exit action DSL). +This is **not** a full UML state-machine framework (no hierarchical states or history). It does support guard delegates, entry/exit actions, and an instance wrapper with `CurrentState` tracking. ## Runtime @@ -44,7 +44,116 @@ public static partial class OrderMachine; ## Diagnostics -DP026–DP031 — duplicate edges, invalid enum members, invalid holder, isolated states. See [Diagnostics](./diagnostics.md#state-transition-table-dp026-dp031). +DP026–DP031 — duplicate edges, invalid enum members, invalid holder, isolated states. DP032/DP034/DP035 for guard method validation. DP036 for literal edge validation. DP037–DP039 for entry/exit action validation. See [Diagnostics](./diagnostics.md#state-transition-table-dp026-dp031). + +## Guard predicates + +Guards are optional predicates that determine whether a transition is allowed at runtime. When a guard returns `false`, the transition is treated as if it does not exist. + +### Runtime API + +```csharp +var table = new TransitionTableBuilder() + .WithInitial(OrderStatus.Draft) + .Add(OrderStatus.Draft, OrderTrigger.Submit, OrderStatus.Submitted, + guard: (state, trigger) => !string.IsNullOrEmpty(orderId)) + .Build(); +``` + +### Source generator + +Use the `Guard` property on `[Transition]` to specify a static method on the holder class: + +```csharp +[StateMachine(typeof(OrderStatus), typeof(OrderTrigger), Initial = OrderStatus.Draft)] +[Transition(OrderStatus.Draft, OrderTrigger.Submit, OrderStatus.Submitted, Guard = nameof(CanSubmit))] +public static partial class OrderMachine +{ + public static bool CanSubmit(OrderStatus state, OrderTrigger trigger) => true; +} +``` + +Guard methods must be `static` with signature `bool Method(TState, TTrigger)`. Diagnostics: DP032, DP034, DP035. + +## Entry/exit actions + +Entry and exit actions are optional side-effect hooks that execute during async transitions. Actions run only via `TryTransitionAsync` (not the synchronous `TryTransition`). + +### Execution order + +For `TryTransitionAsync`: guard → OnExit (sync → async) → OnEnter (sync → async) → return result. + +### Source generator + +Use `OnEnter` and `OnExit` properties on `[Transition]`: + +```csharp +[StateMachine(typeof(OrderStatus), typeof(OrderTrigger), Initial = OrderStatus.Draft)] +[Transition(OrderStatus.Draft, OrderTrigger.Submit, OrderStatus.Submitted, + OnEnter = nameof(OnSubmitted), OnExit = nameof(OnLeaveDraft))] +public static partial class OrderMachine +{ + public static void OnSubmitted(OrderStatus from, OrderStatus to, OrderTrigger trigger) { } + public static void OnLeaveDraft(OrderStatus from, OrderStatus to, OrderTrigger trigger) { } +} +``` + +Action methods must be `static` with signature `void Method(TState from, TState to, TTrigger trigger)` (sync) or `ValueTask Method(TState from, TState to, TTrigger trigger, CancellationToken)` (async). Diagnostics: DP037, DP038, DP039. + +## IStateMachine instance wrapper + +`IStateMachine` is a stateful wrapper around a transition table that automatically tracks `CurrentState` and updates it after each successful transition. + +```csharp +var machine = new StateMachine(table); +// machine.CurrentState == table.InitialState + +if (machine.TryTransition(OrderTrigger.Submit, out var next)) +{ + // machine.CurrentState == OrderStatus.Submitted +} +``` + +> **Thread safety**: `StateMachine` is not thread-safe. Synchronize externally or use a separate instance per thread for multi-threaded scenarios. + +## TransitionTrace + +`TryTransitionTracedAsync` returns a `TransitionTrace` that provides detailed execution progress when entry/exit actions are involved. Unlike `TryTransitionAsync`, action exceptions are caught and recorded in the trace instead of propagating. + +```csharp +var trace = await table.TryTransitionTracedAsync(current, trigger, cancellationToken); + +if (!trace.Succeeded && trace.Exception is not null) +{ + if (trace.OnExitCompleted && !trace.OnEnterCompleted) + { + // OnExit ran, OnEnter failed — compensate + } +} +``` + +The trace includes: `Succeeded`, `NextState`, `OnExitCompleted`, `OnEnterCompleted`, `Exception`. + +## DI integration + +### Generated `RegisterDi` method + +When your project references `DesignPatterns.Extensions.DependencyInjection`, the source generator emits a `RegisterDi` method on the generated state machine class: + +```csharp +OrderStatusStateMachine.RegisterDi(services); +``` + +This registers both the transition table (`ITransitionTable`) and the state machine wrapper (`IStateMachine`). + +### Manual registration extensions + +```csharp +services.AddTransitionTable(OrderStatusTransitionTable.Instance); +services.AddStateMachine(); +``` + +`AddTransitionTable` uses `TryAdd` semantics. Use `ServiceLifetime.Transient` for `AddStateMachine` when each consumer needs its own state tracking. ## Sample diff --git a/docs/strategy.md b/docs/strategy.md index 230a121..cdf6f85 100644 --- a/docs/strategy.md +++ b/docs/strategy.md @@ -45,7 +45,84 @@ await registry.Get(TextProcessorKeys.Length).ExecuteAsync("hello"); ## Diagnostics -DP003–DP007 — duplicate keys, contract mismatch, unregistered types (DP006 + CodeFix), missing ctor. [DP025](./diagnostics.md#registry-key-dp025) for unknown literal keys at lookup sites. See [Registry key conventions](./registry-key-conventions.md). +DP003–DP007 — duplicate keys, contract mismatch, unregistered types (DP006 + CodeFix), missing ctor. [DP025](./diagnostics.md#registry-key-dp025) for unknown literal keys at lookup sites. DP047–DP049 for guard method validation. See [Registry key conventions](./registry-key-conventions.md). + +## Guard predicates + +Conditionally enable or disable strategies at resolution time using guard predicates. + +### Runtime API + +`TryGetWithGuard` evaluates a guard predicate when resolving a strategy. When the guard returns `false`, the strategy is treated as unregistered: + +```csharp +var builder = new StrategyRegistryBuilder() + .Register("alipay", new AlipayPayment(), guard: key => IsPaymentEnabled("alipay")); +var registry = builder.Build(); + +if (registry.TryGetWithGuard("alipay", out var strategy)) +{ + await strategy.ProcessAsync(payment); +} +``` + +**Design constraint**: Guard signatures are `Func` (key only), not `Func`. The registry layer does not know `TInput` (strategies are not required to implement `IStrategy`), so input-based dynamic routing is business logic outside this library's scope. + +### Source generator + +Use the `Guard` property on `[RegisterStrategy]` to reference a static guard method: + +```csharp +[RegisterStrategy("alipay", Guard = nameof(IsEnabled))] +public sealed class AlipayPayment : IPaymentStrategy +{ + private static bool IsEnabled(string key) => IsPaymentEnabled(key); +} +``` + +The generator validates the guard method signature (DP047–DP049). + +## Execution tracing + +Trace strategy execution outcomes for debugging, logging, or metrics without changing your strategy implementations. + +`ExecuteTracedAsync` extension methods return a `StrategyExecutionTrace` containing: + +- `Key` — The strategy key requested +- `Status` — Execution outcome (see below) +- `Output` — Strategy output when successful +- `Exception` — Exception when failed +- `ElapsedMilliseconds` — Resolution and execution time + +```csharp +var trace = await registry.ExecuteTracedAsync("double", 5); +``` + +### Status values + +| `StrategyExecutionStepStatus` | Meaning | +|------------------------------|---------| +| **Executed** | Strategy resolved and executed successfully | +| **KeyNotFound** | Strategy key not found in registry | +| **GuardRejected** | Strategy found but guard predicate returned `false` | +| **Failed** | Strategy resolved but threw an exception during execution | + +### Observers + +Implement `IStrategyExecutionObserver` to receive callbacks for side-effects like logging or metrics: + +```csharp +public sealed class LoggingObserver : IStrategyExecutionObserver +{ + public void OnExecutionCompleted(string key, TInput input, TOutput output, long elapsedMs) + => Console.WriteLine($"Strategy {key} succeeded in {elapsedMs}ms"); + + public void OnExecutionFailed(string key, TInput input, StrategyExecutionTrace trace) + => Console.WriteLine($"Strategy {key} failed: {trace.Exception?.Message}"); +} + +var trace = await registry.ExecuteTracedAsync("double", 5, new LoggingObserver()); +``` ## Sample diff --git a/docs/zh/diagnostics.md b/docs/zh/diagnostics.md index d4b5a41..9c76b4b 100644 --- a/docs/zh/diagnostics.md +++ b/docs/zh/diagnostics.md @@ -97,6 +97,74 @@ IDE 帮助链接指向本页(`#dp###` 锚点)。 见 [State 转换表](./state-transition-table.md)。 +## State guard(DP032、DP034–DP035) {#dp032} + +| ID | 级别 | 触发条件 | +|----|------|----------| +| **DP032** | Error | `[Transition(Guard = nameof(Method))]` guard 方法在 holder 类上未找到 | +| **DP034** | Error | guard 方法非 `static`(防御性 — C# 编译器先于生成器拒绝 static 类的实例方法) | +| **DP035** | Error | guard 方法签名错误(须为 `static bool Method(TState, TTrigger)`) | + +## State 字面量边(DP036) {#dp036} + +| ID | 级别 | 触发条件 | +|----|------|----------| +| **DP036** | Info | `TryTransition` 以字面量 `(state, trigger)` 调用,但未匹配任何 `[Transition]` 声明的边 | + +## State entry/exit action(DP037–DP039) {#dp037} + +| ID | 级别 | 触发条件 | +|----|------|----------| +| **DP037** | Error | `[Transition(OnEnter/OnExit = nameof(Method))]` action 方法在 holder 类上未找到 | +| **DP038** | Error | action 方法非 `static`(实际不可达 — CS0708 先触发;保留供完整性) | +| **DP039** | Error | action 方法签名错误(须为 `static void Method(TState, TState, TTrigger)` 或 `static ValueTask Method(TState, TState, TTrigger, CancellationToken)`) | + +## Composite DI + Visitor(DP040–DP041) {#dp040} + +| ID | 级别 | 触发条件 | +|----|------|----------| +| **DP040** | Error | `BuildRoot(IServiceProvider)` 调用时 composite 节点未注册到容器 | +| **DP041** | Error | 生成的 `I{Contract}NodeVisitor` 未覆盖所有节点类型(visitor 缺少 `Visit` 重载) | + +## Decorator DI + async(DP042–DP043) {#dp042} + +| ID | 级别 | 触发条件 | +|----|------|----------| +| **DP042** | Error | async decorator 方法签名错误(`DecorateAsync` 须返回 `ValueTask`) | +| **DP043** | Error | async decorator 在 `Build(IServiceProvider, core)` 时无法从 DI 解析 | + +## EventAggregator(DP044–DP046) {#dp044} + +| ID | 级别 | 触发条件 | +|----|------|----------| +| **DP044** | Info | 类型实现 `IEventHandler` 但未标注 `[RegisterEventHandler]` | +| **DP045** | Error | 同一 handler 对同一 event type 重复标注 `[RegisterEventHandler]` | +| **DP046** | Error | 类型标注了 `[RegisterEventHandler]` 但未实现 `IEventHandler` | + +## Strategy guard(DP047–DP049) {#dp047} + +| ID | 级别 | 触发条件 | +|----|------|----------| +| **DP047** | Error | `[RegisterStrategy(Guard = nameof(Method))]` guard 方法在策略类上未找到 | +| **DP048** | Error | guard 方法非 `static` | +| **DP049** | Error | guard 方法签名错误(须为 `static bool Method(TKey)`) | + +## Handler guard(DP050–DP052) {#dp050} + +| ID | 级别 | 触发条件 | +|----|------|----------| +| **DP050** | Error | `[HandlerOrder(Guard = nameof(Method))]` guard 方法在 handler 类上未找到 | +| **DP051** | Error | guard 方法非 `static` | +| **DP052** | Error | guard 方法签名错误(须为 `static bool Method(TContext)`) | + +## Factory async + 池化(DP053–DP055) {#dp053} + +| ID | 级别 | 触发条件 | +|----|------|----------| +| **DP053** | Error | `[RegisterFactory(IsAsync = true)]` 但工厂未实现 `IAsyncFactory` | +| **DP054** | Error | `PoolSize` 为负数(须 ≥ 0;0 禁用池化) | +| **DP055** | Warning | `PoolSize` > 1024(可能导致内存占用过高) | + ## CodeFix `DesignPatterns.CodeFixes` 随 **`Skymly.DesignPatterns`** 元包分发(`analyzers/dotnet/cs`)。下列诊断支持 IDE 一键修复(需 C# Workspaces): diff --git a/docs/zh/getting-started.md b/docs/zh/getting-started.md index 73a30e6..42a986e 100644 --- a/docs/zh/getting-started.md +++ b/docs/zh/getting-started.md @@ -10,7 +10,7 @@ 元包在 [nuget.org](https://www.nuget.org/packages/Skymly.DesignPatterns) 上的 ID 为 **`Skymly.DesignPatterns`**。C# 命名空间仍为 `DesignPatterns.*`。 ```xml - + ``` ::: warning 早期预览 diff --git a/docs/zh/index.md b/docs/zh/index.md index 9d69968..6be38de 100644 --- a/docs/zh/index.md +++ b/docs/zh/index.md @@ -19,7 +19,7 @@ features: - title: 源生成器 details: "[RegisterStrategy]、[HandlerOrder] 等在编译期生成 Key、注册表与管道。" - title: 诊断 - details: DP001–DP025 在编译期发现重复 Key、契约不匹配、未注册与未知键等问题。 + details: DP001–DP055 在编译期发现重复 Key、契约不匹配、未注册、guard 签名错误与未知键等问题。 - title: 可选 DI details: DesignPatterns.Extensions.DependencyInjection 提供 RegisterDi 与 MSDI 扩展。 --- @@ -27,7 +27,7 @@ features: ## 项目状态 ::: warning 早期预览 -公共 API、生成代码形态与诊断 ID **尚未稳定**。请从 nuget.org 安装 [`Skymly.DesignPatterns`](https://www.nuget.org/packages/Skymly.DesignPatterns) `0.1.0-preview3`,或使用 sibling 克隆 / 固定 commit,直至稳定公告。 +公共 API、生成代码形态与诊断 ID **尚未稳定**。请从 nuget.org 安装 [`Skymly.DesignPatterns`](https://www.nuget.org/packages/Skymly.DesignPatterns) `0.2.0-preview2`,或使用 sibling 克隆 / 固定 commit,直至稳定公告。 ::: ## 下一步 diff --git a/docs/zh/reference.md b/docs/zh/reference.md index 4047fca..b72aafc 100644 --- a/docs/zh/reference.md +++ b/docs/zh/reference.md @@ -12,11 +12,12 @@ | 包 ID | 预览版本 | 内容 | |-------|----------|------| -| [`Skymly.DesignPatterns`](https://www.nuget.org/packages/Skymly.DesignPatterns) | `0.1.0-preview3` | 元包 — 运行时 + 源生成器 + Analyzer + CodeFix | +| [`Skymly.DesignPatterns`](https://www.nuget.org/packages/Skymly.DesignPatterns) | `0.2.0-preview2` | 元包 — 运行时 + 源生成器 + Analyzer + CodeFix | | `DesignPatterns.Extensions.DependencyInjection` | —(尚未发 NuGet) | MSDI + `RegisterDi` 生成 | +| `DesignPatterns.Extensions.Autofac` | —(尚未发 NuGet) | Autofac + `RegisterAutofac` 生成 | ```xml - + ``` ::: info 已弃用的 GitHub 包 ID @@ -25,7 +26,7 @@ ## 诊断 -见 [诊断](./diagnostics.md)(DP001–DP025)。 +见 [诊断](./diagnostics.md)(DP001–DP055)。 ## 维护者文档