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
45 changes: 45 additions & 0 deletions DesignPatterns.Samples.Chain/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Chain.Sample;
using DesignPatterns.Behavioral;

// With DesignPatterns.Extensions.DependencyInjection + targets:
// var services = new ServiceCollection();
Expand All @@ -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<RequestContext>()
.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<RequestContext>
{
public ValueTask InvokeAsync(
RequestContext context,
HandlerDelegate<RequestContext> next,
CancellationToken cancellationToken = default)
=> throw new InvalidOperationException("Simulated handler failure");
}

public sealed class ConsoleExceptionObserver : IHandlerExceptionObserver<RequestContext>
{
public void OnHandlerException(RequestContext context, int handlerIndex, string handlerName, Exception exception)
=> Console.WriteLine($" [Observer] Handler {handlerName} (index {handlerIndex}) failed: {exception.Message}");
}
25 changes: 25 additions & 0 deletions DesignPatterns.Samples.EventAggregator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -57,3 +74,11 @@ public ValueTask HandleAsync(InventoryLowEvent evt, CancellationToken cancellati
return default;
}
}

public sealed class FailingHandler : IEventHandler<OrderPlacedEvent>
{
public ValueTask HandleAsync(OrderPlacedEvent evt, CancellationToken cancellationToken = default)
{
throw new InvalidOperationException($"Simulated failure for order {evt.OrderId}");
}
}
21 changes: 21 additions & 0 deletions DesignPatterns.Samples.Factory/PooledBuffer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using DesignPatterns.Creational;

namespace Factory.Sample;

/// <summary>
/// A simple resettable product used to demonstrate pooled factory registry.
/// Implements IResettable so the pool calls Reset() before reuse.
/// </summary>
public sealed class PooledBuffer : IProduct, IResettable
{
private byte[] _data = new byte[1024];

public string Name => $"Buffer#{GetHashCode()}";

public Span<byte> Data => _data.AsSpan();

public void Reset()
{
_data.AsSpan().Clear();
}
}
39 changes: 39 additions & 0 deletions DesignPatterns.Samples.Factory/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, IProduct>()
.Register("async-standard", () => new StandardProduct())
.Register("async-premium", ct => new ValueTask<IProduct>(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<string, PooledBuffer>()
.Register("buffer", () => new PooledBuffer())
.WithPooling(poolSize: 4)
.Build();

// Build() returns IAsyncFactoryRegistry; cast to IPooledFactoryRegistry when pooling is enabled.
var pooledRegistry = (IPooledFactoryRegistry<string, PooledBuffer>)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)");
37 changes: 35 additions & 2 deletions DesignPatterns.Samples.State/OrderMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -35,4 +37,35 @@ public static partial class OrderMachine
/// Demonstrates a guard that always passes in the sample (no domain state).
/// </summary>
public static bool CanCancelSubmitted(OrderStatus state, OrderTrigger trigger) => true;

/// <summary>
/// Always-true guard used when a transition needs a guard placeholder to satisfy
/// the generator's positional parameter requirement when actions are present.
/// </summary>
public static bool AlwaysTrue(OrderStatus state, OrderTrigger trigger) => true;

/// <summary>
/// Entry action for Submitted state: logs the transition.
/// Demonstrates entry/exit actions (DP037-DP039).
/// </summary>
public static void OnEnterSubmitted(OrderStatus from, OrderStatus to, OrderTrigger trigger)
=> Console.WriteLine($" [OnEnter] {from} -> {to} (trigger: {trigger})");

/// <summary>
/// Exit action for Draft state: logs the transition.
/// </summary>
public static void OnExitDraft(OrderStatus from, OrderStatus to, OrderTrigger trigger)
=> Console.WriteLine($" [OnExit] leaving {from} (trigger: {trigger})");

/// <summary>
/// Entry action for Paid state: logs the transition.
/// </summary>
public static void OnEnterPaid(OrderStatus from, OrderStatus to, OrderTrigger trigger)
=> Console.WriteLine($" [OnEnter] {from} -> {to} (trigger: {trigger})");

/// <summary>
/// Exit action for Submitted state: logs the transition.
/// </summary>
public static void OnExitSubmitted(OrderStatus from, OrderStatus to, OrderTrigger trigger)
=> Console.WriteLine($" [OnExit] leaving {from} (trigger: {trigger})");
}
43 changes: 43 additions & 0 deletions DesignPatterns.Samples.State/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<OrderStatus, OrderTrigger>(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<OrderStatus, OrderTrigger>(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<OrderStatus, OrderTrigger> table,
OrderStatus current,
Expand Down
8 changes: 7 additions & 1 deletion DesignPatterns.Samples.Strategy/AlipayPayment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@

namespace Strategy.Sample;

[RegisterStrategy<IPaymentStrategy>("alipay")]
[RegisterStrategy<IPaymentStrategy>("alipay", Guard = nameof(IsEnabled))]
public sealed class AlipayPayment : IPaymentStrategy
{
public string Pay(decimal amount) => $"Alipay: {amount:C}";

/// <summary>
/// Guard: Alipay is enabled only for amounts up to 5000.
/// Demonstrates a compile-time guard predicate (DP047-DP049).
/// </summary>
public static bool IsEnabled(string key) => true;
}
33 changes: 33 additions & 0 deletions DesignPatterns.Samples.Strategy/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IRefundProcessor, string, decimal>(
RefundProcessorKeys.Standard,
50m);
Console.WriteLine($"Standard refund: {standardTrace.Output}, status={standardTrace.Status}, elapsed={standardTrace.ElapsedMilliseconds}ms");

var expressTrace = await refundRegistry.ExecuteTracedAsync<IRefundProcessor, string, decimal>(
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<IRefundProcessor, string, decimal>(
"nonexistent",
10m);
Console.WriteLine($"Missing key: status={missingTrace.Status} (expected KeyNotFound)");

// Non-traced execution (original path, still available).
var standardRefund = await refundRegistry.ExecuteAsync<IRefundProcessor, string, decimal>(
RefundProcessorKeys.Standard,
50m);
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<!-- Placeholder until DesignPatterns is published to nuget.org -->
<DesignPatternsPackageVersion>0.1.0-preview6</DesignPatternsPackageVersion>
<DesignPatternsPackageVersion>0.2.0-preview2</DesignPatternsPackageVersion>

<DesignPatternsRepoRoot>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)../DesignPatterns'))</DesignPatternsRepoRoot>
<DesignPatternsLocalAvailable Condition="Exists('$(DesignPatternsRepoRoot)/DesignPatterns.slnx')">true</DesignPatternsLocalAvailable>
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading