From 28f4eb6c5bbe3f5390739131e7c4cbf22bf368d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Recep=20=C5=9Een?= Date: Sun, 31 May 2026 12:41:15 +0300 Subject: [PATCH 1/5] fix(mediator): use implicit conversion and await CancelAsync in tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace Result.Success(42) with implicit int→Result conversion. Fix S6966 warning: use await cts.CancelAsync() instead of cts.Cancel(). Co-Authored-By: Claude Sonnet 4.6 --- .../Mediator/ExceptionHandlingBehaviorTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) => From 15a7499b3aae7b4cc2aa6680141a588ae4f48dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Recep=20=C5=9Een?= Date: Sun, 31 May 2026 12:52:42 +0300 Subject: [PATCH 2/5] refactor(mediator): extract shared BehaviorCache from behaviors Rename ValidationBehaviorCache to BehaviorCache (internal, shared by both ValidationBehavior and ExceptionHandlingBehavior). Extract the duplicated 10-line Expression factory lambda into BehaviorCache.GetOrCreateFactory(). Remove redundant usings from ExceptionHandlingBehavior. Add XML doc to FailureFactories documenting the bounded-growth design decision. Co-Authored-By: Claude Sonnet 4.6 --- .../Behaviors/ExceptionHandlingBehavior.cs | 25 ++------ .../Behaviors/ValidationBehavior.cs | 59 ++++++++++++------- 2 files changed, 43 insertions(+), 41 deletions(-) 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..6f79fbb 100644 --- a/CSharpEssentials.Mediator/Behaviors/ValidationBehavior.cs +++ b/CSharpEssentials.Mediator/Behaviors/ValidationBehavior.cs @@ -10,11 +10,45 @@ namespace CSharpEssentials.Mediator; -internal static class ValidationBehaviorCache +/// +/// 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 { public static readonly Type ResultType = typeof(Result); public 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. + /// public static readonly ConcurrentDictionary> FailureFactories = new(); + + /// + /// Returns (or compiles and caches) a delegate that invokes Result<T>.Failure(errors) + /// for the closed generic type derived from . + /// + internal static Func GetOrCreateFactory(Type responseType) + { + Type genericType = GenericResultType.MakeGenericType(responseType.GenericTypeArguments[0]); + return 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(); + }); + } } public sealed class ValidationBehavior(IEnumerable> validators) @@ -172,32 +206,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); } From b563235e67dae67159ec3b82239fe34468baa4a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Recep=20=C5=9Een?= Date: Sun, 31 May 2026 12:56:31 +0300 Subject: [PATCH 3/5] refactor(mediator): move BehaviorCache to its own file BehaviorCache is shared infrastructure for all pipeline behaviors. Moving it out of ValidationBehavior.cs into BehaviorCache.cs reflects its shared ownership and improves discoverability for future behaviors. Co-Authored-By: Claude Sonnet 4.6 --- .../Behaviors/BehaviorCache.cs | 49 +++++++++++++++++++ .../Behaviors/ValidationBehavior.cs | 44 ----------------- 2 files changed, 49 insertions(+), 44 deletions(-) create mode 100644 CSharpEssentials.Mediator/Behaviors/BehaviorCache.cs diff --git a/CSharpEssentials.Mediator/Behaviors/BehaviorCache.cs b/CSharpEssentials.Mediator/Behaviors/BehaviorCache.cs new file mode 100644 index 0000000..5d09ed9 --- /dev/null +++ b/CSharpEssentials.Mediator/Behaviors/BehaviorCache.cs @@ -0,0 +1,49 @@ +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 +{ + public static readonly Type ResultType = typeof(Result); + public 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. + /// + public static readonly ConcurrentDictionary> FailureFactories = new(); + + /// + /// Returns (or compiles and caches) a delegate that invokes Result<T>.Failure(errors) + /// for the closed generic type derived from . + /// + internal static Func GetOrCreateFactory(Type responseType) + { + Type genericType = GenericResultType.MakeGenericType(responseType.GenericTypeArguments[0]); + return 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(); + }); + } +} diff --git a/CSharpEssentials.Mediator/Behaviors/ValidationBehavior.cs b/CSharpEssentials.Mediator/Behaviors/ValidationBehavior.cs index 6f79fbb..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,47 +7,6 @@ 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 -{ - public static readonly Type ResultType = typeof(Result); - public 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. - /// - public static readonly ConcurrentDictionary> FailureFactories = new(); - - /// - /// Returns (or compiles and caches) a delegate that invokes Result<T>.Failure(errors) - /// for the closed generic type derived from . - /// - internal static Func GetOrCreateFactory(Type responseType) - { - Type genericType = GenericResultType.MakeGenericType(responseType.GenericTypeArguments[0]); - return 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(); - }); - } -} - public sealed class ValidationBehavior(IEnumerable> validators) : IPipelineBehavior where TRequest : IMessage From aeb7311bbec6b1bf419c7ba6236c62f1ac55ba76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Recep=20=C5=9Een?= Date: Sun, 31 May 2026 13:16:46 +0300 Subject: [PATCH 4/5] fix(mediator): address code review findings in BehaviorCache - Change public fields to internal (convention: internal for non-public APIs) - Add ArgumentException guard in GetOrCreateFactory for non-generic types - Remove redundant MakeGenericType call: use responseType directly as cache key (callers guarantee responseType is already the closed Result generic) Co-Authored-By: Claude Sonnet 4.6 --- .../Behaviors/BehaviorCache.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/CSharpEssentials.Mediator/Behaviors/BehaviorCache.cs b/CSharpEssentials.Mediator/Behaviors/BehaviorCache.cs index 5d09ed9..cffa818 100644 --- a/CSharpEssentials.Mediator/Behaviors/BehaviorCache.cs +++ b/CSharpEssentials.Mediator/Behaviors/BehaviorCache.cs @@ -19,23 +19,29 @@ namespace CSharpEssentials.Mediator; /// internal static class BehaviorCache { - public static readonly Type ResultType = typeof(Result); - public static readonly Type GenericResultType = typeof(Result<>); + 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. /// - public static readonly ConcurrentDictionary> FailureFactories = new(); + internal static readonly ConcurrentDictionary> FailureFactories = new(); /// /// Returns (or compiles and caches) a delegate that invokes Result<T>.Failure(errors) - /// for the closed generic type derived from . + /// for the closed generic . /// + /// + /// Thrown when is not a generic type. + /// Callers must verify before calling. + /// internal static Func GetOrCreateFactory(Type responseType) { - Type genericType = GenericResultType.MakeGenericType(responseType.GenericTypeArguments[0]); - return FailureFactories.GetOrAdd(genericType, static type => + 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}."); From 0d6bbaaec79af042778b4fa3fb71ffb8b2c2cfc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Recep=20=C5=9Een?= Date: Sun, 31 May 2026 13:19:56 +0300 Subject: [PATCH 5/5] docs(mediator): add exception handling to pipeline behavior list in README Co-Authored-By: Claude Sonnet 4.6 --- README.MD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) |