diff --git a/CSharpEssentials.Mediator/Behaviors/BehaviorCache.cs b/CSharpEssentials.Mediator/Behaviors/BehaviorCache.cs new file mode 100644 index 0000000..cffa818 --- /dev/null +++ b/CSharpEssentials.Mediator/Behaviors/BehaviorCache.cs @@ -0,0 +1,55 @@ +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; + +using CSharpEssentials.Errors; +using CSharpEssentials.ResultPattern; + +namespace CSharpEssentials.Mediator; + +/// +/// Shared static cache for pipeline behaviors that need to construct +/// / failure responses. +/// +/// is keyed by closed generic types. +/// Growth is bounded by the number of distinct Result<T> response types +/// declared across all handlers in the consuming assembly β€” typically 5–50 entries. +/// Entries are never stale, so eviction is intentionally absent. +/// +/// +internal static class BehaviorCache +{ + internal static readonly Type ResultType = typeof(Result); + internal static readonly Type GenericResultType = typeof(Result<>); + + /// + /// Compiled Result<T>.Failure(errors) factories, keyed by concrete closed generic type. + /// Bounded by the number of distinct Result<T> handler response types in the assembly. + /// + internal static readonly ConcurrentDictionary> FailureFactories = new(); + + /// + /// Returns (or compiles and caches) a delegate that invokes Result<T>.Failure(errors) + /// for the closed generic . + /// + /// + /// Thrown when is not a generic type. + /// Callers must verify before calling. + /// + internal static Func GetOrCreateFactory(Type responseType) + { + if (!responseType.IsGenericType) + throw new ArgumentException($"Expected a generic type, but got {responseType.FullName}.", nameof(responseType)); + + return FailureFactories.GetOrAdd(responseType, static type => + { + MethodInfo method = type.GetMethod(nameof(Result.Failure), [typeof(IEnumerable)]) + ?? throw new InvalidOperationException($"Method {nameof(Result.Failure)} not found on {type.FullName}."); + ParameterExpression param = Expression.Parameter(typeof(Error[]), "errors"); + UnaryExpression asEnumerable = Expression.Convert(param, typeof(IEnumerable)); + MethodCallExpression call = Expression.Call(method, asEnumerable); + UnaryExpression boxed = Expression.Convert(call, typeof(object)); + return Expression.Lambda>(boxed, param).Compile(); + }); + } +} diff --git a/CSharpEssentials.Mediator/Behaviors/ExceptionHandlingBehavior.cs b/CSharpEssentials.Mediator/Behaviors/ExceptionHandlingBehavior.cs index d0b4dc0..4e7c5e0 100644 --- a/CSharpEssentials.Mediator/Behaviors/ExceptionHandlingBehavior.cs +++ b/CSharpEssentials.Mediator/Behaviors/ExceptionHandlingBehavior.cs @@ -1,6 +1,3 @@ -using System.Linq.Expressions; -using System.Reflection; - using Mediator; using CSharpEssentials.Errors; @@ -30,8 +27,8 @@ public sealed class ExceptionHandlingBehavior public ExceptionHandlingBehavior() { Type t = typeof(TResponse); - _canHandleResponse = t == ValidationBehaviorCache.ResultType - || t.IsGenericType && t.GetGenericTypeDefinition() == ValidationBehaviorCache.GenericResultType; + _canHandleResponse = t == BehaviorCache.ResultType + || t.IsGenericType && t.GetGenericTypeDefinition() == BehaviorCache.GenericResultType; } public ValueTask Handle( @@ -86,23 +83,9 @@ static async ValueTask WrapAsync(ValueTask vt, Type respon /// private static TResponse BuildFailureResponse(Error error, Type responseType) { - if (responseType == ValidationBehaviorCache.ResultType) + if (responseType == BehaviorCache.ResultType) return (TResponse)(object)Result.Failure(error); - // Result β€” use compiled factory cached per concrete closed generic type. - Type genericType = ValidationBehaviorCache.GenericResultType.MakeGenericType(responseType.GenericTypeArguments[0]); - - Func factory = ValidationBehaviorCache.FailureFactories.GetOrAdd(genericType, static type => - { - MethodInfo method = type.GetMethod(nameof(Result.Failure), [typeof(IEnumerable)]) - ?? throw new InvalidOperationException($"Method {nameof(Result.Failure)} not found on {type.FullName}."); - ParameterExpression param = Expression.Parameter(typeof(Error[]), "errors"); - UnaryExpression asEnumerable = Expression.Convert(param, typeof(IEnumerable)); - MethodCallExpression call = Expression.Call(method, asEnumerable); - UnaryExpression boxed = Expression.Convert(call, typeof(object)); - return Expression.Lambda>(boxed, param).Compile(); - }); - - return (TResponse)factory([error]); + return (TResponse)BehaviorCache.GetOrCreateFactory(responseType)([error]); } } diff --git a/CSharpEssentials.Mediator/Behaviors/ValidationBehavior.cs b/CSharpEssentials.Mediator/Behaviors/ValidationBehavior.cs index a51259c..bb843b6 100644 --- a/CSharpEssentials.Mediator/Behaviors/ValidationBehavior.cs +++ b/CSharpEssentials.Mediator/Behaviors/ValidationBehavior.cs @@ -1,7 +1,4 @@ using Mediator; -using System.Collections.Concurrent; -using System.Linq.Expressions; -using System.Reflection; using CSharpEssentials.Errors; using CSharpEssentials.Exceptions; @@ -10,13 +7,6 @@ namespace CSharpEssentials.Mediator; -internal static class ValidationBehaviorCache -{ - public static readonly Type ResultType = typeof(Result); - public static readonly Type GenericResultType = typeof(Result<>); - public static readonly ConcurrentDictionary> FailureFactories = new(); -} - public sealed class ValidationBehavior(IEnumerable> validators) : IPipelineBehavior where TRequest : IMessage @@ -172,32 +162,17 @@ static async ValueTask> WrapAsync(ValueTask> v /// private TResponse BuildFailureResponse(Error[] errors) { - if (_responseType == ValidationBehaviorCache.ResultType) + if (_responseType == BehaviorCache.ResultType) return (TResponse)(object)Result.Failure(errors); if (_responseType.IsGenericType - && _responseType.GetGenericTypeDefinition() == ValidationBehaviorCache.GenericResultType) + && _responseType.GetGenericTypeDefinition() == BehaviorCache.GenericResultType) return CreateGenericResultResponse(errors); // TResponse cannot carry error information β€” delegate to GlobalExceptionHandler. throw new EnhancedValidationException(errors); } - private TResponse CreateGenericResultResponse(Error[] errors) - { - Type genericType = ValidationBehaviorCache.GenericResultType.MakeGenericType(_responseType.GenericTypeArguments[0]); - - Func factory = ValidationBehaviorCache.FailureFactories.GetOrAdd(genericType, static type => - { - MethodInfo method = type.GetMethod(nameof(Result.Failure), [typeof(IEnumerable)]) - ?? throw new InvalidOperationException($"Method {nameof(Result.Failure)} not found on {type.FullName}."); - ParameterExpression param = Expression.Parameter(typeof(Error[]), "errors"); - UnaryExpression asEnumerable = Expression.Convert(param, typeof(IEnumerable)); - MethodCallExpression call = Expression.Call(method, asEnumerable); - UnaryExpression boxed = Expression.Convert(call, typeof(object)); - return Expression.Lambda>(boxed, param).Compile(); - }); - - return (TResponse)factory(errors); - } + private TResponse CreateGenericResultResponse(Error[] errors) => + (TResponse)BehaviorCache.GetOrCreateFactory(_responseType)(errors); } diff --git a/CSharpEssentials.Tests/Mediator/ExceptionHandlingBehaviorTests.cs b/CSharpEssentials.Tests/Mediator/ExceptionHandlingBehaviorTests.cs index 7a62e72..cfd8335 100644 --- a/CSharpEssentials.Tests/Mediator/ExceptionHandlingBehaviorTests.cs +++ b/CSharpEssentials.Tests/Mediator/ExceptionHandlingBehaviorTests.cs @@ -40,7 +40,7 @@ public async Task Handle_Should_PassThrough_When_Handler_Returns_GenericResultSu var behavior = new ExceptionHandlingBehavior>(); var command = new TestExceptionCommand("test"); MessageHandlerDelegate> next = - (_, _) => new ValueTask>(Result.Success(42)); + (_, _) => new ValueTask>(42); Result result = await behavior.Handle(command, next, default); @@ -129,7 +129,7 @@ public async Task Handle_Should_Propagate_OperationCanceledException_When_Token_ var behavior = new ExceptionHandlingBehavior(); var command = new TestExceptionCommand("test"); using var cts = new CancellationTokenSource(); - cts.Cancel(); + await cts.CancelAsync(); MessageHandlerDelegate cancellingNext = (_, ct) => diff --git a/README.MD b/README.MD index 9f57794..670317e 100644 --- a/README.MD +++ b/README.MD @@ -27,7 +27,7 @@ Modular .NET NuGet ecosystem that bridges OOP and Functional Programming in C#. | `CSharpEssentials.Core` | String/GUID/collection utilities | [πŸ“–](examples/Examples.Core/README.md) | [![NuGet](https://img.shields.io/nuget/v/CSharpEssentials.Core.svg)](https://www.nuget.org/packages/CSharpEssentials.Core) | | `CSharpEssentials.Enums` | `[StringEnum]` source generator β€” AOT-safe enum↔string | [πŸ“–](examples/Examples.Enums/README.md) | [![NuGet](https://img.shields.io/nuget/v/CSharpEssentials.Enums.svg)](https://www.nuget.org/packages/CSharpEssentials.Enums) | | `CSharpEssentials.Rules` | Composable rule engine with `.And()/.Or()/.Linear()/.Next()` | [πŸ“–](examples/Examples.Rules/README.md) | [![NuGet](https://img.shields.io/nuget/v/CSharpEssentials.Rules.svg)](https://www.nuget.org/packages/CSharpEssentials.Rules) | -| `CSharpEssentials.Mediator` | CQRS pipeline behaviors: validation, logging, caching, transactions | [πŸ“–](CSharpEssentials.Mediator/Readme.MD) | [![NuGet](https://img.shields.io/nuget/v/CSharpEssentials.Mediator.svg)](https://www.nuget.org/packages/CSharpEssentials.Mediator) | +| `CSharpEssentials.Mediator` | CQRS pipeline behaviors: validation, logging, exception handling, caching, transactions | [πŸ“–](CSharpEssentials.Mediator/Readme.MD) | [![NuGet](https://img.shields.io/nuget/v/CSharpEssentials.Mediator.svg)](https://www.nuget.org/packages/CSharpEssentials.Mediator) | | `CSharpEssentials.Entity` | `EntityBase`, soft deletion, domain events | [πŸ“–](examples/Examples.Entity/README.md) | [![NuGet](https://img.shields.io/nuget/v/CSharpEssentials.Entity.svg)](https://www.nuget.org/packages/CSharpEssentials.Entity) | | `CSharpEssentials.EntityFrameworkCore` | EF Core interceptors (audit, events, slow queries) + pagination | [πŸ“–](examples/Examples.EntityFrameworkCore/README.md) | [![NuGet](https://img.shields.io/nuget/v/CSharpEssentials.EntityFrameworkCore.svg)](https://www.nuget.org/packages/CSharpEssentials.EntityFrameworkCore) | | `CSharpEssentials.AspNetCore` | `GlobalExceptionHandler`, `ResultEndpointFilter`, Swagger versioning | [πŸ“–](examples/Examples.AspNetCore/README.md) | [![NuGet](https://img.shields.io/nuget/v/CSharpEssentials.AspNetCore.svg)](https://www.nuget.org/packages/CSharpEssentials.AspNetCore) |