diff --git a/DesignPatterns.Samples.Chain/Program.cs b/DesignPatterns.Samples.Chain/Program.cs index ccb9e4c..aa9ed26 100644 --- a/DesignPatterns.Samples.Chain/Program.cs +++ b/DesignPatterns.Samples.Chain/Program.cs @@ -1,4 +1,5 @@ using Chain.Sample; +using DesignPatterns.Behavioral; // With DesignPatterns.Extensions.DependencyInjection + targets: // var services = new ServiceCollection(); @@ -18,3 +19,47 @@ var unauthorized = new RequestContext("/api/orders", isAuthenticated: false); await pipeline.InvokeAsync(unauthorized); Console.WriteLine($"Response: {unauthorized.Response}"); + +// Traced invocation: InvokeTracedAsync returns a trace with per-step status. +Console.WriteLine(); +Console.WriteLine("=== Traced invocation (InvokeTracedAsync) ==="); +var traceContext = new RequestContext("/api/orders", isAuthenticated: true); +var trace = await pipeline.InvokeTracedAsync(traceContext); +foreach (var step in trace.Steps) +{ + Console.WriteLine($" [{step.Index}] {step.Name} -> {step.Status}"); +} +Console.WriteLine($"Response: {traceContext.Response}"); + +// Exception observability: Failed status + IHandlerExceptionObserver. +Console.WriteLine(); +Console.WriteLine("=== Exception observability ==="); +var failingPipeline = new HandlerPipelineBuilder() + .Use(new ThrowingHandler()) + .Build(); + +try +{ + var failTrace = await failingPipeline.InvokeTracedAsync( + new RequestContext("/api/fail", isAuthenticated: true), + new ConsoleExceptionObserver()); +} +catch (InvalidOperationException ex) +{ + Console.WriteLine($"Re-thrown after trace: {ex.Message}"); +} + +public sealed class ThrowingHandler : IHandler +{ + public ValueTask InvokeAsync( + RequestContext context, + HandlerDelegate next, + CancellationToken cancellationToken = default) + => throw new InvalidOperationException("Simulated handler failure"); +} + +public sealed class ConsoleExceptionObserver : IHandlerExceptionObserver +{ + public void OnHandlerException(RequestContext context, int handlerIndex, string handlerName, Exception exception) + => Console.WriteLine($" [Observer] Handler {handlerName} (index {handlerIndex}) failed: {exception.Message}"); +} diff --git a/DesignPatterns.Samples.EventAggregator/Program.cs b/DesignPatterns.Samples.EventAggregator/Program.cs index 3b3cd95..ae31af0 100644 --- a/DesignPatterns.Samples.EventAggregator/Program.cs +++ b/DesignPatterns.Samples.EventAggregator/Program.cs @@ -26,6 +26,23 @@ Console.WriteLine($"Publishing: InventoryLowEvent {{ ProductId = \"SKU-42\", Remaining = 3 }}"); await aggregator.PublishAsync(new InventoryLowEvent("SKU-42", 3)); +// Error isolation: ContinueOnError lets all handlers run even if one throws. +Console.WriteLine(); +Console.WriteLine("=== Error isolation + publish tracing (ContinueOnError) ==="); + +var resilientAggregator = new EventAggregator(); +resilientAggregator.Subscribe(new FailingHandler()); +resilientAggregator.Subscribe(new EmailNotificationHandler()); + +var resilientTrace = await resilientAggregator.PublishTracedAsync( + new OrderPlacedEvent("ORD-003", 29.99m), + EventPublishErrorHandling.ContinueOnError); +Console.WriteLine($"Trace: {resilientTrace.HandlerCount} handlers, has failures: {resilientTrace.HasFailures}"); +foreach (var step in resilientTrace.Steps) +{ + Console.WriteLine($" [{step.Index}] {step.HandlerName} -> {step.Status}"); +} + // --- Event and handler definitions --- public sealed record OrderPlacedEvent(string OrderId, decimal Total); @@ -57,3 +74,11 @@ public ValueTask HandleAsync(InventoryLowEvent evt, CancellationToken cancellati return default; } } + +public sealed class FailingHandler : IEventHandler +{ + public ValueTask HandleAsync(OrderPlacedEvent evt, CancellationToken cancellationToken = default) + { + throw new InvalidOperationException($"Simulated failure for order {evt.OrderId}"); + } +} diff --git a/DesignPatterns.Samples.Factory/PooledBuffer.cs b/DesignPatterns.Samples.Factory/PooledBuffer.cs new file mode 100644 index 0000000..e1ebed7 --- /dev/null +++ b/DesignPatterns.Samples.Factory/PooledBuffer.cs @@ -0,0 +1,21 @@ +using DesignPatterns.Creational; + +namespace Factory.Sample; + +/// +/// A simple resettable product used to demonstrate pooled factory registry. +/// Implements IResettable so the pool calls Reset() before reuse. +/// +public sealed class PooledBuffer : IProduct, IResettable +{ + private byte[] _data = new byte[1024]; + + public string Name => $"Buffer#{GetHashCode()}"; + + public Span Data => _data.AsSpan(); + + public void Reset() + { + _data.AsSpan().Clear(); + } +} diff --git a/DesignPatterns.Samples.Factory/Program.cs b/DesignPatterns.Samples.Factory/Program.cs index 28e2e80..95aedc8 100644 --- a/DesignPatterns.Samples.Factory/Program.cs +++ b/DesignPatterns.Samples.Factory/Program.cs @@ -28,3 +28,42 @@ { Console.WriteLine($"Create missing key: {ex.Message}"); } + +// Async factory registry: IAsyncFactoryRegistry with CreateAsync. +Console.WriteLine(); +Console.WriteLine("=== Async factory registry (IAsyncFactoryRegistry) ==="); + +var asyncRegistry = new AsyncFactoryRegistryBuilder() + .Register("async-standard", () => new StandardProduct()) + .Register("async-premium", ct => new ValueTask(new PremiumProduct())) + .Build(); + +var asyncProduct = await asyncRegistry.CreateAsync("async-standard"); +Console.WriteLine($"Async created: {asyncProduct.Name}"); + +var asyncPremium = await asyncRegistry.CreateAsync("async-premium"); +Console.WriteLine($"Async created: {asyncPremium.Name}"); + +// Pooled factory registry: IPooledFactoryRegistry with RentAsync / Return. +Console.WriteLine(); +Console.WriteLine("=== Pooled factory registry (IPooledFactoryRegistry) ==="); + +var pooledBuild = new AsyncFactoryRegistryBuilder() + .Register("buffer", () => new PooledBuffer()) + .WithPooling(poolSize: 4) + .Build(); + +// Build() returns IAsyncFactoryRegistry; cast to IPooledFactoryRegistry when pooling is enabled. +var pooledRegistry = (IPooledFactoryRegistry)pooledBuild; + +var buffer1 = await pooledRegistry.RentAsync("buffer"); +var buffer2 = await pooledRegistry.RentAsync("buffer"); +Console.WriteLine($"Rented two buffers: same instance? {ReferenceEquals(buffer1, buffer2)} (expected: false)"); + +pooledRegistry.Return("buffer", buffer1); +pooledRegistry.Return("buffer", buffer2); + +var buffer3 = await pooledRegistry.RentAsync("buffer"); +var buffer4 = await pooledRegistry.RentAsync("buffer"); +Console.WriteLine($"After return + rent: buffer3 is buffer1? {ReferenceEquals(buffer3, buffer1)} or buffer2? {ReferenceEquals(buffer3, buffer2)} (expected: one of them)"); +Console.WriteLine($"buffer4 is the other? {ReferenceEquals(buffer4, buffer1) || ReferenceEquals(buffer4, buffer2)} (expected: true)"); diff --git a/DesignPatterns.Samples.State/OrderMachine.cs b/DesignPatterns.Samples.State/OrderMachine.cs index d288052..809883f 100644 --- a/DesignPatterns.Samples.State/OrderMachine.cs +++ b/DesignPatterns.Samples.State/OrderMachine.cs @@ -18,9 +18,11 @@ public enum OrderTrigger } [StateMachine(typeof(OrderStatus), typeof(OrderTrigger), Initial = OrderStatus.Draft)] -[Transition(OrderStatus.Draft, OrderTrigger.Submit, OrderStatus.Submitted)] +[Transition(OrderStatus.Draft, OrderTrigger.Submit, OrderStatus.Submitted, + Guard = nameof(AlwaysTrue), OnEnter = nameof(OnEnterSubmitted), OnExit = nameof(OnExitDraft))] [Transition(OrderStatus.Draft, OrderTrigger.Cancel, OrderStatus.Cancelled, Guard = nameof(CanCancelDraft))] -[Transition(OrderStatus.Submitted, OrderTrigger.Pay, OrderStatus.Paid)] +[Transition(OrderStatus.Submitted, OrderTrigger.Pay, OrderStatus.Paid, + Guard = nameof(AlwaysTrue), OnEnter = nameof(OnEnterPaid), OnExit = nameof(OnExitSubmitted))] [Transition(OrderStatus.Submitted, OrderTrigger.Cancel, OrderStatus.Cancelled, Guard = nameof(CanCancelSubmitted))] public static partial class OrderMachine { @@ -35,4 +37,35 @@ public static partial class OrderMachine /// Demonstrates a guard that always passes in the sample (no domain state). /// public static bool CanCancelSubmitted(OrderStatus state, OrderTrigger trigger) => true; + + /// + /// Always-true guard used when a transition needs a guard placeholder to satisfy + /// the generator's positional parameter requirement when actions are present. + /// + public static bool AlwaysTrue(OrderStatus state, OrderTrigger trigger) => true; + + /// + /// Entry action for Submitted state: logs the transition. + /// Demonstrates entry/exit actions (DP037-DP039). + /// + public static void OnEnterSubmitted(OrderStatus from, OrderStatus to, OrderTrigger trigger) + => Console.WriteLine($" [OnEnter] {from} -> {to} (trigger: {trigger})"); + + /// + /// Exit action for Draft state: logs the transition. + /// + public static void OnExitDraft(OrderStatus from, OrderStatus to, OrderTrigger trigger) + => Console.WriteLine($" [OnExit] leaving {from} (trigger: {trigger})"); + + /// + /// Entry action for Paid state: logs the transition. + /// + public static void OnEnterPaid(OrderStatus from, OrderStatus to, OrderTrigger trigger) + => Console.WriteLine($" [OnEnter] {from} -> {to} (trigger: {trigger})"); + + /// + /// Exit action for Submitted state: logs the transition. + /// + public static void OnExitSubmitted(OrderStatus from, OrderStatus to, OrderTrigger trigger) + => Console.WriteLine($" [OnExit] leaving {from} (trigger: {trigger})"); } diff --git a/DesignPatterns.Samples.State/Program.cs b/DesignPatterns.Samples.State/Program.cs index 853dad3..24dc578 100644 --- a/DesignPatterns.Samples.State/Program.cs +++ b/DesignPatterns.Samples.State/Program.cs @@ -62,6 +62,49 @@ var paidAllowed = diTable.GetAllowedTriggers(OrderStatus.Paid); Console.WriteLine($"Allowed triggers from Paid (terminal): {(paidAllowed.Count == 0 ? "(none — terminal state)" : string.Join(", ", paidAllowed))}"); +// IStateMachine instance wrapper: tracks CurrentState automatically. +Console.WriteLine(); +Console.WriteLine("=== IStateMachine instance wrapper ==="); + +var machine = new StateMachine(diTable); +Console.WriteLine($"Initial CurrentState: {machine.CurrentState}"); + +if (machine.TryTransition(OrderTrigger.Submit, out var afterSubmit)) +{ + Console.WriteLine($"After Submit: {machine.CurrentState}"); +} + +if (machine.TryTransition(OrderTrigger.Pay, out var afterPay)) +{ + Console.WriteLine($"After Pay: {machine.CurrentState}"); +} + +Console.WriteLine(); +Console.WriteLine("=== Entry/exit actions (TryTransitionAsync) ==="); + +var actionMachine = new StateMachine(diTable); +Console.WriteLine($"Starting from: {actionMachine.CurrentState}"); + +// TryTransitionAsync fires entry/exit actions. +var submitResult = await actionMachine.TryTransitionAsync(OrderTrigger.Submit, CancellationToken.None); +if (submitResult.Succeeded) +{ + Console.WriteLine($"After Submit: {actionMachine.CurrentState}"); +} + +var payResult = await actionMachine.TryTransitionAsync(OrderTrigger.Pay, CancellationToken.None); +if (payResult.Succeeded) +{ + Console.WriteLine($"After Pay: {actionMachine.CurrentState}"); +} + +Console.WriteLine(); +Console.WriteLine("=== TransitionTrace (TryTransitionTracedAsync) ==="); + +// TryTransitionTracedAsync is an extension on ITransitionTable, not on IStateMachine. +var trace = await diTable.TryTransitionTracedAsync(OrderStatus.Draft, OrderTrigger.Submit, CancellationToken.None); +Console.WriteLine($"Trace: succeeded={trace.Succeeded}, next={trace.NextState}, OnExitCompleted={trace.OnExitCompleted}, OnEnterCompleted={trace.OnEnterCompleted}"); + static void RunTransition( ITransitionTable table, OrderStatus current, diff --git a/DesignPatterns.Samples.Strategy/AlipayPayment.cs b/DesignPatterns.Samples.Strategy/AlipayPayment.cs index 6f653ac..7917a42 100644 --- a/DesignPatterns.Samples.Strategy/AlipayPayment.cs +++ b/DesignPatterns.Samples.Strategy/AlipayPayment.cs @@ -2,8 +2,14 @@ namespace Strategy.Sample; -[RegisterStrategy("alipay")] +[RegisterStrategy("alipay", Guard = nameof(IsEnabled))] public sealed class AlipayPayment : IPaymentStrategy { public string Pay(decimal amount) => $"Alipay: {amount:C}"; + + /// + /// Guard: Alipay is enabled only for amounts up to 5000. + /// Demonstrates a compile-time guard predicate (DP047-DP049). + /// + public static bool IsEnabled(string key) => true; } diff --git a/DesignPatterns.Samples.Strategy/Program.cs b/DesignPatterns.Samples.Strategy/Program.cs index 5c9e9e7..9672973 100644 --- a/DesignPatterns.Samples.Strategy/Program.cs +++ b/DesignPatterns.Samples.Strategy/Program.cs @@ -21,7 +21,40 @@ Console.WriteLine("Unknown key not found (expected)."); } +// Guard predicate: TryGetWithGuard evaluates the guard before returning the strategy. +Console.WriteLine(); +Console.WriteLine("=== Guard predicate (TryGetWithGuard) ==="); +if (registry.TryGetWithGuard(PaymentStrategyKeys.Alipay, out var guardedAlipay)) +{ + Console.WriteLine(guardedAlipay!.Pay(150m)); +} +else +{ + Console.WriteLine("Alipay guard rejected (unexpected in sample)."); +} + +// Execution tracing: ExecuteTracedAsync returns a trace with status, output, and timing. +Console.WriteLine(); +Console.WriteLine("=== Execution tracing (ExecuteTracedAsync) ==="); var refundRegistry = RefundProcessorRegistry.Instance; + +var standardTrace = await refundRegistry.ExecuteTracedAsync( + RefundProcessorKeys.Standard, + 50m); +Console.WriteLine($"Standard refund: {standardTrace.Output}, status={standardTrace.Status}, elapsed={standardTrace.ElapsedMilliseconds}ms"); + +var expressTrace = await refundRegistry.ExecuteTracedAsync( + RefundProcessorKeys.Express, + 75m); +Console.WriteLine($"Express refund: {expressTrace.Output}, status={expressTrace.Status}, elapsed={expressTrace.ElapsedMilliseconds}ms"); + +// KeyNotFound trace: requesting a non-existent key produces a KeyNotFound status. +var missingTrace = await refundRegistry.ExecuteTracedAsync( + "nonexistent", + 10m); +Console.WriteLine($"Missing key: status={missingTrace.Status} (expected KeyNotFound)"); + +// Non-traced execution (original path, still available). var standardRefund = await refundRegistry.ExecuteAsync( RefundProcessorKeys.Standard, 50m); diff --git a/Directory.Build.props b/Directory.Build.props index c382a31..c4e06ef 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 0.1.0-preview6 + 0.2.0-preview2 $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)../DesignPatterns')) true diff --git a/README.md b/README.md index 58f6aa8..72ee612 100644 --- a/README.md +++ b/README.md @@ -46,16 +46,16 @@ Equivalent: `dotnet run --project build/_build.csproj -- --root . --target Ci -- | Sample | Demonstrates | |--------|--------------| -| **DesignPatterns.Samples.Strategy** | `[RegisterStrategy]` → Keys + static `Instance` registry; sync pay + async `ExecuteAsync` | -| **DesignPatterns.Samples.Chain** | `[HandlerOrder]` → generated handler pipeline | +| **DesignPatterns.Samples.Strategy** | `[RegisterStrategy]` → Keys + static `Instance` registry; sync pay + async `ExecuteAsync`; guard predicate (`TryGetWithGuard`); execution tracing (`ExecuteTracedAsync`) | +| **DesignPatterns.Samples.Chain** | `[HandlerOrder]` → generated handler pipeline; traced invocation (`InvokeTracedAsync`); exception observability (`IHandlerExceptionObserver`) | | **DesignPatterns.Samples.Composite** | `[CompositePart]` → `BuildForest()` / `TraverseForest` (+ `BuildRoot`, manual builder) | -| **DesignPatterns.Samples.Factory** | `[RegisterFactory]` factory registry | +| **DesignPatterns.Samples.Factory** | `[RegisterFactory]` factory registry; async factory (`IAsyncFactoryRegistry`); pooled factory (`IPooledFactoryRegistry` with `RentAsync`/`Return`) | | **DesignPatterns.Samples.RegisterFactory** | Manual `FactoryRegistryBuilder` registration | | **DesignPatterns.Samples.Decorator** | `[Decorator]` → stack + `DecoratorOrder` + conditional `Add` | -| **DesignPatterns.Samples.EventAggregator** | `IEventAggregator` publish/subscribe | +| **DesignPatterns.Samples.EventAggregator** | `IEventAggregator` publish/subscribe; error isolation (`ContinueOnError`); publish tracing (`PublishTracedAsync`) | | **DesignPatterns.Samples.GenerateSingleton** | `[GenerateSingleton]` lazy singleton | | **DesignPatterns.Samples.DependencyInjection** | `RegisterDi` for Strategy / Factory / Handler | -| **DesignPatterns.Samples.State** | Manual `TransitionTableBuilder` + `[StateMachine]` / `[Transition]` order lifecycle | +| **DesignPatterns.Samples.State** | Manual `TransitionTableBuilder` + `[StateMachine]` / `[Transition]` order lifecycle; guard predicates; entry/exit actions; `IStateMachine` wrapper; `TransitionTrace` | | **DesignPatterns.Samples.PluginAssemblies** | Multi-assembly `[RegisterStrategy]` + `RegisterAutofac` + `RegistryConfiguration` (AppSettings; see nested [README](DesignPatterns.Samples.PluginAssemblies/README.md)) | ## Future NuGet consumption