diff --git a/docs/examples/production-ready-integrations.md b/docs/examples/production-ready-integrations.md index 9a1a929d..acfc6342 100644 --- a/docs/examples/production-ready-integrations.md +++ b/docs/examples/production-ready-integrations.md @@ -18,6 +18,35 @@ using var provider = services.BuildServiceProvider(validateScopes: true); var catalog = provider.GetRequiredService(); ``` +## Register runnable examples + +The examples package also exposes a fluent IoC surface for every catalog entry. Existing applications can import the complete example suite with one call: + +```csharp +using Microsoft.Extensions.DependencyInjection; +using PatternKit.Examples.DependencyInjection; + +var services = new ServiceCollection() + .AddLogging() + .AddPatternKitExamples(); + +using var provider = services.BuildServiceProvider(validateScopes: true); + +var pricing = provider.GetRequiredService(); +var catalog = provider.GetRequiredService(); +``` + +Each example also has its own focused extension, so sample applications can import only the slice they need: + +```csharp +services + .AddPricingCalculatorExample() + .AddPaymentProcessorDecoratorExample() + .AddMessagingBackplaneFacadeExample(); +``` + +Every extension registers a concrete typed entry point, such as `PricingCalculatorExample`, `EnterpriseFeatureSlicesExample`, `MinimalWebRequestRouterExample`, or `ReactiveTransactionExample`. The companion `PatternKitExampleServiceDescriptor` registrations make the IoC surface auditable: tests compare those descriptors against the production-readiness catalog and resolve every registered example through `IServiceProvider`. + Each `PatternKitExampleDescriptor` includes: | Field | Purpose | @@ -84,6 +113,7 @@ Every documented example should satisfy the same baseline contract: | Documented in DocFX | `docs/examples/toc.yml` links to the page and documentation tests validate hrefs. | | Covered by TinyBDD scenarios | The catalog points to a test file and catalog tests verify it exists. | | Importable from the examples package | The catalog is part of `PatternKit.Examples` and is registered through `IServiceCollection`/`IHostApplicationBuilder`. | +| Has an IoC entry point | `AddPatternKitExamples()` and per-example `Add...Example()` extensions register typed example services through `IServiceCollection`. | | Host/tooling friendly where applicable | Integration flags identify examples that demonstrate `IOptions`, DI, generic host, ASP.NET Core, source generators, messaging, or external infrastructure. | | Production-shaped behavior called out | `ProductionChecks` records the behaviors each example must keep validating. | diff --git a/src/PatternKit.Examples/DependencyInjection/PatternKitExampleServiceCollectionExtensions.cs b/src/PatternKit.Examples/DependencyInjection/PatternKitExampleServiceCollectionExtensions.cs new file mode 100644 index 00000000..7690629f --- /dev/null +++ b/src/PatternKit.Examples/DependencyInjection/PatternKitExampleServiceCollectionExtensions.cs @@ -0,0 +1,473 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using PatternKit.Behavioral.Chain; +using PatternKit.Behavioral.Strategy; +using PatternKit.Behavioral.TypeDispatcher; +using PatternKit.Creational.Prototype; +using PatternKit.Creational.Singleton; +using PatternKit.Examples.ApiGateway; +using PatternKit.Examples.AsyncStateDemo; +using PatternKit.Examples.Chain; +using PatternKit.Examples.Chain.ConfigDriven; +using PatternKit.Examples.EnterpriseFeatureSlices; +using PatternKit.Examples.FlyweightDemo; +using PatternKit.Examples.Generators.Builders.CorporateApplicationBuilderDemo; +using PatternKit.Examples.Generators.Visitors; +using PatternKit.Examples.MementoDemo; +using PatternKit.Examples.Messaging; +using PatternKit.Examples.ObserverDemo; +using PatternKit.Examples.PatternShowcase; +using PatternKit.Examples.PointOfSale; +using PatternKit.Examples.Pricing; +using PatternKit.Examples.ProductionReadiness; +using PatternKit.Examples.PrototypeDemo; +using PatternKit.Examples.ProxyDemo; +using PatternKit.Examples.Singleton; +using PatternKit.Examples.Strategies.Coercion; +using PatternKit.Examples.Strategies.Composed; +using PatternKit.Examples.TemplateDemo; +using PatternKit.Examples.VisitorDemo; +using PatternKit.Messaging.Routing; +using PatternKit.Structural.Decorator; +using PatternKit.Structural.Proxy; +using CheckoutRequest = PatternKit.Examples.Messaging.CheckoutRequest; +using ConfigTenderHandler = PatternKit.Examples.Chain.ConfigDriven.ITenderHandler; +using ConfigPaymentPipeline = PatternKit.Examples.Chain.ConfigDriven.ConfigDrivenPipelineDemo.PaymentPipeline; +using DocumentValidationResult = PatternKit.Examples.Generators.Visitors.DocumentProcessingDemo.ValidationResult; +using EnterpriseCheckout = PatternKit.Examples.EnterpriseFeatureSlices.EnterpriseFeatureSlicesDemo.IEnterpriseCheckout; +using EnterpriseCheckoutRequest = PatternKit.Examples.EnterpriseFeatureSlices.EnterpriseFeatureSlicesDemo.CheckoutRequest; +using EnterpriseCheckoutResult = PatternKit.Examples.EnterpriseFeatureSlices.EnterpriseFeatureSlicesDemo.CheckoutResult; +using PosPaymentKind = PatternKit.Examples.ObserverDemo.PaymentKind; +using ShowcaseFacade = PatternKit.Examples.PatternShowcase.PatternShowcase.IOrderProcessingFacade; +using TransactionPipeline = PatternKit.Examples.Chain.TransactionPipeline; +using VisitorTender = PatternKit.Examples.VisitorDemo.Tender; + +namespace PatternKit.Examples.DependencyInjection; + +/// +/// Describes the concrete service registered for an example-level PatternKit integration. +/// +public sealed record PatternKitExampleServiceDescriptor( + string ExampleName, + Type ServiceType, + ExampleIntegrationSurface Integration); + +/// +/// Open-generic coercion adapter so consumers can inject coercers through standard IoC. +/// +public interface ICoercer +{ + T? From(object? value); +} + +/// +/// Default open-generic coercion adapter backed by . +/// +public sealed class CoercerService : ICoercer +{ + public T? From(object? value) => Coercer.From(value); +} + +public sealed record ProductionReadyExampleIntegrations(IPatternKitExampleCatalog Catalog); +public sealed record AuthLoggingChainExample(ActionChain Chain, List Log); +public sealed record CoercionExample(ICoercer Integers, ICoercer Booleans, ICoercer Strings); +public sealed record ComposedNotificationStrategyExample(AsyncStrategy Strategy); +public sealed record MediatedTransactionPipelineExample(TransactionPipeline Pipeline); +public sealed record ConfigDrivenTransactionPipelineExample(ConfigPaymentPipeline Pipeline); +public sealed record EnterpriseFeatureSlicesExample(EnterpriseCheckout Checkout, Func CreateRequest); +public sealed record MinimalWebRequestRouterExample(MiniRouter Router); +public sealed record PaymentProcessorDecoratorExample(Decorator Processor); +public sealed record PosAppStateSingletonExample(Singleton State); +public sealed record PricingCalculatorExample(PricingDemo.DemoArtifacts Artifacts); +public sealed record PosTenderVisitorExample(TypeDispatcher Renderer, ActionTypeDispatcher Router); +public sealed record ApiExceptionMappingVisitorExample(Func RunAsync); +public sealed record EventProcessingVisitorExample(Func RunAsync); +public sealed record MessageRouterVisitorExample(Func Run); +public sealed record PatternsShowcaseExample(ShowcaseFacade Facade); +public sealed record SourceGeneratorApplicationSuiteExample(Func> BuildProductionAsync); +public sealed record EnterpriseMessagingWorkflowSuiteExample(Func Run); +public sealed record ResilientCheckoutMailboxesExample(Func Run); +public sealed record MessagingBackplaneFacadeExample(Func> RunAsync); +public sealed record PrototypeGameCharacterFactoryExample(Prototype Factory); +public sealed record ProxyPatternDemonstrationsExample(Proxy RemoteProxy, Proxy<(string To, string Subject, string Body), bool> EmailProxy); +public sealed record FlyweightGlyphCacheExample(Func> RenderSentence); +public sealed record TextEditorMementoExample(MementoDemo.MementoDemo.TextEditor Editor); +public sealed record ObserverEventHubExample(EventHub Hub); +public sealed record ReactiveViewModelExample(ProfileViewModel ViewModel); +public sealed record ReactiveTransactionExample(ReactiveTransaction Transaction); +public sealed record AsyncConnectionStateMachineExample(Func Log)>> RunAsync); +public sealed record TemplateMethodSubclassingExample(DataProcessor Processor); +public sealed record TemplateMethodAsyncExample(AsyncDataPipeline Pipeline); + +/// +/// Fluent registration helpers for importing every documented PatternKit example into Microsoft.Extensions.DependencyInjection. +/// +public static class PatternKitExampleServiceCollectionExtensions +{ + public static IServiceCollection AddPatternKitExamples(this IServiceCollection services, IConfiguration? configuration = null) + => services + .AddProductionReadyExampleIntegrations() + .AddAuthLoggingChainExample() + .AddStrategyBasedDataCoercionExample() + .AddComposedNotificationStrategyExample() + .AddMediatedTransactionPipelineExample() + .AddConfigurationDrivenTransactionPipelineExample(configuration) + .AddEnterpriseFeatureSlicesExample() + .AddMinimalWebRequestRouterExample() + .AddPaymentProcessorDecoratorExample() + .AddPosAppStateSingletonExample() + .AddPricingCalculatorExample() + .AddPosTenderVisitorExample() + .AddApiExceptionMappingVisitorExample() + .AddEventProcessingVisitorExample() + .AddMessageRouterVisitorExample() + .AddPatternsShowcaseExample() + .AddSourceGeneratorApplicationSuiteExample() + .AddEnterpriseMessagingWorkflowSuiteExample() + .AddResilientCheckoutMailboxesExample() + .AddMessagingBackplaneFacadeExample() + .AddPrototypeGameCharacterFactoryExample() + .AddProxyPatternDemonstrationsExample() + .AddFlyweightGlyphCacheExample() + .AddTextEditorMementoExample() + .AddObserverEventHubExample() + .AddReactiveViewModelExample() + .AddReactiveTransactionExample() + .AddAsyncConnectionStateMachineExample() + .AddTemplateMethodSubclassingExample() + .AddTemplateMethodAsyncExample(); + + public static IServiceCollection AddProductionReadyExampleIntegrations(this IServiceCollection services) + { + services.AddPatternKitExampleCatalog(); + services.AddSingleton(sp => + new(sp.GetRequiredService())); + return services.RegisterExample("Production-Ready Example Integrations", ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost | ExampleIntegrationSurface.AspNetCore); + } + + public static IServiceCollection AddAuthLoggingChainExample(this IServiceCollection services) + { + services.AddSingleton(_ => + { + var log = new List(); + var chain = ActionChain.Create() + .When(static (in r) => r.Headers.ContainsKey("X-Request-Id")) + .ThenContinue(r => log.Add($"reqid={r.Headers["X-Request-Id"]}")) + .When(static (in r) => r.Path.StartsWith("/admin", StringComparison.Ordinal) && !r.Headers.ContainsKey("Authorization")) + .ThenStop(_ => log.Add("deny: missing auth")) + .Finally((in r, next) => + { + log.Add($"{r.Method} {r.Path}"); + next(r); + }) + .Build(); + return new AuthLoggingChainExample(chain, log); + }); + return services.RegisterExample("Auth & Logging Chain", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddStrategyBasedDataCoercionExample(this IServiceCollection services) + { + services.AddSingleton(typeof(ICoercer<>), typeof(CoercerService<>)); + services.AddSingleton(sp => new( + sp.GetRequiredService>(), + sp.GetRequiredService>(), + sp.GetRequiredService>())); + return services.RegisterExample("Strategy-Based Data Coercion", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddComposedNotificationStrategyExample(this IServiceCollection services) + { + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.AddSingleton(sp => new( + ComposedStrategies.BuildPreferenceAware( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService()))); + return services.RegisterExample("Composed Notification Strategy", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddMediatedTransactionPipelineExample(this IServiceCollection services) + { + services.TryAddSingleton(); + services.TryAddSingleton(_ => new(new() + { + [CardVendor.Visa] = new GenericProcessor("VisaNet"), + [CardVendor.Mastercard] = new GenericProcessor("MC"), + [CardVendor.Amex] = new GenericProcessor("Amex"), + [CardVendor.Chase] = new GenericProcessor("ChaseNet"), + [CardVendor.InHouse] = new GenericProcessor("InHouse"), + [CardVendor.Unknown] = new GenericProcessor("FallbackNet") + })); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.AddSingleton(sp => + { + var pipeline = TransactionPipelineBuilder.New() + .WithDeviceBus(sp.GetRequiredService()) + .WithTenderHandlers(sp.GetServices().ToArray()) + .AddPreauth() + .AddDiscountsAndTax() + .AddRounding() + .AddTenderHandling() + .AddFinalize() + .Build(); + + return new MediatedTransactionPipelineExample(pipeline); + }); + return services.RegisterExample("Mediated Transaction Pipeline", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddConfigurationDrivenTransactionPipelineExample(this IServiceCollection services, IConfiguration? configuration = null) + { + services.AddPaymentPipeline(configuration ?? new ConfigurationBuilder().Build()); + services.AddSingleton(sp => new(sp.GetRequiredService())); + return services.RegisterExample("Configuration-Driven Transaction Pipeline", ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.Options); + } + + public static IServiceCollection AddEnterpriseFeatureSlicesExample(this IServiceCollection services) + { + services.AddEnterpriseFeatureSlices(); + services.AddSingleton(sp => new( + sp.GetRequiredService(), + () => EnterpriseFeatureSlicesDemo.CreateRetailRequest())); + return services.RegisterExample("Enterprise Feature Slices with .NET DI", ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddMinimalWebRequestRouterExample(this IServiceCollection services) + { + services.AddSingleton(_ => MiniRouter.Create() + .Map(static (in r) => string.Equals(r.Path, "/health", StringComparison.OrdinalIgnoreCase), static (in _) => Responses.Text(200, "ok")) + .Map(static (in r) => string.Equals(r.Path, "/orders", StringComparison.OrdinalIgnoreCase), static (in _) => Responses.Json(200, """{"items":[]}""")) + .NotFound(static (in _) => Responses.NotFound()) + .Build()); + services.AddSingleton(sp => new(sp.GetRequiredService())); + return services.RegisterExample("Minimal Web Request Router", ExampleIntegrationSurface.AspNetCore | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddPaymentProcessorDecoratorExample(this IServiceCollection services) + { + services.AddSingleton(_ => PaymentProcessorDemo.CreateEcommerceProcessor([])); + services.AddSingleton(sp => new(sp.GetRequiredService>())); + return services.RegisterExample("Payment Processor Decorator", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddPosAppStateSingletonExample(this IServiceCollection services) + { + services.AddSingleton(_ => PosAppStateDemo.BuildLazy()); + services.AddSingleton(sp => new(sp.GetRequiredService>())); + return services.RegisterExample("POS App State Singleton", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddPricingCalculatorExample(this IServiceCollection services) + { + services.AddSingleton(_ => PricingDemo.BuildDefault()); + services.AddSingleton(sp => new(sp.GetRequiredService())); + return services.RegisterExample("Pricing Calculator", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddPosTenderVisitorExample(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(_ => ReceiptRendering.CreateRenderer()); + services.AddSingleton(sp => Routing.CreateRouter(sp.GetRequiredService())); + services.AddSingleton(sp => new( + sp.GetRequiredService>(), + sp.GetRequiredService>())); + return services.RegisterExample("POS Tender Visitor", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddApiExceptionMappingVisitorExample(this IServiceCollection services) + { + services.AddSingleton(new ApiExceptionMappingVisitorExample(DocumentProcessingDemo.RunAsync)); + return services.RegisterExample("API Exception Mapping Visitor", ExampleIntegrationSurface.AspNetCore | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddEventProcessingVisitorExample(this IServiceCollection services) + { + services.AddSingleton(new EventProcessingVisitorExample(DocumentProcessingDemo.RunAsync)); + return services.RegisterExample("Event Processing Visitor", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddMessageRouterVisitorExample(this IServiceCollection services) + { + services.AddSingleton(new MessageRouterVisitorExample(MessageRoutingExample.Run)); + return services.RegisterExample("Message Router Visitor", ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddPatternsShowcaseExample(this IServiceCollection services) + { + services.AddSingleton(_ => PatternShowcase.PatternShowcase.Build()); + services.AddSingleton(sp => new(sp.GetRequiredService())); + return services.RegisterExample("Patterns Showcase", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddSourceGeneratorApplicationSuiteExample(this IServiceCollection services) + { + services.AddSingleton(new SourceGeneratorApplicationSuiteExample(CorporateApplicationDemo.BuildProductionAsync)); + return services.RegisterExample("Source Generator Application Suite", ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost); + } + + public static IServiceCollection AddEnterpriseMessagingWorkflowSuiteExample(this IServiceCollection services) + { + services.AddSingleton(new EnterpriseMessagingWorkflowSuiteExample(MessageEnvelopeExample.Run)); + return services.RegisterExample("Enterprise Messaging Workflow Suite", ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddResilientCheckoutMailboxesExample(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(new ResilientCheckoutMailboxesExample(ResilientCheckoutDemo.Run)); + return services.RegisterExample("Resilient Checkout and Collaborating Mailboxes", ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddMessagingBackplaneFacadeExample(this IServiceCollection services) + { + services.AddSingleton(new MessagingBackplaneFacadeExample(BackplaneFacadeDemo.RunAsync)); + return services.RegisterExample("Messaging Backplane Facade", ExampleIntegrationSurface.GenericHost | ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.ExternalInfrastructure | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddPrototypeGameCharacterFactoryExample(this IServiceCollection services) + { + services.AddSingleton(_ => PrototypeDemo.PrototypeDemo.CreateCharacterFactory()); + services.AddSingleton(sp => new(sp.GetRequiredService>())); + return services.RegisterExample("Prototype Game Character Factory", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddProxyPatternDemonstrationsExample(this IServiceCollection services) + { + services.AddSingleton(_ => Proxy.Create(id => $"Remote data for ID {id}").CachingProxy().Build()); + services.AddSingleton(_ => + { + var mock = ProxyDemo.ProxyDemo.MockFramework.CreateMock<(string To, string Subject, string Body), bool>() + .Setup(input => input.To.Contains("@example.com", StringComparison.OrdinalIgnoreCase), true) + .Returns(true); + return mock.Build(); + }); + services.AddSingleton(sp => new( + sp.GetRequiredService>(), + sp.GetRequiredService>())); + return services.RegisterExample("Proxy Pattern Demonstrations", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddFlyweightGlyphCacheExample(this IServiceCollection services) + { + services.AddSingleton(new FlyweightGlyphCacheExample(FlyweightDemo.FlyweightDemo.RenderSentence)); + return services.RegisterExample("Flyweight Glyph Cache", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddTextEditorMementoExample(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(sp => new(sp.GetRequiredService())); + return services.RegisterExample("Text Editor Memento", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddObserverEventHubExample(this IServiceCollection services) + { + services.AddSingleton(_ => EventHub.CreateDefault()); + services.AddSingleton(sp => new(sp.GetRequiredService>())); + return services.RegisterExample("Observer Event Hub", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddReactiveViewModelExample(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(sp => new(sp.GetRequiredService())); + return services.RegisterExample("Reactive ViewModel", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddReactiveTransactionExample(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(sp => new(sp.GetRequiredService())); + return services.RegisterExample("Reactive Transaction", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddAsyncConnectionStateMachineExample(this IServiceCollection services) + { + services.AddSingleton(new AsyncConnectionStateMachineExample(events => ConnectionStateDemo.RunAsync(events))); + return services.RegisterExample("Async Connection State Machine", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddTemplateMethodSubclassingExample(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(sp => new(sp.GetRequiredService())); + return services.RegisterExample("Template Method Subclassing", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + public static IServiceCollection AddTemplateMethodAsyncExample(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(sp => new(sp.GetRequiredService())); + return services.RegisterExample("Template Method Async", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.DependencyInjection); + } + + private static IServiceCollection RegisterExample( + this IServiceCollection services, + string name, + ExampleIntegrationSurface integration) + where T : class + { + services.AddSingleton(new PatternKitExampleServiceDescriptor(name, typeof(T), integration)); + return services; + } + + private sealed class DemoIdentityService : IIdentityService + { + public ValueTask HasVerifiedEmailAsync(Guid userId, CancellationToken ct) => new(true); + public ValueTask HasSmsOptInAsync(Guid userId, CancellationToken ct) => new(true); + public ValueTask HasPushTokenAsync(Guid userId, CancellationToken ct) => new(true); + } + + private sealed class DemoPresenceService : IPresenceService + { + public ValueTask IsOnlineInImAsync(Guid userId, CancellationToken ct) => new(true); + public ValueTask IsDoNotDisturbAsync(Guid userId, CancellationToken ct) => new(false); + } + + private sealed class DemoRateLimiter : IRateLimiter + { + public ValueTask CanSendAsync(Channel channel, Guid userId, CancellationToken ct) => new(true); + } + + private sealed class DemoPreferenceService : IPreferenceService + { + public ValueTask GetPreferredOrderAsync(Guid userId, CancellationToken ct) + => new([Channel.Push, Channel.Email, Channel.Sms]); + } + + private sealed class DemoEmailSender : IEmailSender + { + public ValueTask SendAsync(SendContext ctx, CancellationToken ct) => new ValueTask(new SendResult(Channel.Email, true, "email:accepted")); + } + + private sealed class DemoSmsSender : ISmsSender + { + public ValueTask SendAsync(SendContext ctx, CancellationToken ct) => new ValueTask(new SendResult(Channel.Sms, true, "sms:accepted")); + } + + private sealed class DemoPushSender : IPushSender + { + public ValueTask SendAsync(SendContext ctx, CancellationToken ct) => new ValueTask(new SendResult(Channel.Push, true, "push:accepted")); + } + + private sealed class DemoImSender : IImSender + { + public ValueTask SendAsync(SendContext ctx, CancellationToken ct) => new ValueTask(new SendResult(Channel.Im, true, "im:accepted")); + } +} diff --git a/test/PatternKit.Examples.Tests/DependencyInjection/PatternKitExampleDependencyInjectionTests.cs b/test/PatternKit.Examples.Tests/DependencyInjection/PatternKitExampleDependencyInjectionTests.cs new file mode 100644 index 00000000..ac8bcf27 --- /dev/null +++ b/test/PatternKit.Examples.Tests/DependencyInjection/PatternKitExampleDependencyInjectionTests.cs @@ -0,0 +1,170 @@ +using Microsoft.Extensions.DependencyInjection; +using PatternKit.Examples.ApiGateway; +using PatternKit.Examples.DependencyInjection; +using PatternKit.Examples.ObserverDemo; +using PatternKit.Examples.PointOfSale; +using PatternKit.Examples.ProductionReadiness; +using PatternKit.Examples.Strategies.Composed; +using Showcase = PatternKit.Examples.PatternShowcase.PatternShowcase; +using TinyBDD; +using TinyBDD.Xunit; +using Xunit.Abstractions; + +namespace PatternKit.Examples.Tests.DependencyInjection; + +[Feature("Example-level dependency injection integrations")] +public sealed class PatternKitExampleDependencyInjectionTests(ITestOutputHelper output) : TinyBddXunitBase(output) +{ + [Scenario("Every catalog example has a fluent IServiceCollection integration")] + [Fact] + public Task Every_Catalog_Example_Has_A_Fluent_IServiceCollection_Integration() + => Given("a service collection configured with all PatternKit examples", () => + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddPatternKitExamples(); + return services.BuildServiceProvider(validateScopes: true); + }) + .When("resolving the catalog and registered example service descriptors", provider => new + { + Provider = provider, + Catalog = provider.GetRequiredService(), + Descriptors = provider.GetServices().ToArray() + }) + .Then("each catalog entry has a matching IoC descriptor", result => + result.Catalog.Entries.All(entry => + result.Descriptors.Any(descriptor => + string.Equals(descriptor.ExampleName, entry.Name, StringComparison.Ordinal)))) + .And("each descriptor resolves its concrete integration service", result => + result.Descriptors.All(descriptor => + result.Provider.GetRequiredService(descriptor.ServiceType) is not null)) + .And("each integration advertises dependency injection as an available surface", result => + result.Descriptors.All(descriptor => + descriptor.Integration.HasFlag(ExampleIntegrationSurface.DependencyInjection))) + .AssertPassed(); + + [Scenario("IoC-registered examples can be used by importing applications")] + [Fact] + public Task IoC_Registered_Examples_Can_Be_Used_By_Importing_Applications() + => Given("a provider configured with all PatternKit examples", () => + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddPatternKitExamples(); + return services.BuildServiceProvider(validateScopes: true); + }) + .When("using representative registered examples", UseRegisteredExamples) + .Then("the registered examples produce expected outputs", checks => + { + foreach (var check in checks) + ScenarioExpect.True(check.Passed, check.Name); + }) + .AssertPassed(); + + private static IReadOnlyList<(string Name, bool Passed)> UseRegisteredExamples(ServiceProvider provider) + { + var coercion = provider.GetRequiredService(); + var notifications = provider.GetRequiredService(); + var auth = provider.GetRequiredService(); + var router = provider.GetRequiredService(); + var payment = provider.GetRequiredService(); + var singleton = provider.GetRequiredService(); + var showcase = provider.GetRequiredService(); + var proxy = provider.GetRequiredService(); + var flyweight = provider.GetRequiredService(); + var editor = provider.GetRequiredService(); + var eventHub = provider.GetRequiredService(); + var viewModel = provider.GetRequiredService(); + var transaction = provider.GetRequiredService(); + var asyncState = provider.GetRequiredService(); + var template = provider.GetRequiredService(); + var asyncTemplate = provider.GetRequiredService(); + var routing = provider.GetRequiredService(); + var envelope = provider.GetRequiredService(); + var checkout = provider.GetRequiredService(); + + auth.Chain.Execute(new PatternKit.Examples.Chain.HttpRequest("GET", "/admin/metrics", new Dictionary())); + + var json = router.Router.Handle(new Request("GET", "/orders", new Dictionary())); + var receipt = payment.Processor.Execute(CreateOrder()); + var order = showcase.Facade.Place(new Showcase.OrderDto( + "ORD-DI", + "VIP-100", + "cash", + [new Showcase.OrderItemDto("SKU-1", "Widget", 20m, 1)])); + + var received = false; + using var subscription = eventHub.Hub.On((in UserEvent _) => received = true); + eventHub.Hub.Publish(new UserEvent(1, "login")); + + viewModel.ViewModel.FirstName.Value = "Ada"; + viewModel.ViewModel.LastName.Value = "Lovelace"; + transaction.Transaction.AddItem(new LineItem("SKU-1", 1, 10m)); + transaction.Transaction.SetPayment(PaymentKind.CreditCard); + + var send = notifications.Strategy.ExecuteAsync(new SendContext(Guid.NewGuid(), "hello", false), CancellationToken.None) + .GetAwaiter() + .GetResult(); + var state = asyncState.RunAsync(["connect", "ok"]).GetAwaiter().GetResult(); + var asyncResult = asyncTemplate.Pipeline.ExecuteAsync(7, CancellationToken.None).GetAwaiter().GetResult(); + editor.Editor.Insert("hello"); + + return + [ + ("integer coercer converts text", coercion.Integers.From("42") == 42), + ("boolean coercer converts text", coercion.Booleans.From("true") == true), + ("string coercer accepts strings", coercion.Strings.From("patternkit") == "patternkit"), + ("notification strategy sends", send.Success), + ("auth chain logs denied admin requests", auth.Log.Contains("deny: missing auth", StringComparer.Ordinal)), + ("minimal router returns a successful response", json.StatusCode == 200), + ("decorated payment processor totals the order", receipt.FinalTotal > 0), + ("singleton POS app state is initialized", singleton.State.Instance.Devices.PrinterReady), + ("pattern showcase facade places an order", order.ok), + ("remote proxy returns remote data", proxy.RemoteProxy.Execute(42).Contains("42", StringComparison.Ordinal)), + ("email proxy accepts example addresses", proxy.EmailProxy.Execute(("user@example.com", "Hello", "Body"))), + ("flyweight renderer returns one glyph per character", flyweight.RenderSentence("hello").Count == 5), + ("memento editor tracks inserted text", editor.Editor.State.Text == "hello"), + ("observer event hub publishes events", received), + ("reactive view model enables save", viewModel.ViewModel.CanSave.Value), + ("reactive transaction enables checkout", transaction.Transaction.CanCheckout.Value), + ("async state machine connects", state.Final == PatternKit.Examples.AsyncStateDemo.ConnectionStateDemo.Mode.Connected), + ("template method counts words", template.Processor.Execute("one two") == 2), + ("async template method formats payloads", asyncResult == "PAYLOAD:7"), + ("message router visitor aggregates totals", routing.Run().AggregatedTotal == 100m), + ("message envelope example tracks first attempt", envelope.Run().Attempt == 1), + ("resilient checkout succeeds", checkout.Run(CreateCheckoutRequest(), new PatternKit.Examples.Messaging.CheckoutServices()).Succeeded) + ]; + } + + private static PurchaseOrder CreateOrder() + => new() + { + OrderId = "DI-ORDER", + Customer = new CustomerInfo { CustomerId = "CUST-1", LoyaltyTier = "Gold" }, + Store = new StoreLocation + { + StoreId = "STORE-1", + State = "CA", + Country = "USA", + StateTaxRate = 0.0725m, + LocalTaxRate = 0.0125m + }, + Items = + [ + new OrderLineItem + { + Sku = "SKU-1", + ProductName = "Widget", + UnitPrice = 25m, + Quantity = 1, + Category = "General" + } + ] + }; + + private static PatternKit.Examples.Messaging.CheckoutRequest CreateCheckoutRequest() + => new( + "checkout-di", + "customer-1", + [new PatternKit.Examples.Messaging.CheckoutLine("SKU-1", 1, 25m)]); +}