From a40c0d5567c88d9672eb3048dd381f123b21ef0e Mon Sep 17 00:00:00 2001 From: "Jerrett D. Davis" Date: Fri, 19 Sep 2025 23:23:42 -0500 Subject: [PATCH 1/6] refactor: minor code cleanup --- .../Behavioral/Command/Command.cs | 36 ++-- .../Behavioral/Mediator/Mediator.cs | 15 +- .../Creational/Builder/MutableBuilder.cs | 13 +- .../Creational/Factory/Factory.cs | 48 ++++- .../Structural/Composite/Composite.cs | 3 +- .../MediatorDemo/Abstractions.cs | 70 +------ .../MediatorDemo/MediatorScanning.cs | 138 +++++++++++++ src/PatternKit.Generators/packages.lock.json | 188 ++++++++++++++++++ 8 files changed, 400 insertions(+), 111 deletions(-) create mode 100644 src/PatternKit.Examples/MediatorDemo/MediatorScanning.cs diff --git a/src/PatternKit.Core/Behavioral/Command/Command.cs b/src/PatternKit.Core/Behavioral/Command/Command.cs index 9d57cba..96f6c98 100644 --- a/src/PatternKit.Core/Behavioral/Command/Command.cs +++ b/src/PatternKit.Core/Behavioral/Command/Command.cs @@ -121,12 +121,12 @@ ValueTask Do(in TCtx ctx, CancellationToken ct) for (var i = 0; i < items.Length; i++) { var vt = items[i].Execute(in ctx, ct); - if (!vt.IsCompletedSuccessfully) - { - // Slow path: await then continue; copy ctx to avoid capturing 'in' in async state machine - var copy = ctx; - return AwaitNext(i, vt, copy, ct, items); - } + if (vt.IsCompletedSuccessfully) + continue; + + // Slow path: await then continue; copy ctx to avoid capturing 'in' in async state machine + var copy = ctx; + return AwaitNext(i, vt, copy, ct, items); } return default; @@ -150,14 +150,11 @@ ValueTask Undo(in TCtx ctx, CancellationToken ct) { for (var i = items.Length - 1; i >= 0; i--) { - if (items[i].TryUndo(in ctx, ct, out var vt)) - { - if (!vt.IsCompletedSuccessfully) - { - var copy = ctx; - return AwaitUndo(i, vt, copy, ct, items); - } - } + if (!items[i].TryUndo(in ctx, ct, out var vt) || vt.IsCompletedSuccessfully) + continue; + + var copy = ctx; + return AwaitUndo(i, vt, copy, ct, items); } return default; @@ -167,11 +164,12 @@ static async ValueTask AwaitUndo(int startIndex, ValueTask pending, TCtx localCt await pending.ConfigureAwait(false); for (var j = startIndex - 1; j >= 0; j--) { - if (itemsArr[j].TryUndo(in localCtx, ct2, out var t)) - { - if (!t.IsCompletedSuccessfully) - await t.ConfigureAwait(false); - } + if (!itemsArr[j].TryUndo(in localCtx, ct2, out var t) || + t.IsCompletedSuccessfully) + continue; + + // Slow path: await then continue; copy ctx to avoid capturing 'in' in async state machine + await t.ConfigureAwait(false); } } } diff --git a/src/PatternKit.Core/Behavioral/Mediator/Mediator.cs b/src/PatternKit.Core/Behavioral/Mediator/Mediator.cs index d4cf25d..85d2537 100644 --- a/src/PatternKit.Core/Behavioral/Mediator/Mediator.cs +++ b/src/PatternKit.Core/Behavioral/Mediator/Mediator.cs @@ -110,10 +110,10 @@ private Mediator( /// Cancellation token. /// The handler produced response. /// Thrown if no handler exists or the result cannot be cast to . - public ValueTask Send(in TRequest request, CancellationToken ct = default) + public ValueTask Send(in TRequest request, CancellationToken ct = default) => Core(request, ct); - private async ValueTask Core(TRequest request, CancellationToken ct) + private async ValueTask Core(TRequest request, CancellationToken ct) { if (!_commands.TryGetValue(typeof(TRequest), out var handler)) throw new InvalidOperationException($"No command handler registered for request type '{typeof(TRequest)}'."); @@ -135,10 +135,13 @@ private async ValueTask Core(TRequest request, C foreach (var b in _post) await b(in boxed, obj, ct).ConfigureAwait(false); - if (obj is TResponse r) return r; - if (obj is null) return default(TResponse)!; - throw new InvalidOperationException( - $"Handler returned incompatible result for '{typeof(TRequest)}' -> expected '{typeof(TResponse)}', got '{obj.GetType()}'."); + return obj switch + { + TResponse r => r, + null => default, + _ => throw new InvalidOperationException( + $"Handler returned incompatible result for '{typeof(TRequest)}' -> expected '{typeof(TResponse)}', got '{obj.GetType()}'.") + }; } /// diff --git a/src/PatternKit.Core/Creational/Builder/MutableBuilder.cs b/src/PatternKit.Core/Creational/Builder/MutableBuilder.cs index 28d54e7..506ed67 100644 --- a/src/PatternKit.Core/Creational/Builder/MutableBuilder.cs +++ b/src/PatternKit.Core/Creational/Builder/MutableBuilder.cs @@ -154,19 +154,14 @@ private interface IValidator string? Validate(TValue value); } - private sealed class FuncValidator : IValidator + private sealed class FuncValidator(Func f) : IValidator { - private readonly Func _f; - public FuncValidator(Func f) => _f = f; - public string? Validate(T value) => _f(value); + public string? Validate(T value) => f(value); } - private sealed class StatefulValidator : IValidator + private sealed class StatefulValidator(TState state, Func f) : IValidator { - private readonly TState _state; - private readonly Func _f; - public StatefulValidator(TState state, Func f) => (_state, _f) = (state, f); - public string? Validate(T value) => _f(value, _state); + public string? Validate(T value) => f(value, state); } } diff --git a/src/PatternKit.Core/Creational/Factory/Factory.cs b/src/PatternKit.Core/Creational/Factory/Factory.cs index 840a509..9f02971 100644 --- a/src/PatternKit.Core/Creational/Factory/Factory.cs +++ b/src/PatternKit.Core/Creational/Factory/Factory.cs @@ -38,9 +38,20 @@ public TOut Create(TKey key) /// Try to create an instance for . Returns false only if no mapping and no default. public bool TryCreate(TKey key, out TOut value) { - if (_creators.TryGetValue(key, out var ctor)) { value = ctor(); return true; } - if (_hasDefault) { value = _default(); return true; } - value = default!; return false; + if (_creators.TryGetValue(key, out var ctor)) + { + value = ctor(); + return true; + } + + if (_hasDefault) + { + value = _default(); + return true; + } + + value = default!; + return false; } private static void ThrowNoMapping(TKey key) @@ -62,7 +73,11 @@ public Builder Map(TKey key, Creator creator) } /// Set or replace the default creator used when no key mapping exists. - public Builder Default(Creator creator) { _default = creator; return this; } + public Builder Default(Creator creator) + { + _default = creator; + return this; + } /// Build an immutable factory snapshot. public Factory Build() @@ -114,9 +129,20 @@ public TOut Create(TKey key, in TIn input) /// Try to create an instance for using . Returns false only if no mapping and no default. public bool TryCreate(TKey key, in TIn input, out TOut value) { - if (_creators.TryGetValue(key, out var ctor)) { value = ctor(in input); return true; } - if (_hasDefault) { value = _default(in input); return true; } - value = default!; return false; + if (_creators.TryGetValue(key, out var ctor)) + { + value = ctor(in input); + return true; + } + + if (_hasDefault) + { + value = _default(in input); + return true; + } + + value = default!; + return false; } private static void ThrowNoMapping(TKey key) @@ -138,7 +164,11 @@ public Builder Map(TKey key, Creator creator) } /// Set or replace the default creator used when no key mapping exists. - public Builder Default(Creator creator) { _default = creator; return this; } + public Builder Default(Creator creator) + { + _default = creator; + return this; + } /// Build an immutable factory snapshot. public Factory Build() @@ -151,4 +181,4 @@ public Factory Build() static TOut ThrowBeforeDefault() => throw new InvalidOperationException("No Default() configured."); } } -} +} \ No newline at end of file diff --git a/src/PatternKit.Core/Structural/Composite/Composite.cs b/src/PatternKit.Core/Structural/Composite/Composite.cs index 1d86fda..b005422 100644 --- a/src/PatternKit.Core/Structural/Composite/Composite.cs +++ b/src/PatternKit.Core/Structural/Composite/Composite.cs @@ -75,8 +75,7 @@ public TOut Execute(in TIn input) return _leaf!(in input); } var acc = _seed!(in input); - var kids = _children; - foreach (var child in kids) + foreach (var child in _children) { var childResult = child.Execute(in input); acc = _combine!(in input, acc, childResult); diff --git a/src/PatternKit.Examples/MediatorDemo/Abstractions.cs b/src/PatternKit.Examples/MediatorDemo/Abstractions.cs index 1e1f7d5..408e303 100644 --- a/src/PatternKit.Examples/MediatorDemo/Abstractions.cs +++ b/src/PatternKit.Examples/MediatorDemo/Abstractions.cs @@ -113,6 +113,7 @@ public static class ServiceCollectionExtensions { /// /// Scan the supplied assemblies for mediator abstractions (handlers, behaviors) and register them. + /// Uses a Strategy-based scanner () to avoid deep nested conditionals. /// /// Service collection. /// Assemblies to scan (defaults to executing assembly if empty). @@ -125,73 +126,10 @@ public static IServiceCollection AddPatternKitMediator(this IServiceCollection s if (assemblies is null || assemblies.Length == 0) assemblies = [Assembly.GetExecutingAssembly()]; - var commands = new List<(Type, Type, Type)>(); - var notes = new List<(Type, Type)>(); - var streams = new List<(Type, Type, Type)>(); - var behaviors = new List<(Type, Type, Type)>(); - - foreach (var asm in assemblies.Distinct()) - { - foreach (var t in asm.GetTypes()) - { - if (t.IsAbstract || t.IsInterface) continue; - foreach (var it in t.GetInterfaces()) - { - if (!it.IsGenericType) - continue; - - var def = it.GetGenericTypeDefinition(); - var args = it.GetGenericArguments(); - - if (def == typeof(ICommandHandler<,>)) - { - services.AddTransient(it, t); - services.AddTransient(t); // allow resolving by concrete type if needed - commands.Add((args[0], args[1], t)); - } - else if (def == typeof(INotificationHandler<>)) - { - services.AddTransient(it, t); - services.AddTransient(t); // allow resolving by concrete type if needed - notes.Add((args[0], t)); - } -#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER - else if (def == typeof(IStreamRequestHandler<,>)) - { - services.AddTransient(it, t); - services.AddTransient(t); // allow resolving by concrete type if needed - streams.Add((args[0], args[1], t)); - } -#endif - else if (def == typeof(IPipelineBehavior<,>)) - { - // Behaviors can be open generic or closed; avoid invalid DI registrations for open generics. - if (t.IsGenericTypeDefinition) - { - // Skip registering open-generic behavior as a concrete service mapping. - // We'll resolve a closed instance via ActivatorUtilities when executing the pipeline. - } - else - { - // Closed/concrete behavior: allow DI to construct it if needed. - services.AddTransient(it, t); - services.AddTransient(t); - } - - behaviors.Add((t, args[0], args[1])); - } - } - } - } - - services.AddSingleton(new MediatorRegistry - { - Commands = commands.ToArray(), - Notifications = notes.ToArray(), - Streams = streams.ToArray(), - Behaviors = behaviors.ToArray(), - }); + // Delegate actual discovery to strategy-based scanner. + var registry = MediatorAssemblyScanner.Scan(assemblies, services); + services.AddSingleton(registry); services.AddScoped(); return services; } diff --git a/src/PatternKit.Examples/MediatorDemo/MediatorScanning.cs b/src/PatternKit.Examples/MediatorDemo/MediatorScanning.cs new file mode 100644 index 0000000..50ceee2 --- /dev/null +++ b/src/PatternKit.Examples/MediatorDemo/MediatorScanning.cs @@ -0,0 +1,138 @@ +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace PatternKit.Examples.MediatorDemo; + +/// +/// Pure (side-effect minimized) assembly scanner for mediator components. Discovers handler/behavior types, +/// produces immutable registration descriptors, then applies DI registrations in a single pass. +/// +internal static class MediatorAssemblyScanner +{ + private enum RegistrationKind { Command, Notification, Stream, Behavior } + + private sealed record Registration( + RegistrationKind Kind, + Type Implementation, + Type Interface, + Type? RequestType, + Type? ResponseOrItemType, + bool RegisterWithDi, + bool IsOpenBehavior); + + private interface IStrategy + { + Registration? Create(Type impl, Type iface); + } + + private sealed class CommandStrategy : IStrategy + { + public Registration? Create(Type impl, Type iface) + { + if (!iface.IsGenericType || iface.GetGenericTypeDefinition() != typeof(ICommandHandler<,>)) return null; + var args = iface.GetGenericArguments(); + return new(RegistrationKind.Command, impl, iface, args[0], args[1], RegisterWithDi: true, IsOpenBehavior: false); + } + } + + private sealed class NotificationStrategy : IStrategy + { + public Registration? Create(Type impl, Type iface) + { + if (!iface.IsGenericType || iface.GetGenericTypeDefinition() != typeof(INotificationHandler<>)) return null; + var arg = iface.GetGenericArguments()[0]; + return new(RegistrationKind.Notification, impl, iface, arg, null, RegisterWithDi: true, IsOpenBehavior: false); + } + } + +#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER + private sealed class StreamStrategy : IStrategy + { + public Registration? Create(Type impl, Type iface) + { + if (!iface.IsGenericType || iface.GetGenericTypeDefinition() != typeof(IStreamRequestHandler<,>)) return null; + var args = iface.GetGenericArguments(); + return new(RegistrationKind.Stream, impl, iface, args[0], args[1], RegisterWithDi: true, IsOpenBehavior: false); + } + } +#endif + + private sealed class BehaviorStrategy : IStrategy + { + public Registration? Create(Type impl, Type iface) + { + if (!iface.IsGenericType || iface.GetGenericTypeDefinition() != typeof(IPipelineBehavior<,>)) return null; + var args = iface.GetGenericArguments(); + var isOpen = impl.IsGenericTypeDefinition; + // Open behaviors are resolved/closed manually later => don't DI-register the open definition. + return new(RegistrationKind.Behavior, impl, iface, args[0], args[1], RegisterWithDi: !isOpen, IsOpenBehavior: isOpen); + } + } + + private static readonly IStrategy[] Strategies = CreateStrategies(); + private static IStrategy[] CreateStrategies() + { + var list = new List + { + new CommandStrategy(), + new NotificationStrategy(), + new BehaviorStrategy() + }; +#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER + list.Add(new StreamStrategy()); +#endif + return list.ToArray(); + } + + /// + /// Discover mediator registrations in the provided assemblies, build a registry, then apply DI registrations. + /// + /// Assemblies to scan. + /// Service collection to populate. + public static MediatorRegistry Scan(IEnumerable assemblies, IServiceCollection services) + { + var regs = assemblies + .Distinct() + .SelectMany(a => a.GetTypes()) + .Where(t => !t.IsAbstract && !t.IsInterface) + .SelectMany(impl => impl.GetInterfaces() + .Where(i => i.IsGenericType) + .Select(i => Strategies.Select(s => s.Create(impl, i)).FirstOrDefault(r => r is not null))) + .Where(r => r is not null) + .Cast() + .ToList(); + + // Apply DI registrations (single pass) without duplicates. + var diPairs = new HashSet<(Type iface, Type impl)>(); + foreach (var r in regs.Where(r => r.RegisterWithDi)) + { + var key = (r.Interface, r.Implementation); + if (!diPairs.Add(key)) + continue; + + services.AddTransient(r.Interface, r.Implementation); + services.AddTransient(r.Implementation); // allow resolving by concrete type + } + + var commands = regs.Where(r => r.Kind == RegistrationKind.Command) + .Select(r => (r.RequestType!, r.ResponseOrItemType!, r.Implementation)) + .ToArray(); + var notifications = regs.Where(r => r.Kind == RegistrationKind.Notification) + .Select(r => (r.RequestType!, r.Implementation)) + .ToArray(); + var streams = regs.Where(r => r.Kind == RegistrationKind.Stream) + .Select(r => (r.RequestType!, r.ResponseOrItemType!, r.Implementation)) + .ToArray(); + var behaviors = regs.Where(r => r.Kind == RegistrationKind.Behavior) + .Select(r => (r.Implementation, r.RequestType!, r.ResponseOrItemType!)) + .ToArray(); + + return new MediatorRegistry + { + Commands = commands, + Notifications = notifications, + Streams = streams, + Behaviors = behaviors + }; + } +} \ No newline at end of file diff --git a/src/PatternKit.Generators/packages.lock.json b/src/PatternKit.Generators/packages.lock.json index 4eb9db9..40a2a74 100644 --- a/src/PatternKit.Generators/packages.lock.json +++ b/src/PatternKit.Generators/packages.lock.json @@ -117,6 +117,194 @@ "System.Runtime.CompilerServices.Unsafe": "4.5.3" } } + }, + ".NETStandard,Version=v2.1": { + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Direct", + "requested": "[3.11.0, )", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Direct", + "requested": "[4.14.0, )", + "resolved": "4.14.0", + "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[4.14.0]", + "System.Buffers": "4.5.1", + "System.Collections.Immutable": "9.0.0", + "System.Memory": "4.5.5", + "System.Numerics.Vectors": "4.5.0", + "System.Reflection.Metadata": "9.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encoding.CodePages": "7.0.0", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "System.Collections.Immutable": { + "type": "Direct", + "requested": "[9.0.9, )", + "resolved": "9.0.9", + "contentHash": "/kpkgDxH984e3J3z5v/DIFi+0TWbUJXS8HNKUYBy3YnXtK09JVGs3cw5aOV6fDSw5NxbWLWlGrYjRteu6cjX3w==", + "dependencies": { + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "System.Buffers": "4.5.1", + "System.Collections.Immutable": "9.0.0", + "System.Memory": "4.5.5", + "System.Numerics.Vectors": "4.5.0", + "System.Reflection.Metadata": "9.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encoding.CodePages": "7.0.0", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Numerics.Vectors": "4.4.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "ANiqLu3DxW9kol/hMmTWbt3414t9ftdIuiIU7j80okq2YzAueo120M442xk1kDJWtmZTqWQn7wHDvMRipVOEOQ==", + "dependencies": { + "System.Collections.Immutable": "9.0.0", + "System.Memory": "4.5.5" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==", + "dependencies": { + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + } + }, + "net8.0": { + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Direct", + "requested": "[3.11.0, )", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Direct", + "requested": "[4.14.0, )", + "resolved": "4.14.0", + "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[4.14.0]", + "System.Collections.Immutable": "9.0.0", + "System.Reflection.Metadata": "9.0.0" + } + }, + "System.Collections.Immutable": { + "type": "Direct", + "requested": "[9.0.9, )", + "resolved": "9.0.9", + "contentHash": "/kpkgDxH984e3J3z5v/DIFi+0TWbUJXS8HNKUYBy3YnXtK09JVGs3cw5aOV6fDSw5NxbWLWlGrYjRteu6cjX3w==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "System.Collections.Immutable": "9.0.0", + "System.Reflection.Metadata": "9.0.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "ANiqLu3DxW9kol/hMmTWbt3414t9ftdIuiIU7j80okq2YzAueo120M442xk1kDJWtmZTqWQn7wHDvMRipVOEOQ==", + "dependencies": { + "System.Collections.Immutable": "9.0.0" + } + } + }, + "net9.0": { + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Direct", + "requested": "[3.11.0, )", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Direct", + "requested": "[4.14.0, )", + "resolved": "4.14.0", + "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[4.14.0]", + "System.Collections.Immutable": "9.0.0", + "System.Reflection.Metadata": "9.0.0" + } + }, + "System.Collections.Immutable": { + "type": "Direct", + "requested": "[9.0.9, )", + "resolved": "9.0.9", + "contentHash": "/kpkgDxH984e3J3z5v/DIFi+0TWbUJXS8HNKUYBy3YnXtK09JVGs3cw5aOV6fDSw5NxbWLWlGrYjRteu6cjX3w==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "System.Collections.Immutable": "9.0.0", + "System.Reflection.Metadata": "9.0.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "ANiqLu3DxW9kol/hMmTWbt3414t9ftdIuiIU7j80okq2YzAueo120M442xk1kDJWtmZTqWQn7wHDvMRipVOEOQ==" + } } } } \ No newline at end of file From 759fbf33416d7a92936ffc845902e73d4f5e8661 Mon Sep 17 00:00:00 2001 From: JD Davis Date: Fri, 19 Sep 2025 23:26:43 -0500 Subject: [PATCH 2/6] Update src/PatternKit.Core/Behavioral/Command/Command.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/PatternKit.Core/Behavioral/Command/Command.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PatternKit.Core/Behavioral/Command/Command.cs b/src/PatternKit.Core/Behavioral/Command/Command.cs index 96f6c98..69deaa5 100644 --- a/src/PatternKit.Core/Behavioral/Command/Command.cs +++ b/src/PatternKit.Core/Behavioral/Command/Command.cs @@ -168,7 +168,6 @@ static async ValueTask AwaitUndo(int startIndex, ValueTask pending, TCtx localCt t.IsCompletedSuccessfully) continue; - // Slow path: await then continue; copy ctx to avoid capturing 'in' in async state machine await t.ConfigureAwait(false); } } From 5626e83f55cce8bc1fd1fc7a46bda1da2857054f Mon Sep 17 00:00:00 2001 From: JD Davis Date: Fri, 19 Sep 2025 23:26:49 -0500 Subject: [PATCH 3/6] Update src/PatternKit.Core/Behavioral/Command/Command.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/PatternKit.Core/Behavioral/Command/Command.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PatternKit.Core/Behavioral/Command/Command.cs b/src/PatternKit.Core/Behavioral/Command/Command.cs index 69deaa5..e02f18a 100644 --- a/src/PatternKit.Core/Behavioral/Command/Command.cs +++ b/src/PatternKit.Core/Behavioral/Command/Command.cs @@ -124,7 +124,7 @@ ValueTask Do(in TCtx ctx, CancellationToken ct) if (vt.IsCompletedSuccessfully) continue; - // Slow path: await then continue; copy ctx to avoid capturing 'in' in async state machine + // Not completed successfully: enter slow path (await then continue); copy ctx to avoid capturing 'in' in async state machine var copy = ctx; return AwaitNext(i, vt, copy, ct, items); } From 6bb63b799763196e716e4c97f19d580d8e3ef3b4 Mon Sep 17 00:00:00 2001 From: "Jerrett D. Davis" Date: Fri, 19 Sep 2025 23:37:48 -0500 Subject: [PATCH 4/6] refactor: more general cleanup --- .../Behavioral/Mediator/Mediator.cs | 50 +++-- src/PatternKit.Core/Common/Throw.cs | 6 + src/PatternKit.Generators/packages.lock.json | 188 ------------------ 3 files changed, 34 insertions(+), 210 deletions(-) diff --git a/src/PatternKit.Core/Behavioral/Mediator/Mediator.cs b/src/PatternKit.Core/Behavioral/Mediator/Mediator.cs index 85d2537..4c63329 100644 --- a/src/PatternKit.Core/Behavioral/Mediator/Mediator.cs +++ b/src/PatternKit.Core/Behavioral/Mediator/Mediator.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using System.Runtime.CompilerServices; +using PatternKit.Common; namespace PatternKit.Behavioral.Mediator; @@ -215,9 +216,12 @@ private async IAsyncEnumerable StreamCore( await foreach (var item in seq.ConfigureAwait(false).WithCancellation(ct)) { - if (item is TItem ti) yield return ti; - else if (item is null && default(TItem) is null) yield return default!; - else throw new InvalidOperationException($"Stream item type mismatch. Expected '{typeof(TItem)}' but got '{item?.GetType()}'."); + yield return item switch + { + TItem ti => ti, + null when default(TItem) is null => default!, + _ => throw new InvalidOperationException($"Stream item type mismatch. Expected '{typeof(TItem)}' but got '{item?.GetType()}'.") + }; } // After stream completes @@ -225,10 +229,9 @@ private async IAsyncEnumerable StreamCore( await b(in boxed, null, ct).ConfigureAwait(false); } - private readonly struct BoxedAsyncEnumerable + private readonly struct BoxedAsyncEnumerable(IAsyncEnumerable inner) { - public readonly IAsyncEnumerable Inner; - public BoxedAsyncEnumerable(IAsyncEnumerable inner) => Inner = inner; + public readonly IAsyncEnumerable Inner = inner; } #endif @@ -243,13 +246,13 @@ public sealed class Builder // Typed delegate shapes for registration (with 'in' parameters) public delegate ValueTask CommandHandlerTyped(in TRequest request, CancellationToken ct); - public delegate TResponse SyncCommandHandlerTyped(in TRequest request); + public delegate TResponse SyncCommandHandlerTyped(in TRequest request); public delegate ValueTask NotificationHandlerTyped(in TNotification notification, CancellationToken ct); public delegate void SyncNotificationHandlerTyped(in TNotification notification); #if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER - public delegate IAsyncEnumerable StreamHandlerTyped(in TRequest request, CancellationToken ct); + public delegate IAsyncEnumerable StreamHandlerTyped(in TRequest request, CancellationToken ct); #endif private readonly Dictionary _commands = new(); @@ -262,37 +265,38 @@ public sealed class Builder private readonly List _whole = new(4); /// Add a pre behavior executed before any handler logic. - public Builder Pre(PreBehavior behavior) + public Builder Pre(PreBehavior? behavior) { if (behavior is not null) _pre.Add(behavior); return this; } /// Add a post behavior executed after handler or stream completion. - public Builder Post(PostBehavior behavior) + public Builder Post(PostBehavior? behavior) { if (behavior is not null) _post.Add(behavior); return this; } /// Add a whole (around) behavior wrapping handler + remaining behaviors. - public Builder Whole(WholeBehavior behavior) + public Builder Whole(WholeBehavior? behavior) { if (behavior is not null) _whole.Add(behavior); return this; } /// Register an asynchronous command handler. + /// public Builder Command(CommandHandlerTyped handler) { - if (handler is null) throw new ArgumentNullException(nameof(handler)); + Throw.ArgumentNullWhenNull(handler); _commands[typeof(TRequest)] = Adapt; return this; ValueTask Adapt(in object? req, CancellationToken ct) - => req is not TRequest r - ? throw new InvalidOperationException($"Invalid request type for '{typeof(TRequest)}'.") + => req is not TRequest r + ? throw new InvalidOperationException($"Invalid request type for '{typeof(TRequest)}'.") : MediatorHelpers.Box(handler(in r, ct)); } @@ -305,15 +309,16 @@ public Builder Notification(NotificationHandlerTyped n is TNotification t - ? handler(in t, ct) - : throw new InvalidOperationException($"Invalid notification type for '{typeof(TNotification)}'."); - if (!_notifications.TryGetValue(typeof(TNotification), out var list)) _notifications[typeof(TNotification)] = list = new List(2); + list.Add(Adapt); return this; + + ValueTask Adapt(in object n, CancellationToken ct) + => n is TNotification t + ? handler(in t, ct) + : throw new InvalidOperationException($"Invalid notification type for '{typeof(TNotification)}'."); } /// Register a synchronous notification handler (wrapped internally). @@ -339,7 +344,7 @@ public Builder Stream(StreamHandlerTyped handl : throw new InvalidOperationException($"Invalid request type for stream '{typeof(TRequest)}'."); static async IAsyncEnumerable AdaptEnum( - IAsyncEnumerable items, + IAsyncEnumerable items, [EnumeratorCancellation] CancellationToken ct) { await foreach (var it in items.ConfigureAwait(false).WithCancellation(ct)) @@ -395,8 +400,9 @@ internal static class MediatorHelpers /// Boxed value task returning an or null. public static ValueTask Box(ValueTask vt) { - if (vt.IsCompletedSuccessfully) return new ValueTask(vt.Result); - return Await(vt); + return vt.IsCompletedSuccessfully + ? new ValueTask(vt.Result) + : Await(vt); static async ValueTask Await(ValueTask v) { diff --git a/src/PatternKit.Core/Common/Throw.cs b/src/PatternKit.Core/Common/Throw.cs index 89aab50..833f32d 100644 --- a/src/PatternKit.Core/Common/Throw.cs +++ b/src/PatternKit.Core/Common/Throw.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; namespace PatternKit.Common; @@ -74,4 +75,9 @@ public static T NoStrategyMatched() => [DoesNotReturn] public static void NoStrategyMatched() => throw new InvalidOperationException("No strategy matched and no default provided."); + + public static void ArgumentNullWhenNull([CallerMemberName] object? arg = null) + { + if (arg is null) throw new ArgumentNullException(nameof(arg)); + } } \ No newline at end of file diff --git a/src/PatternKit.Generators/packages.lock.json b/src/PatternKit.Generators/packages.lock.json index 40a2a74..4eb9db9 100644 --- a/src/PatternKit.Generators/packages.lock.json +++ b/src/PatternKit.Generators/packages.lock.json @@ -117,194 +117,6 @@ "System.Runtime.CompilerServices.Unsafe": "4.5.3" } } - }, - ".NETStandard,Version=v2.1": { - "Microsoft.CodeAnalysis.Analyzers": { - "type": "Direct", - "requested": "[3.11.0, )", - "resolved": "3.11.0", - "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" - }, - "Microsoft.CodeAnalysis.CSharp": { - "type": "Direct", - "requested": "[4.14.0, )", - "resolved": "4.14.0", - "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.11.0", - "Microsoft.CodeAnalysis.Common": "[4.14.0]", - "System.Buffers": "4.5.1", - "System.Collections.Immutable": "9.0.0", - "System.Memory": "4.5.5", - "System.Numerics.Vectors": "4.5.0", - "System.Reflection.Metadata": "9.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Encoding.CodePages": "7.0.0", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "System.Collections.Immutable": { - "type": "Direct", - "requested": "[9.0.9, )", - "resolved": "9.0.9", - "contentHash": "/kpkgDxH984e3J3z5v/DIFi+0TWbUJXS8HNKUYBy3YnXtK09JVGs3cw5aOV6fDSw5NxbWLWlGrYjRteu6cjX3w==", - "dependencies": { - "System.Memory": "4.5.5", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "Microsoft.CodeAnalysis.Common": { - "type": "Transitive", - "resolved": "4.14.0", - "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.11.0", - "System.Buffers": "4.5.1", - "System.Collections.Immutable": "9.0.0", - "System.Memory": "4.5.5", - "System.Numerics.Vectors": "4.5.0", - "System.Reflection.Metadata": "9.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Encoding.CodePages": "7.0.0", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.5.1", - "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" - }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.5.5", - "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", - "dependencies": { - "System.Buffers": "4.5.1", - "System.Numerics.Vectors": "4.4.0", - "System.Runtime.CompilerServices.Unsafe": "4.5.3" - } - }, - "System.Numerics.Vectors": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "ANiqLu3DxW9kol/hMmTWbt3414t9ftdIuiIU7j80okq2YzAueo120M442xk1kDJWtmZTqWQn7wHDvMRipVOEOQ==", - "dependencies": { - "System.Collections.Immutable": "9.0.0", - "System.Memory": "4.5.5" - } - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" - }, - "System.Text.Encoding.CodePages": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==", - "dependencies": { - "System.Memory": "4.5.5", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "System.Threading.Tasks.Extensions": { - "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "4.5.3" - } - } - }, - "net8.0": { - "Microsoft.CodeAnalysis.Analyzers": { - "type": "Direct", - "requested": "[3.11.0, )", - "resolved": "3.11.0", - "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" - }, - "Microsoft.CodeAnalysis.CSharp": { - "type": "Direct", - "requested": "[4.14.0, )", - "resolved": "4.14.0", - "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.11.0", - "Microsoft.CodeAnalysis.Common": "[4.14.0]", - "System.Collections.Immutable": "9.0.0", - "System.Reflection.Metadata": "9.0.0" - } - }, - "System.Collections.Immutable": { - "type": "Direct", - "requested": "[9.0.9, )", - "resolved": "9.0.9", - "contentHash": "/kpkgDxH984e3J3z5v/DIFi+0TWbUJXS8HNKUYBy3YnXtK09JVGs3cw5aOV6fDSw5NxbWLWlGrYjRteu6cjX3w==" - }, - "Microsoft.CodeAnalysis.Common": { - "type": "Transitive", - "resolved": "4.14.0", - "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.11.0", - "System.Collections.Immutable": "9.0.0", - "System.Reflection.Metadata": "9.0.0" - } - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "ANiqLu3DxW9kol/hMmTWbt3414t9ftdIuiIU7j80okq2YzAueo120M442xk1kDJWtmZTqWQn7wHDvMRipVOEOQ==", - "dependencies": { - "System.Collections.Immutable": "9.0.0" - } - } - }, - "net9.0": { - "Microsoft.CodeAnalysis.Analyzers": { - "type": "Direct", - "requested": "[3.11.0, )", - "resolved": "3.11.0", - "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" - }, - "Microsoft.CodeAnalysis.CSharp": { - "type": "Direct", - "requested": "[4.14.0, )", - "resolved": "4.14.0", - "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.11.0", - "Microsoft.CodeAnalysis.Common": "[4.14.0]", - "System.Collections.Immutable": "9.0.0", - "System.Reflection.Metadata": "9.0.0" - } - }, - "System.Collections.Immutable": { - "type": "Direct", - "requested": "[9.0.9, )", - "resolved": "9.0.9", - "contentHash": "/kpkgDxH984e3J3z5v/DIFi+0TWbUJXS8HNKUYBy3YnXtK09JVGs3cw5aOV6fDSw5NxbWLWlGrYjRteu6cjX3w==" - }, - "Microsoft.CodeAnalysis.Common": { - "type": "Transitive", - "resolved": "4.14.0", - "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.11.0", - "System.Collections.Immutable": "9.0.0", - "System.Reflection.Metadata": "9.0.0" - } - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "ANiqLu3DxW9kol/hMmTWbt3414t9ftdIuiIU7j80okq2YzAueo120M442xk1kDJWtmZTqWQn7wHDvMRipVOEOQ==" - } } } } \ No newline at end of file From f8849b47107520361a2692fce6c4f3472a5be708 Mon Sep 17 00:00:00 2001 From: "Jerrett D. Davis" Date: Fri, 19 Sep 2025 23:45:28 -0500 Subject: [PATCH 5/6] docs: updated docs for command usage instructions. --- README.md | 10 +- .../Behavioral/Command/Command.cs | 149 +++++++++++++++--- 2 files changed, 129 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index ef539fa..b70029c 100644 --- a/README.md +++ b/README.md @@ -68,11 +68,11 @@ if (parse.Execute("123", out var n)) PatternKit will grow to cover **Creational**, **Structural**, and **Behavioral** patterns with fluent, discoverable APIs: -| Category | Patterns ✓ = implemented | -| -------------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **Creational** | [Factory](docs/patterns/creational/factory/factory.md) ✓ • [Composer](docs/patterns/creational/builder/composer.md) ✓ • [ChainBuilder](docs/patterns/creational/builder/chainbuilder.md) ✓ • [BranchBuilder](docs/patterns/creational/builder/chainbuilder.md) ✓ • [MutableBuilder](docs/patterns/creational/builder/mutablebuilder.md) ✓ • [Prototype](docs/patterns/creational/prototype/prototype.md) ✓ • [Singleton](docs/patterns/creational/singleton/singleton.md) ✓ | -| **Structural** | [Adapter](docs/patterns/structural/adapter/fluent-adapter.md) ✓ • [Bridge](docs/patterns/structural/bridge/bridge.md) ✓ • [Composite](docs/patterns/structural/composite/composite.md) ✓ • Decorator (planned) • Facade (planned) • Flyweight (planned) • Proxy (planned) | -| **Behavioral** | [Strategy](docs/patterns/behavioral/strategy/strategy.md) ✓ • [TryStrategy](docs/patterns/behavioral/strategy/trystrategy.md) ✓ • [ActionStrategy](docs/patterns/behavioral/strategy/actionstrategy.md) ✓ • [ActionChain](docs/patterns/behavioral/chain/actionchain.md) ✓ • [ResultChain](docs/patterns/behavioral/chain/resultchain.md) ✓ • Command (planned) • Iterator (planned) • Mediator (planned) • Memento (planned) • Observer (planned) • State (planned) • Template Method (planned) • Visitor (planned) | +| Category | Patterns ✓ = implemented | +| -------------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Creational** | [Factory](docs/patterns/creational/factory/factory.md) ✓ • [Composer](docs/patterns/creational/builder/composer.md) ✓ • [ChainBuilder](docs/patterns/creational/builder/chainbuilder.md) ✓ • [BranchBuilder](docs/patterns/creational/builder/chainbuilder.md) ✓ • [MutableBuilder](docs/patterns/creational/builder/mutablebuilder.md) ✓ • [Prototype](docs/patterns/creational/prototype/prototype.md) ✓ • [Singleton](docs/patterns/creational/singleton/singleton.md) ✓ | +| **Structural** | [Adapter](docs/patterns/structural/adapter/fluent-adapter.md) ✓ • [Bridge](docs/patterns/structural/bridge/bridge.md) ✓ • [Composite](docs/patterns/structural/composite/composite.md) ✓ • Decorator (planned) • Facade (planned) • Flyweight (planned) • Proxy (planned) | +| **Behavioral** | [Strategy](docs/patterns/behavioral/strategy/strategy.md) ✓ • [TryStrategy](docs/patterns/behavioral/strategy/trystrategy.md) ✓ • [ActionStrategy](docs/patterns/behavioral/strategy/actionstrategy.md) ✓ • [ActionChain](docs/patterns/behavioral/chain/actionchain.md) ✓ • [ResultChain](docs/patterns/behavioral/chain/resultchain.md) ✓ • [Command](docs/patterns/behavioral/command/command.md) ✓ • Iterator (planned) • [Mediator](docs/behavioral/mediator/mediator.md) • Memento (planned) • Observer (planned) • State (planned) • Template Method (planned) • Visitor (planned) | Each pattern will ship with: diff --git a/src/PatternKit.Core/Behavioral/Command/Command.cs b/src/PatternKit.Core/Behavioral/Command/Command.cs index e02f18a..3d1ffef 100644 --- a/src/PatternKit.Core/Behavioral/Command/Command.cs +++ b/src/PatternKit.Core/Behavioral/Command/Command.cs @@ -3,12 +3,94 @@ namespace PatternKit.Behavioral.Command; /// -/// Allocation-light command that executes an action (and optional undo) over an input context. -/// Supports sync/async via ValueTask and can be composed into a macro with ordered execution and reverse undo. +/// Command Pattern (allocation-light) +/// Encapsulates a unit of work (an action) plus an optional inverse (undo) so it can be +/// executed, queued, composed, retried, or reversed in a uniform way. This implementation +/// focuses on very low allocation overhead while still supporting synchronous and asynchronous +/// () execution and macro (composite) commands. /// +/// +/// When should I use this? +/// +/// You need to encapsulate business operations so callers do not depend on concrete implementation details. +/// You want an optional undo step (optimistic UI, reversible batch / maintenance tasks, editor actions). +/// You want to compose several smaller operations into an ordered macro (with automatic reverse-order undo). +/// You plan to queue, schedule, audit, retry, or log operations uniformly. +/// You need both sync and async handlers without separate abstractions. +/// +/// When NOT to use: If you simply need to call a method once with no intent to compose or undo, introducing a command adds unnecessary indirection. +/// Thread safety: A instance is immutable after and therefore reusable across threads. Thread safety of the underlying action depends on the provided delegates and the contents. +/// Performance notes: Delegates are captured once; execution avoids allocations for the fast path when underlying work completes synchronously. Undo is optional; if you do not configure it, is false and returns false without allocations. +/// Value semantics of in TCtx: Passing the context by readonly reference avoids copying large structs. For mutable operations prefer reference types or ensure struct methods mutate internal state safely. +/// Related patterns: See also Composite (macro commands), Memento (for more complex state restoration), and Strategy (for pluggable behavior without undo). +/// Failure handling: Exceptions thrown inside Do or Undo propagate to the caller. Macro commands stop on first failure; previously executed sub-commands are not automatically undone unless you explicitly call the macro's undo afterward. If you need transactional semantics, wrap macro execution in a try/catch and invoke undo only when appropriate. +/// +/// Examples +/// +/// 1. Basic synchronous command with undo +/// .Create() +/// .Do(c => c.Value++) +/// .Undo(c => c.Value--) +/// .Build(); +/// await increment.Execute(in counter); // Value: 1 +/// if (increment.TryUndo(in counter, out var undoTask)) +/// await undoTask; // Value: 0 +/// ]]> +/// +/// +/// 2. Asynchronous command (I/O) with cancellation +/// .Create() +/// .Do(async (in string path, CancellationToken ct) => { +/// using var fs = File.Create(path); +/// await fs.WriteAsync(new byte[]{1,2,3}, ct); +/// }) +/// .Build(); +/// await save.Execute(in filePath, cancellationToken); +/// ]]> +/// +/// +/// 3. Macro (composite) command with conditional stage and reverse-order undo +/// Log); +/// var compile = Command.Create().Do(c => c.Log.Add("compile")).Undo(c => c.Log.Add("undo-compile")).Build(); +/// var test = Command.Create().Do(c => c.Log.Add("test")).Undo(c => c.Log.Add("undo-test")).Build(); +/// var pack = Command.Create().Do(c => c.Log.Add("pack")).Build(); // no undo +/// bool runTests = true; +/// var pipeline = Command.Macro() +/// .Add(compile) +/// .AddIf(runTests, test) +/// .Add(pack) +/// .Build(); +/// var ctx = new BuildCtx(new List()); +/// await pipeline.Execute(in ctx); // Log: compile, test, pack +/// if (pipeline.TryUndo(in ctx, out var undoVt)) +/// await undoVt; // Log adds: undo-test, undo-compile (reverse, skipping pack) +/// ]]> +/// +/// +/// 4. Optimistic UI action with optional undo +/// >.Create() +/// .Do(list => list.Add("draft")) +/// .Undo(list => list.Remove("draft")) +/// .Build(); +/// ]]> +/// +/// +/// Context type the command operates on. public sealed class Command { - /// Asynchronous execution delegate for a command. + /// + /// Asynchronous (or synchronous) execution delegate signature. + /// Return default / completed for synchronous completion. + /// + /// Context (readonly reference). + /// Cancellation token. public delegate ValueTask Exec(in TCtx ctx, CancellationToken ct); private readonly Exec _do; @@ -16,12 +98,25 @@ public sealed class Command private Command(Exec @do, Exec? undo) => (_do, _undo) = (@do, undo); - /// Execute the command against . + /// + /// Execute the command logic. Throws if the underlying delegate throws. + /// + /// Context value. + /// Cancellation token. + /// enabling allocation-free sync completion. public ValueTask Execute(in TCtx ctx, CancellationToken ct) => _do(in ctx, ct); + /// Execute with . public ValueTask Execute(in TCtx ctx) => _do(in ctx, CancellationToken.None); - /// Attempt to undo the command; returns false when no undo was configured. + /// + /// Attempt to undo. Returns false if no undo handler was configured. + /// The returned must be awaited if true. + /// + /// Context. + /// Cancellation token. + /// Undo value task (valid only when result is true). + /// true if an undo handler exists; otherwise false. public bool TryUndo(in TCtx ctx, CancellationToken ct, out ValueTask undoTask) { if (_undo is null) @@ -29,87 +124,91 @@ public bool TryUndo(in TCtx ctx, CancellationToken ct, out ValueTask undoTask) undoTask = default; return false; } - undoTask = _undo(in ctx, ct); return true; } + /// Convenience overload using . public bool TryUndo(in TCtx ctx, out ValueTask undoTask) => TryUndo(in ctx, default, out undoTask); - /// Whether this command has an undo handler. + /// Indicates whether this command has an undo handler. public bool HasUndo => _undo is not null; // ---- Builder ---- + /// + /// Start building a new command. Provide a Do delegate (required) and optionally an Undo delegate. + /// public static Builder Create() => new(); - /// Fluent builder for . + /// + /// Fluent builder for . Not thread-safe; configure on one thread then call once. + /// public sealed class Builder { private Exec? _do; private Exec? _undo; - /// Set the required Do handler. + /// Set the required asynchronous (or synchronous) Do handler. public Builder Do(Exec @do) { _do = @do; return this; } - /// Set sync Do handler (adapter). + /// Set a synchronous Do handler (adapter wrapped in a ). public Builder Do(Action @do) { - _do = (in c, _) => - { - @do(c); - return default; - }; + _do = (in c, _) => { @do(c); return default; }; return this; } - /// Set optional Undo handler. + /// Set the optional asynchronous Undo handler. public Builder Undo(Exec undo) { _undo = undo; return this; } - /// Set sync Undo handler (adapter). + /// Set a synchronous Undo handler (adapter). public Builder Undo(Action undo) { - _undo = (in c, _) => - { - undo(c); - return default; - }; + _undo = (in c, _) => { undo(c); return default; }; return this; } - /// Build an immutable command. + /// Build an immutable command instance. Throws if no Do handler was supplied. public Command Build() => new(_do ?? throw new InvalidOperationException("Command requires Do handler."), _undo); } // ---- Macro composition ---- + /// + /// Begin a macro (composite) command definition. Sub-commands execute in registration order; undo runs in reverse order and skips those without undo handlers. + /// public static MacroBuilder Macro() => new(); - /// Builder for a macro command that runs sub-commands in order and undoes in reverse. + /// + /// Builder for a macro command that runs sub-commands in order and undoes in reverse. Supports conditional inclusion via . + /// public sealed class MacroBuilder { private readonly ChainBuilder> _chain = ChainBuilder>.Create(); + /// Add a sub-command to the macro. public MacroBuilder Add(Command cmd) { _chain.Add(cmd); return this; } + /// Add a sub-command only when is true. public MacroBuilder AddIf(bool condition, Command cmd) { _chain.AddIf(condition, cmd); return this; } - /// Build a macro command. + /// Finalize and build a macro command. public Command Build() { var items = _chain.Build(static cmds => cmds); From 405adb1d4daedc36abc41db6c11a7daac6bc5326 Mon Sep 17 00:00:00 2001 From: "Jerrett D. Davis" Date: Fri, 19 Sep 2025 23:52:37 -0500 Subject: [PATCH 6/6] docs: updated links in docs to point to patterns. --- README.md | 10 +- docs/index.md | 15 +- src/PatternKit.Generators/packages.lock.json | 188 +++++++++++++++++++ 3 files changed, 201 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b70029c..8188c0c 100644 --- a/README.md +++ b/README.md @@ -68,11 +68,11 @@ if (parse.Execute("123", out var n)) PatternKit will grow to cover **Creational**, **Structural**, and **Behavioral** patterns with fluent, discoverable APIs: -| Category | Patterns ✓ = implemented | -| -------------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **Creational** | [Factory](docs/patterns/creational/factory/factory.md) ✓ • [Composer](docs/patterns/creational/builder/composer.md) ✓ • [ChainBuilder](docs/patterns/creational/builder/chainbuilder.md) ✓ • [BranchBuilder](docs/patterns/creational/builder/chainbuilder.md) ✓ • [MutableBuilder](docs/patterns/creational/builder/mutablebuilder.md) ✓ • [Prototype](docs/patterns/creational/prototype/prototype.md) ✓ • [Singleton](docs/patterns/creational/singleton/singleton.md) ✓ | -| **Structural** | [Adapter](docs/patterns/structural/adapter/fluent-adapter.md) ✓ • [Bridge](docs/patterns/structural/bridge/bridge.md) ✓ • [Composite](docs/patterns/structural/composite/composite.md) ✓ • Decorator (planned) • Facade (planned) • Flyweight (planned) • Proxy (planned) | -| **Behavioral** | [Strategy](docs/patterns/behavioral/strategy/strategy.md) ✓ • [TryStrategy](docs/patterns/behavioral/strategy/trystrategy.md) ✓ • [ActionStrategy](docs/patterns/behavioral/strategy/actionstrategy.md) ✓ • [ActionChain](docs/patterns/behavioral/chain/actionchain.md) ✓ • [ResultChain](docs/patterns/behavioral/chain/resultchain.md) ✓ • [Command](docs/patterns/behavioral/command/command.md) ✓ • Iterator (planned) • [Mediator](docs/behavioral/mediator/mediator.md) • Memento (planned) • Observer (planned) • State (planned) • Template Method (planned) • Visitor (planned) | +| Category | Patterns ✓ = implemented | +| -------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Creational** | [Factory](docs/patterns/creational/factory/factory.md) ✓ • [Composer](docs/patterns/creational/builder/composer.md) ✓ • [ChainBuilder](docs/patterns/creational/builder/chainbuilder.md) ✓ • [BranchBuilder](docs/patterns/creational/builder/chainbuilder.md) ✓ • [MutableBuilder](docs/patterns/creational/builder/mutablebuilder.md) ✓ • [Prototype](docs/patterns/creational/prototype/prototype.md) ✓ • [Singleton](docs/patterns/creational/singleton/singleton.md) ✓ | +| **Structural** | [Adapter](docs/patterns/structural/adapter/fluent-adapter.md) ✓ • [Bridge](docs/patterns/structural/bridge/bridge.md) ✓ • [Composite](docs/patterns/structural/composite/composite.md) ✓ • Decorator (planned) • Facade (planned) • Flyweight (planned) • Proxy (planned) | +| **Behavioral** | [Strategy](docs/patterns/behavioral/strategy/strategy.md) ✓ • [TryStrategy](docs/patterns/behavioral/strategy/trystrategy.md) ✓ • [ActionStrategy](docs/patterns/behavioral/strategy/actionstrategy.md) ✓ • [ActionChain](docs/patterns/behavioral/chain/actionchain.md) ✓ • [ResultChain](docs/patterns/behavioral/chain/resultchain.md) ✓ • [Command](docs/patterns/behavioral/command/command.md) ✓ • Iterator (planned) • [Mediator](docs/behavioral/mediator/mediator.md) ✓ • Memento (planned) • Observer (planned) • State (planned) • Template Method (planned) • Visitor (planned) | Each pattern will ship with: diff --git a/docs/index.md b/docs/index.md index eeb0475..79b2b0a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -60,15 +60,16 @@ if (parser.Execute("123", out var value)) ## 📚 Available Patterns -PatternKit will ultimately support the full spectrum of **creational**, **structural**, and **behavioral** patterns: +PatternKit will grow to cover **Creational**, **Structural**, and **Behavioral** patterns with fluent, discoverable APIs: -| Category | Patterns ✓ = Implemented | -| -------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **Creational** | Factory (planned) • [Builder](xref:PatternKit.Creational.Builder) (planned) • Prototype (planned) • Singleton (planned) | -| **Structural** | Adapter (planned) • Bridge (planned) • Composite (planned) • Decorator (planned) • Facade (planned) • Flyweight (planned) • Proxy (planned) | -| **Behavioral** | [Strategy](xref:PatternKit.Behavioral.Strategy.Strategy`2) ✓ • [TryStrategy](xref:PatternKit.Behavioral.Strategy.TryStrategy`2) ✓ • Chain of Responsibility (planned) • Command (planned) • Iterator (planned) • Mediator (planned) • Memento (planned) • Observer (planned) • State (planned) • Template Method (planned) • Visitor (planned) | +| Category | Patterns ✓ = implemented | +| -------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Creational** | [Factory](patterns/creational/factory/factory.md) ✓ • [Composer](patterns/creational/builder/composer.md) ✓ • [ChainBuilder](patterns/creational/builder/chainbuilder.md) ✓ • [BranchBuilder](patterns/creational/builder/chainbuilder.md) ✓ • [MutableBuilder](patterns/creational/builder/mutablebuilder.md) ✓ • [Prototype](patterns/creational/prototype/prototype.md) ✓ • [Singleton](patterns/creational/singleton/singleton.md) ✓ | +| **Structural** | [Adapter](patterns/structural/adapter/fluent-adapter.md) ✓ • [Bridge](patterns/structural/bridge/bridge.md) ✓ • [Composite](patterns/structural/composite/composite.md) ✓ • Decorator (planned) • Facade (planned) • Flyweight (planned) • Proxy (planned) | +| **Behavioral** | [Strategy](patterns/behavioral/strategy/strategy.md) ✓ • [TryStrategy](patterns/behavioral/strategy/trystrategy.md) ✓ • [ActionStrategy](patterns/behavioral/strategy/actionstrategy.md) ✓ • [ActionChain](patterns/behavioral/chain/actionchain.md) ✓ • [ResultChain](patterns/behavioral/chain/resultchain.md) ✓ • [Command](patterns/behavioral/command/command.md) ✓ • Iterator (planned) • [Mediator](behavioral/mediator/mediator.md) ✓ • Memento (planned) • Observer (planned) • State (planned) • Template Method (planned) • Visitor (planned) | + +Each pattern will ship with: -Each pattern ships with: * **Fluent API**: readable and composable (`.When(...)`, `.Then(...)`, `.Finally(...)`) * **Strongly-typed handlers** using `in` parameters diff --git a/src/PatternKit.Generators/packages.lock.json b/src/PatternKit.Generators/packages.lock.json index 4eb9db9..40a2a74 100644 --- a/src/PatternKit.Generators/packages.lock.json +++ b/src/PatternKit.Generators/packages.lock.json @@ -117,6 +117,194 @@ "System.Runtime.CompilerServices.Unsafe": "4.5.3" } } + }, + ".NETStandard,Version=v2.1": { + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Direct", + "requested": "[3.11.0, )", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Direct", + "requested": "[4.14.0, )", + "resolved": "4.14.0", + "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[4.14.0]", + "System.Buffers": "4.5.1", + "System.Collections.Immutable": "9.0.0", + "System.Memory": "4.5.5", + "System.Numerics.Vectors": "4.5.0", + "System.Reflection.Metadata": "9.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encoding.CodePages": "7.0.0", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "System.Collections.Immutable": { + "type": "Direct", + "requested": "[9.0.9, )", + "resolved": "9.0.9", + "contentHash": "/kpkgDxH984e3J3z5v/DIFi+0TWbUJXS8HNKUYBy3YnXtK09JVGs3cw5aOV6fDSw5NxbWLWlGrYjRteu6cjX3w==", + "dependencies": { + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "System.Buffers": "4.5.1", + "System.Collections.Immutable": "9.0.0", + "System.Memory": "4.5.5", + "System.Numerics.Vectors": "4.5.0", + "System.Reflection.Metadata": "9.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encoding.CodePages": "7.0.0", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Numerics.Vectors": "4.4.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "ANiqLu3DxW9kol/hMmTWbt3414t9ftdIuiIU7j80okq2YzAueo120M442xk1kDJWtmZTqWQn7wHDvMRipVOEOQ==", + "dependencies": { + "System.Collections.Immutable": "9.0.0", + "System.Memory": "4.5.5" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==", + "dependencies": { + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + } + }, + "net8.0": { + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Direct", + "requested": "[3.11.0, )", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Direct", + "requested": "[4.14.0, )", + "resolved": "4.14.0", + "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[4.14.0]", + "System.Collections.Immutable": "9.0.0", + "System.Reflection.Metadata": "9.0.0" + } + }, + "System.Collections.Immutable": { + "type": "Direct", + "requested": "[9.0.9, )", + "resolved": "9.0.9", + "contentHash": "/kpkgDxH984e3J3z5v/DIFi+0TWbUJXS8HNKUYBy3YnXtK09JVGs3cw5aOV6fDSw5NxbWLWlGrYjRteu6cjX3w==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "System.Collections.Immutable": "9.0.0", + "System.Reflection.Metadata": "9.0.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "ANiqLu3DxW9kol/hMmTWbt3414t9ftdIuiIU7j80okq2YzAueo120M442xk1kDJWtmZTqWQn7wHDvMRipVOEOQ==", + "dependencies": { + "System.Collections.Immutable": "9.0.0" + } + } + }, + "net9.0": { + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Direct", + "requested": "[3.11.0, )", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Direct", + "requested": "[4.14.0, )", + "resolved": "4.14.0", + "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[4.14.0]", + "System.Collections.Immutable": "9.0.0", + "System.Reflection.Metadata": "9.0.0" + } + }, + "System.Collections.Immutable": { + "type": "Direct", + "requested": "[9.0.9, )", + "resolved": "9.0.9", + "contentHash": "/kpkgDxH984e3J3z5v/DIFi+0TWbUJXS8HNKUYBy3YnXtK09JVGs3cw5aOV6fDSw5NxbWLWlGrYjRteu6cjX3w==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "System.Collections.Immutable": "9.0.0", + "System.Reflection.Metadata": "9.0.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "ANiqLu3DxW9kol/hMmTWbt3414t9ftdIuiIU7j80okq2YzAueo120M442xk1kDJWtmZTqWQn7wHDvMRipVOEOQ==" + } } } } \ No newline at end of file