Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions CSharpEssentials.Mediator/Behaviors/BehaviorCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;

using CSharpEssentials.Errors;
using CSharpEssentials.ResultPattern;

namespace CSharpEssentials.Mediator;

/// <summary>
/// Shared static cache for pipeline behaviors that need to construct
/// <see cref="Result"/> / <see cref="Result{T}"/> failure responses.
/// <para>
/// <see cref="FailureFactories"/> is keyed by closed generic <see cref="Result{T}"/> types.
/// Growth is bounded by the number of distinct <c>Result&lt;T&gt;</c> response types
/// declared across all handlers in the consuming assembly โ€” typically 5โ€“50 entries.
/// Entries are never stale, so eviction is intentionally absent.
/// </para>
/// </summary>
internal static class BehaviorCache
{
internal static readonly Type ResultType = typeof(Result);
internal static readonly Type GenericResultType = typeof(Result<>);

/// <summary>
/// Compiled <c>Result&lt;T&gt;.Failure(errors)</c> factories, keyed by concrete closed generic type.
/// Bounded by the number of distinct <c>Result&lt;T&gt;</c> handler response types in the assembly.
/// </summary>
internal static readonly ConcurrentDictionary<Type, Func<Error[], object>> FailureFactories = new();

/// <summary>
/// Returns (or compiles and caches) a delegate that invokes <c>Result&lt;T&gt;.Failure(errors)</c>
/// for the closed generic <paramref name="responseType"/>.
/// </summary>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="responseType"/> is not a generic type.
/// Callers must verify <see cref="Type.IsGenericType"/> before calling.
/// </exception>
internal static Func<Error[], object> 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<Error>)])
?? 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<Error>));
MethodCallExpression call = Expression.Call(method, asEnumerable);
UnaryExpression boxed = Expression.Convert(call, typeof(object));
return Expression.Lambda<Func<Error[], object>>(boxed, param).Compile();
});
}
}
25 changes: 4 additions & 21 deletions CSharpEssentials.Mediator/Behaviors/ExceptionHandlingBehavior.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
using System.Linq.Expressions;
using System.Reflection;

using Mediator;

using CSharpEssentials.Errors;
Expand Down Expand Up @@ -30,8 +27,8 @@ public sealed class ExceptionHandlingBehavior<TRequest, TResponse>
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<TResponse> Handle(
Expand Down Expand Up @@ -86,23 +83,9 @@ static async ValueTask<TResponse> WrapAsync(ValueTask<TResponse> vt, Type respon
/// </summary>
private static TResponse BuildFailureResponse(Error error, Type responseType)
{
if (responseType == ValidationBehaviorCache.ResultType)
if (responseType == BehaviorCache.ResultType)
return (TResponse)(object)Result.Failure(error);

// Result<T> โ€” use compiled factory cached per concrete closed generic type.
Type genericType = ValidationBehaviorCache.GenericResultType.MakeGenericType(responseType.GenericTypeArguments[0]);

Func<Error[], object> factory = ValidationBehaviorCache.FailureFactories.GetOrAdd(genericType, static type =>
{
MethodInfo method = type.GetMethod(nameof(Result.Failure), [typeof(IEnumerable<Error>)])
?? 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<Error>));
MethodCallExpression call = Expression.Call(method, asEnumerable);
UnaryExpression boxed = Expression.Convert(call, typeof(object));
return Expression.Lambda<Func<Error[], object>>(boxed, param).Compile();
});

return (TResponse)factory([error]);
return (TResponse)BehaviorCache.GetOrCreateFactory(responseType)([error]);
}
}
33 changes: 4 additions & 29 deletions CSharpEssentials.Mediator/Behaviors/ValidationBehavior.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using Mediator;
using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;

using CSharpEssentials.Errors;
using CSharpEssentials.Exceptions;
Expand All @@ -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<Type, Func<Error[], object>> FailureFactories = new();
}

public sealed class ValidationBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IMessage
Expand Down Expand Up @@ -172,32 +162,17 @@ static async ValueTask<Result<TRequest>> WrapAsync(ValueTask<Result<TRequest>> v
/// </summary>
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<Error[], object> factory = ValidationBehaviorCache.FailureFactories.GetOrAdd(genericType, static type =>
{
MethodInfo method = type.GetMethod(nameof(Result.Failure), [typeof(IEnumerable<Error>)])
?? 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<Error>));
MethodCallExpression call = Expression.Call(method, asEnumerable);
UnaryExpression boxed = Expression.Convert(call, typeof(object));
return Expression.Lambda<Func<Error[], object>>(boxed, param).Compile();
});

return (TResponse)factory(errors);
}
private TResponse CreateGenericResultResponse(Error[] errors) =>
(TResponse)BehaviorCache.GetOrCreateFactory(_responseType)(errors);
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public async Task Handle_Should_PassThrough_When_Handler_Returns_GenericResultSu
var behavior = new ExceptionHandlingBehavior<TestExceptionCommand, Result<int>>();
var command = new TestExceptionCommand("test");
MessageHandlerDelegate<TestExceptionCommand, Result<int>> next =
(_, _) => new ValueTask<Result<int>>(Result<int>.Success(42));
(_, _) => new ValueTask<Result<int>>(42);

Result<int> result = await behavior.Handle(command, next, default);

Expand Down Expand Up @@ -129,7 +129,7 @@ public async Task Handle_Should_Propagate_OperationCanceledException_When_Token_
var behavior = new ExceptionHandlingBehavior<TestExceptionCommand, Result>();
var command = new TestExceptionCommand("test");
using var cts = new CancellationTokenSource();
cts.Cancel();
await cts.CancelAsync();

MessageHandlerDelegate<TestExceptionCommand, Result> cancellingNext =
(_, ct) =>
Expand Down
2 changes: 1 addition & 1 deletion README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -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<TId>`, 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) |
Expand Down
Loading