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) | [](https://www.nuget.org/packages/CSharpEssentials.Core) |
| `CSharpEssentials.Enums` | `[StringEnum]` source generator β AOT-safe enumβstring | [π](examples/Examples.Enums/README.md) | [](https://www.nuget.org/packages/CSharpEssentials.Enums) |
| `CSharpEssentials.Rules` | Composable rule engine with `.And()/.Or()/.Linear()/.Next()` | [π](examples/Examples.Rules/README.md) | [](https://www.nuget.org/packages/CSharpEssentials.Rules) |
-| `CSharpEssentials.Mediator` | CQRS pipeline behaviors: validation, logging, caching, transactions | [π](CSharpEssentials.Mediator/Readme.MD) | [](https://www.nuget.org/packages/CSharpEssentials.Mediator) |
+| `CSharpEssentials.Mediator` | CQRS pipeline behaviors: validation, logging, exception handling, caching, transactions | [π](CSharpEssentials.Mediator/Readme.MD) | [](https://www.nuget.org/packages/CSharpEssentials.Mediator) |
| `CSharpEssentials.Entity` | `EntityBase`, soft deletion, domain events | [π](examples/Examples.Entity/README.md) | [](https://www.nuget.org/packages/CSharpEssentials.Entity) |
| `CSharpEssentials.EntityFrameworkCore` | EF Core interceptors (audit, events, slow queries) + pagination | [π](examples/Examples.EntityFrameworkCore/README.md) | [](https://www.nuget.org/packages/CSharpEssentials.EntityFrameworkCore) |
| `CSharpEssentials.AspNetCore` | `GlobalExceptionHandler`, `ResultEndpointFilter`, Swagger versioning | [π](examples/Examples.AspNetCore/README.md) | [](https://www.nuget.org/packages/CSharpEssentials.AspNetCore) |