Compare commits
No commits in common. "feature/improvements" and "main" have entirely different histories.
feature/im
...
main
@ -42,8 +42,7 @@
|
||||
"WebFetch(domain:blog.rsuter.com)",
|
||||
"WebFetch(domain:natemcmaster.com)",
|
||||
"WebFetch(domain:www.nuget.org)",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(nul)"
|
||||
"Bash(mkdir:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -3,9 +3,6 @@
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
nul
|
||||
Svrnty.Sample/nul
|
||||
|
||||
.research/
|
||||
|
||||
# User-specific files
|
||||
|
||||
@ -1,132 +1,51 @@
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Svrnty.CQRS.Abstractions.Attributes;
|
||||
|
||||
namespace Svrnty.CQRS.Abstractions.Discovery;
|
||||
|
||||
public sealed class CommandMeta : ICommandMeta
|
||||
{
|
||||
private readonly string _name;
|
||||
private readonly string _lowerCamelCaseName;
|
||||
|
||||
public CommandMeta(Type commandType, Type serviceType, Type commandResultType)
|
||||
{
|
||||
CommandType = commandType;
|
||||
ServiceType = serviceType;
|
||||
CommandResultType = commandResultType;
|
||||
|
||||
// Cache reflection and computed values once in constructor
|
||||
var nameAttribute = commandType.GetCustomAttribute<CommandNameAttribute>();
|
||||
_name = nameAttribute?.Name ?? commandType.Name.Replace("Command", string.Empty);
|
||||
_lowerCamelCaseName = ComputeLowerCamelCaseName(_name);
|
||||
|
||||
// Build compiled delegate for handler invocation
|
||||
CompiledInvoker = BuildCompiledInvoker(serviceType, commandType, commandResultType);
|
||||
}
|
||||
|
||||
public CommandMeta(Type commandType, Type serviceType)
|
||||
{
|
||||
CommandType = commandType;
|
||||
ServiceType = serviceType;
|
||||
|
||||
// Cache reflection and computed values once in constructor
|
||||
var nameAttribute = commandType.GetCustomAttribute<CommandNameAttribute>();
|
||||
_name = nameAttribute?.Name ?? commandType.Name.Replace("Command", string.Empty);
|
||||
_lowerCamelCaseName = ComputeLowerCamelCaseName(_name);
|
||||
|
||||
// Build compiled delegate for handler invocation
|
||||
CompiledInvoker = BuildCompiledInvoker(serviceType, commandType, null);
|
||||
}
|
||||
|
||||
private static string ComputeLowerCamelCaseName(string name)
|
||||
private CommandNameAttribute NameAttribute => CommandType.GetCustomAttribute<CommandNameAttribute>();
|
||||
|
||||
public string Name
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
get
|
||||
{
|
||||
var name = NameAttribute?.Name ?? CommandType.Name.Replace("Command", string.Empty);
|
||||
return name;
|
||||
|
||||
var firstLetter = char.ToLowerInvariant(name[0]);
|
||||
return $"{firstLetter}{name.Substring(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
private static Func<object, object, CancellationToken, Task<object?>> BuildCompiledInvoker(
|
||||
Type serviceType,
|
||||
Type commandType,
|
||||
Type? resultType)
|
||||
{
|
||||
// Parameters: (object handler, object command, CancellationToken ct)
|
||||
var handlerParam = Expression.Parameter(typeof(object), "handler");
|
||||
var commandParam = Expression.Parameter(typeof(object), "command");
|
||||
var ctParam = Expression.Parameter(typeof(CancellationToken), "ct");
|
||||
|
||||
// Cast handler to actual handler type
|
||||
var handlerCast = Expression.Convert(handlerParam, serviceType);
|
||||
|
||||
// Cast command to actual command type
|
||||
var commandCast = Expression.Convert(commandParam, commandType);
|
||||
|
||||
// Get HandleAsync method with correct return type
|
||||
var expectedReturnType = resultType == null ? typeof(Task) : typeof(Task<>).MakeGenericType(resultType);
|
||||
var handleMethod = serviceType.GetMethod("HandleAsync",
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null,
|
||||
new[] { commandType, typeof(CancellationToken) },
|
||||
null);
|
||||
|
||||
if (handleMethod == null || handleMethod.ReturnType != expectedReturnType)
|
||||
throw new InvalidOperationException($"HandleAsync method with return type {expectedReturnType} not found on {serviceType}");
|
||||
|
||||
// Call handler.HandleAsync(command, ct)
|
||||
var methodCall = Expression.Call(handlerCast, handleMethod, commandCast, ctParam);
|
||||
|
||||
Expression invokeCall;
|
||||
if (resultType == null)
|
||||
{
|
||||
// Command without result: Task HandleAsync(...)
|
||||
var asyncHelperMethod = typeof(CommandMeta).GetMethod(nameof(InvokeAndReturnNull), System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)!;
|
||||
invokeCall = Expression.Call(asyncHelperMethod, methodCall);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Command with result: Task<TResult> HandleAsync(...)
|
||||
var asyncHelperMethod = typeof(CommandMeta).GetMethod(nameof(InvokeAndBox), System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)!
|
||||
.MakeGenericMethod(resultType);
|
||||
invokeCall = Expression.Call(asyncHelperMethod, methodCall);
|
||||
}
|
||||
|
||||
// Compile to delegate
|
||||
var lambda = Expression.Lambda<Func<object, object, CancellationToken, Task<object?>>>(
|
||||
invokeCall,
|
||||
handlerParam,
|
||||
commandParam,
|
||||
ctParam);
|
||||
|
||||
return lambda.Compile();
|
||||
}
|
||||
|
||||
private static async Task<object?> InvokeAndReturnNull(Task task)
|
||||
{
|
||||
await task;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static async Task<object?> InvokeAndBox<TResult>(Task<TResult> task)
|
||||
{
|
||||
var result = await task;
|
||||
return result;
|
||||
}
|
||||
|
||||
public string Name => _name;
|
||||
public Type CommandType { get; }
|
||||
public Type ServiceType { get; }
|
||||
public Type CommandResultType { get; }
|
||||
public string LowerCamelCaseName => _lowerCamelCaseName;
|
||||
|
||||
/// <summary>
|
||||
/// Compiled delegate for invoking the handler without reflection.
|
||||
/// Signature: (object handler, object command, CancellationToken ct) => Task<object?>
|
||||
/// </summary>
|
||||
public Func<object, object, CancellationToken, Task<object?>> CompiledInvoker { get; }
|
||||
public string LowerCamelCaseName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(Name))
|
||||
return Name;
|
||||
|
||||
var name = Name;
|
||||
var firstLetter = Char.ToLowerInvariant(name[0]);
|
||||
var ret = $"{firstLetter}{name.Substring(1)}";
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Svrnty.CQRS.Abstractions.Discovery;
|
||||
|
||||
@ -11,11 +9,5 @@ public interface ICommandMeta
|
||||
Type ServiceType { get; }
|
||||
Type CommandResultType { get; }
|
||||
string LowerCamelCaseName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Compiled delegate for invoking the handler without reflection.
|
||||
/// Signature: (object handler, object command, CancellationToken ct) => Task<object?>
|
||||
/// </summary>
|
||||
Func<object, object, CancellationToken, Task<object?>> CompiledInvoker { get; }
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Svrnty.CQRS.Abstractions.Discovery;
|
||||
|
||||
@ -12,10 +10,4 @@ public interface IQueryMeta
|
||||
Type QueryResultType { get; }
|
||||
string Category { get; }
|
||||
string LowerCamelCaseName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Compiled delegate for invoking the handler without reflection.
|
||||
/// Signature: (object handler, object query, CancellationToken ct) => Task<object?>
|
||||
/// </summary>
|
||||
Func<object, object, CancellationToken, Task<object?>> CompiledInvoker { get; }
|
||||
}
|
||||
@ -1,83 +1,28 @@
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Svrnty.CQRS.Abstractions.Attributes;
|
||||
|
||||
namespace Svrnty.CQRS.Abstractions.Discovery;
|
||||
|
||||
public class QueryMeta : IQueryMeta
|
||||
{
|
||||
private readonly string _name;
|
||||
|
||||
public QueryMeta(Type queryType, Type serviceType, Type queryResultType)
|
||||
{
|
||||
QueryType = queryType;
|
||||
ServiceType = serviceType;
|
||||
QueryResultType = queryResultType;
|
||||
|
||||
// Cache reflection and computed value once in constructor
|
||||
var nameAttribute = queryType.GetCustomAttribute<QueryNameAttribute>();
|
||||
_name = nameAttribute?.Name ?? queryType.Name.Replace("Query", string.Empty);
|
||||
|
||||
// Build compiled delegate for handler invocation
|
||||
CompiledInvoker = BuildCompiledInvoker(serviceType, queryType, queryResultType);
|
||||
}
|
||||
|
||||
private static Func<object, object, CancellationToken, Task<object?>> BuildCompiledInvoker(
|
||||
Type serviceType,
|
||||
Type queryType,
|
||||
Type resultType)
|
||||
protected virtual QueryNameAttribute NameAttribute => QueryType.GetCustomAttribute<QueryNameAttribute>();
|
||||
|
||||
public virtual string Name
|
||||
{
|
||||
// Parameters: (object handler, object query, CancellationToken ct)
|
||||
var handlerParam = Expression.Parameter(typeof(object), "handler");
|
||||
var queryParam = Expression.Parameter(typeof(object), "query");
|
||||
var ctParam = Expression.Parameter(typeof(CancellationToken), "ct");
|
||||
|
||||
// Cast handler to actual handler type
|
||||
var handlerCast = Expression.Convert(handlerParam, serviceType);
|
||||
|
||||
// Cast query to actual query type
|
||||
var queryCast = Expression.Convert(queryParam, queryType);
|
||||
|
||||
// Get HandleAsync method with correct return type (queries always return Task<TResult>)
|
||||
var expectedReturnType = typeof(Task<>).MakeGenericType(resultType);
|
||||
var handleMethod = serviceType.GetMethod("HandleAsync",
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null,
|
||||
new[] { queryType, typeof(CancellationToken) },
|
||||
null);
|
||||
|
||||
if (handleMethod == null || handleMethod.ReturnType != expectedReturnType)
|
||||
throw new InvalidOperationException($"HandleAsync method with return type {expectedReturnType} not found on {serviceType}");
|
||||
|
||||
// Call handler.HandleAsync(query, ct) - returns Task<TResult>
|
||||
var methodCall = Expression.Call(handlerCast, handleMethod, queryCast, ctParam);
|
||||
|
||||
// Use helper method to box the result
|
||||
var asyncHelperMethod = typeof(QueryMeta).GetMethod(nameof(InvokeAndBox), System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)!
|
||||
.MakeGenericMethod(resultType);
|
||||
|
||||
var invokeCall = Expression.Call(asyncHelperMethod, methodCall);
|
||||
|
||||
// Compile to delegate
|
||||
var lambda = Expression.Lambda<Func<object, object, CancellationToken, Task<object?>>>(
|
||||
invokeCall,
|
||||
handlerParam,
|
||||
queryParam,
|
||||
ctParam);
|
||||
|
||||
return lambda.Compile();
|
||||
}
|
||||
|
||||
private static async Task<object?> InvokeAndBox<TResult>(Task<TResult> task)
|
||||
get
|
||||
{
|
||||
var result = await task;
|
||||
return result;
|
||||
var name = NameAttribute?.Name ?? QueryType.Name.Replace("Query", string.Empty);
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string Name => _name;
|
||||
|
||||
public virtual Type QueryType { get; }
|
||||
public virtual Type ServiceType { get; }
|
||||
@ -88,20 +33,14 @@ public class QueryMeta : IQueryMeta
|
||||
{
|
||||
get
|
||||
{
|
||||
// Use virtual Name property so derived classes can override
|
||||
if (string.IsNullOrEmpty(Name))
|
||||
return Name;
|
||||
|
||||
var name = Name;
|
||||
if (string.IsNullOrEmpty(name))
|
||||
return name;
|
||||
|
||||
var firstLetter = char.ToLowerInvariant(name[0]);
|
||||
return $"{firstLetter}{name[1..]}";
|
||||
var ret = $"{firstLetter}{name[1..]}";
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compiled delegate for invoking the handler without reflection.
|
||||
/// Signature: (object handler, object query, CancellationToken ct) => Task<object?>
|
||||
/// </summary>
|
||||
public Func<object, object, CancellationToken, Task<object?>> CompiledInvoker { get; }
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Svrnty.CQRS.Abstractions.Discovery;
|
||||
|
||||
@ -31,7 +28,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddTransient<ICommandHandler<TCommand, TCommandResult>, TCommandHandler>();
|
||||
|
||||
// add for discovery purposes.
|
||||
var commandMeta = new CommandMeta(typeof(TCommand), typeof(ICommandHandler<TCommand, TCommandResult>), typeof(TCommandResult));
|
||||
var commandMeta = new CommandMeta(typeof(TCommand), typeof(ICommandHandler<TCommand>), typeof(TCommandResult));
|
||||
services.AddSingleton<ICommandMeta>(commandMeta);
|
||||
|
||||
return services;
|
||||
@ -50,131 +47,4 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans the specified assembly and registers all command handlers found.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection</param>
|
||||
/// <param name="assembly">The assembly to scan for command handlers</param>
|
||||
/// <param name="filter">Optional filter to include/exclude specific handler types</param>
|
||||
/// <param name="lifetime">Service lifetime for handlers (default: Transient)</param>
|
||||
/// <returns>The service collection for chaining</returns>
|
||||
public static IServiceCollection AddCommandsFromAssembly(
|
||||
this IServiceCollection services,
|
||||
Assembly assembly,
|
||||
Func<Type, bool>? filter = null,
|
||||
ServiceLifetime lifetime = ServiceLifetime.Transient)
|
||||
{
|
||||
var commandHandlerInterfaces = assembly.GetTypes()
|
||||
.Where(type => type.IsClass && !type.IsAbstract && !type.IsGenericTypeDefinition)
|
||||
.Where(type => filter == null || filter(type))
|
||||
.SelectMany(handlerType => handlerType.GetInterfaces()
|
||||
.Where(i => i.IsGenericType &&
|
||||
(i.GetGenericTypeDefinition() == typeof(ICommandHandler<>) ||
|
||||
i.GetGenericTypeDefinition() == typeof(ICommandHandler<,>)))
|
||||
.Select(interfaceType => new { HandlerType = handlerType, InterfaceType = interfaceType }))
|
||||
.ToList();
|
||||
|
||||
foreach (var registration in commandHandlerInterfaces)
|
||||
{
|
||||
var genericArgs = registration.InterfaceType.GetGenericArguments();
|
||||
var commandType = genericArgs[0];
|
||||
var handlerType = registration.HandlerType;
|
||||
|
||||
if (genericArgs.Length == 1)
|
||||
{
|
||||
// ICommandHandler<TCommand>
|
||||
RegisterCommandHandler(services, commandType, handlerType, null, lifetime);
|
||||
}
|
||||
else if (genericArgs.Length == 2)
|
||||
{
|
||||
// ICommandHandler<TCommand, TResult>
|
||||
var resultType = genericArgs[1];
|
||||
RegisterCommandHandler(services, commandType, handlerType, resultType, lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans the specified assembly and registers all query handlers found.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection</param>
|
||||
/// <param name="assembly">The assembly to scan for query handlers</param>
|
||||
/// <param name="filter">Optional filter to include/exclude specific handler types</param>
|
||||
/// <param name="lifetime">Service lifetime for handlers (default: Transient)</param>
|
||||
/// <returns>The service collection for chaining</returns>
|
||||
public static IServiceCollection AddQueriesFromAssembly(
|
||||
this IServiceCollection services,
|
||||
Assembly assembly,
|
||||
Func<Type, bool>? filter = null,
|
||||
ServiceLifetime lifetime = ServiceLifetime.Transient)
|
||||
{
|
||||
var queryHandlerInterfaces = assembly.GetTypes()
|
||||
.Where(type => type.IsClass && !type.IsAbstract && !type.IsGenericTypeDefinition)
|
||||
.Where(type => filter == null || filter(type))
|
||||
.SelectMany(handlerType => handlerType.GetInterfaces()
|
||||
.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IQueryHandler<,>))
|
||||
.Select(interfaceType => new { HandlerType = handlerType, InterfaceType = interfaceType }))
|
||||
.ToList();
|
||||
|
||||
foreach (var registration in queryHandlerInterfaces)
|
||||
{
|
||||
var genericArgs = registration.InterfaceType.GetGenericArguments();
|
||||
var queryType = genericArgs[0];
|
||||
var resultType = genericArgs[1];
|
||||
var handlerType = registration.HandlerType;
|
||||
|
||||
RegisterQueryHandler(services, queryType, resultType, handlerType, lifetime);
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static void RegisterCommandHandler(
|
||||
IServiceCollection services,
|
||||
Type commandType,
|
||||
Type handlerType,
|
||||
Type? resultType,
|
||||
ServiceLifetime lifetime)
|
||||
{
|
||||
// Register handler with DI
|
||||
Type serviceType;
|
||||
if (resultType == null)
|
||||
{
|
||||
// ICommandHandler<TCommand>
|
||||
serviceType = typeof(ICommandHandler<>).MakeGenericType(commandType);
|
||||
}
|
||||
else
|
||||
{
|
||||
// ICommandHandler<TCommand, TResult>
|
||||
serviceType = typeof(ICommandHandler<,>).MakeGenericType(commandType, resultType);
|
||||
}
|
||||
|
||||
services.Add(new ServiceDescriptor(serviceType, handlerType, lifetime));
|
||||
|
||||
// Register metadata for discovery
|
||||
var commandMeta = resultType == null
|
||||
? new CommandMeta(commandType, serviceType)
|
||||
: new CommandMeta(commandType, serviceType, resultType);
|
||||
|
||||
services.AddSingleton<ICommandMeta>(commandMeta);
|
||||
}
|
||||
|
||||
private static void RegisterQueryHandler(
|
||||
IServiceCollection services,
|
||||
Type queryType,
|
||||
Type resultType,
|
||||
Type handlerType,
|
||||
ServiceLifetime lifetime)
|
||||
{
|
||||
// Register handler with DI
|
||||
var serviceType = typeof(IQueryHandler<,>).MakeGenericType(queryType, resultType);
|
||||
services.Add(new ServiceDescriptor(serviceType, handlerType, lifetime));
|
||||
|
||||
// Register metadata for discovery
|
||||
var queryMeta = new QueryMeta(queryType, serviceType, resultType);
|
||||
services.AddSingleton<IQueryMeta>(queryMeta);
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using FluentValidation;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Svrnty.CQRS.Abstractions;
|
||||
@ -45,101 +42,4 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans the specified assembly and registers all FluentValidation validators found.
|
||||
/// Supports multiple validators per type (e.g., for composite validation scenarios).
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection</param>
|
||||
/// <param name="assembly">The assembly to scan for validators</param>
|
||||
/// <param name="filter">Optional filter to include/exclude specific validator types</param>
|
||||
/// <param name="lifetime">Service lifetime for validators (default: Transient)</param>
|
||||
/// <returns>The service collection for chaining</returns>
|
||||
public static IServiceCollection AddValidatorsFromAssembly(
|
||||
this IServiceCollection services,
|
||||
Assembly assembly,
|
||||
Func<Type, bool>? filter = null,
|
||||
ServiceLifetime lifetime = ServiceLifetime.Transient)
|
||||
{
|
||||
return AddValidatorsFromAssembly(services, assembly, filter, null, lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans the specified assembly and registers FluentValidation validators for command types only.
|
||||
/// Filters validators to only include those validating types ending with "Command" or registered as command handlers.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection</param>
|
||||
/// <param name="assembly">The assembly to scan for validators</param>
|
||||
/// <param name="filter">Optional additional filter to include/exclude specific validator types</param>
|
||||
/// <param name="lifetime">Service lifetime for validators (default: Transient)</param>
|
||||
/// <returns>The service collection for chaining</returns>
|
||||
public static IServiceCollection AddCommandValidatorsFromAssembly(
|
||||
this IServiceCollection services,
|
||||
Assembly assembly,
|
||||
Func<Type, bool>? filter = null,
|
||||
ServiceLifetime lifetime = ServiceLifetime.Transient)
|
||||
{
|
||||
return AddValidatorsFromAssembly(
|
||||
services,
|
||||
assembly,
|
||||
filter,
|
||||
validatedType => validatedType.Name.EndsWith("Command", StringComparison.Ordinal),
|
||||
lifetime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans the specified assembly and registers FluentValidation validators for query types only.
|
||||
/// Filters validators to only include those validating types ending with "Query" or registered as query handlers.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection</param>
|
||||
/// <param name="assembly">The assembly to scan for validators</param>
|
||||
/// <param name="filter">Optional additional filter to include/exclude specific validator types</param>
|
||||
/// <param name="lifetime">Service lifetime for validators (default: Transient)</param>
|
||||
/// <returns>The service collection for chaining</returns>
|
||||
public static IServiceCollection AddQueryValidatorsFromAssembly(
|
||||
this IServiceCollection services,
|
||||
Assembly assembly,
|
||||
Func<Type, bool>? filter = null,
|
||||
ServiceLifetime lifetime = ServiceLifetime.Transient)
|
||||
{
|
||||
return AddValidatorsFromAssembly(
|
||||
services,
|
||||
assembly,
|
||||
filter,
|
||||
validatedType => validatedType.Name.EndsWith("Query", StringComparison.Ordinal),
|
||||
lifetime);
|
||||
}
|
||||
|
||||
private static IServiceCollection AddValidatorsFromAssembly(
|
||||
IServiceCollection services,
|
||||
Assembly assembly,
|
||||
Func<Type, bool>? validatorTypeFilter,
|
||||
Func<Type, bool>? validatedTypeFilter,
|
||||
ServiceLifetime lifetime)
|
||||
{
|
||||
var validatorInterfaces = assembly.GetTypes()
|
||||
.Where(type => type.IsClass && !type.IsAbstract && !type.IsGenericTypeDefinition)
|
||||
.Where(type => validatorTypeFilter == null || validatorTypeFilter(type))
|
||||
.SelectMany(validatorType => validatorType.GetInterfaces()
|
||||
.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IValidator<>))
|
||||
.Select(interfaceType => new { ValidatorType = validatorType, InterfaceType = interfaceType }))
|
||||
.ToList();
|
||||
|
||||
foreach (var registration in validatorInterfaces)
|
||||
{
|
||||
var validatedType = registration.InterfaceType.GetGenericArguments()[0];
|
||||
|
||||
// Apply validated type filter (for command/query separation)
|
||||
if (validatedTypeFilter != null && !validatedTypeFilter(validatedType))
|
||||
continue;
|
||||
|
||||
var validatorType = registration.ValidatorType;
|
||||
|
||||
// Register the validator
|
||||
var serviceType = typeof(IValidator<>).MakeGenericType(validatedType);
|
||||
services.Add(new ServiceDescriptor(serviceType, validatorType, lifetime));
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@ -27,7 +27,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.AspNetCore" Version="2.71.0" />
|
||||
<PackageReference Include="Grpc.AspNetCore" Version="2.68.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -65,9 +65,15 @@ public static class EndpointRouteBuilderExtensions
|
||||
return Results.BadRequest("Invalid query payload");
|
||||
|
||||
var handler = serviceProvider.GetRequiredService(handlerType);
|
||||
var handleMethod = handlerType.GetMethod("HandleAsync");
|
||||
if (handleMethod == null)
|
||||
return Results.Problem("Handler method not found");
|
||||
|
||||
// Use compiled delegate for zero-reflection invocation
|
||||
var result = await queryMeta.CompiledInvoker(handler, query, cancellationToken);
|
||||
var task = (Task)handleMethod.Invoke(handler, [query, cancellationToken])!;
|
||||
await task;
|
||||
|
||||
var resultProperty = task.GetType().GetProperty("Result");
|
||||
var result = resultProperty?.GetValue(task);
|
||||
|
||||
return Results.Ok(result);
|
||||
})
|
||||
@ -124,9 +130,15 @@ public static class EndpointRouteBuilderExtensions
|
||||
}
|
||||
|
||||
var handler = serviceProvider.GetRequiredService(handlerType);
|
||||
var handleMethod = handlerType.GetMethod("HandleAsync");
|
||||
if (handleMethod == null)
|
||||
return Results.Problem("Handler method not found");
|
||||
|
||||
// Use compiled delegate for zero-reflection invocation
|
||||
var result = await queryMeta.CompiledInvoker(handler, query, cancellationToken);
|
||||
var task = (Task)handleMethod.Invoke(handler, [query, cancellationToken])!;
|
||||
await task;
|
||||
|
||||
var resultProperty = task.GetType().GetProperty("Result");
|
||||
var result = resultProperty?.GetValue(task);
|
||||
|
||||
return Results.Ok(result);
|
||||
})
|
||||
@ -189,9 +201,11 @@ public static class EndpointRouteBuilderExtensions
|
||||
return Results.BadRequest("Invalid command payload");
|
||||
|
||||
var handler = serviceProvider.GetRequiredService(handlerType);
|
||||
var handleMethod = handlerType.GetMethod("HandleAsync");
|
||||
if (handleMethod == null)
|
||||
return Results.Problem("Handler method not found");
|
||||
|
||||
// Use compiled delegate for zero-reflection invocation
|
||||
await commandMeta.CompiledInvoker(handler, command, cancellationToken);
|
||||
await (Task)handleMethod.Invoke(handler, [command, cancellationToken])!;
|
||||
return Results.Ok();
|
||||
})
|
||||
.AddEndpointFilter((IEndpointFilter)Activator.CreateInstance(typeof(ValidationFilter<>).MakeGenericType(commandMeta.CommandType))!)
|
||||
@ -229,9 +243,15 @@ public static class EndpointRouteBuilderExtensions
|
||||
return Results.BadRequest("Invalid command payload");
|
||||
|
||||
var handler = serviceProvider.GetRequiredService(handlerType);
|
||||
var handleMethod = handlerType.GetMethod("HandleAsync");
|
||||
if (handleMethod == null)
|
||||
return Results.Problem("Handler method not found");
|
||||
|
||||
// Use compiled delegate for zero-reflection invocation
|
||||
var result = await commandMeta.CompiledInvoker(handler, command, cancellationToken);
|
||||
var task = (Task)handleMethod.Invoke(handler, [command, cancellationToken])!;
|
||||
await task;
|
||||
|
||||
var resultProperty = task.GetType().GetProperty("Result");
|
||||
var result = resultProperty?.GetValue(task);
|
||||
|
||||
return Results.Ok(result);
|
||||
})
|
||||
|
||||
@ -7,37 +7,16 @@ namespace Svrnty.CQRS.Discovery;
|
||||
|
||||
public sealed class CommandDiscovery : ICommandDiscovery
|
||||
{
|
||||
private readonly List<ICommandMeta> _commandMetas;
|
||||
private readonly Dictionary<string, ICommandMeta> _commandsByName;
|
||||
private readonly Dictionary<Type, ICommandMeta> _commandsByType;
|
||||
private readonly IEnumerable<ICommandMeta> _commandMetas;
|
||||
|
||||
public CommandDiscovery(IEnumerable<ICommandMeta> commandMetas)
|
||||
{
|
||||
// Materialize the enumerable to a list once
|
||||
_commandMetas = commandMetas.ToList();
|
||||
|
||||
// Build lookup dictionaries for O(1) access
|
||||
_commandsByName = new Dictionary<string, ICommandMeta>(_commandMetas.Count);
|
||||
_commandsByType = new Dictionary<Type, ICommandMeta>(_commandMetas.Count);
|
||||
|
||||
foreach (var meta in _commandMetas)
|
||||
{
|
||||
_commandsByName[meta.Name] = meta;
|
||||
_commandsByType[meta.CommandType] = meta;
|
||||
}
|
||||
_commandMetas = commandMetas;
|
||||
}
|
||||
|
||||
public IEnumerable<ICommandMeta> GetCommands() => _commandMetas;
|
||||
|
||||
public ICommandMeta FindCommand(string name) =>
|
||||
_commandsByName.TryGetValue(name, out var meta) ? meta : null;
|
||||
|
||||
public ICommandMeta FindCommand(Type commandType) =>
|
||||
_commandsByType.TryGetValue(commandType, out var meta) ? meta : null;
|
||||
|
||||
public bool CommandExists(string name) =>
|
||||
_commandsByName.ContainsKey(name);
|
||||
|
||||
public bool CommandExists(Type commandType) =>
|
||||
_commandsByType.ContainsKey(commandType);
|
||||
public ICommandMeta FindCommand(string name) => _commandMetas.FirstOrDefault(t => t.Name == name);
|
||||
public ICommandMeta FindCommand(Type commandType) => _commandMetas.FirstOrDefault(t => t.CommandType == commandType);
|
||||
public bool CommandExists(string name) => _commandMetas.Any(t => t.Name == name);
|
||||
public bool CommandExists(Type commandType) => _commandMetas.Any(t => t.CommandType == commandType);
|
||||
}
|
||||
|
||||
@ -7,38 +7,17 @@ namespace Svrnty.CQRS.Discovery;
|
||||
|
||||
public sealed class QueryDiscovery : IQueryDiscovery
|
||||
{
|
||||
private readonly List<IQueryMeta> _queryMetas;
|
||||
private readonly Dictionary<string, IQueryMeta> _queriesByName;
|
||||
private readonly Dictionary<Type, IQueryMeta> _queriesByType;
|
||||
private readonly IEnumerable<IQueryMeta> _queryMetas;
|
||||
|
||||
public QueryDiscovery(IEnumerable<IQueryMeta> queryMetas)
|
||||
{
|
||||
// Materialize the enumerable to a list once
|
||||
_queryMetas = queryMetas.ToList();
|
||||
|
||||
// Build lookup dictionaries for O(1) access
|
||||
_queriesByName = new Dictionary<string, IQueryMeta>(_queryMetas.Count);
|
||||
_queriesByType = new Dictionary<Type, IQueryMeta>(_queryMetas.Count);
|
||||
|
||||
foreach (var meta in _queryMetas)
|
||||
{
|
||||
_queriesByName[meta.Name] = meta;
|
||||
_queriesByType[meta.QueryType] = meta;
|
||||
}
|
||||
_queryMetas = queryMetas;
|
||||
}
|
||||
|
||||
public IEnumerable<IQueryMeta> GetQueries() => _queryMetas;
|
||||
|
||||
public IQueryMeta FindQuery(string name) =>
|
||||
_queriesByName.TryGetValue(name, out var meta) ? meta : null;
|
||||
|
||||
public IQueryMeta FindQuery(Type queryType) =>
|
||||
_queriesByType.TryGetValue(queryType, out var meta) ? meta : null;
|
||||
|
||||
public bool QueryExists(string name) =>
|
||||
_queriesByName.ContainsKey(name);
|
||||
|
||||
public bool QueryExists(Type queryType) =>
|
||||
_queriesByType.ContainsKey(queryType);
|
||||
public IQueryMeta FindQuery(string name) => _queryMetas.FirstOrDefault(t => t.Name == name);
|
||||
public IQueryMeta FindQuery(Type queryType) => _queryMetas.FirstOrDefault(t => t.QueryType == queryType);
|
||||
public bool QueryExists(string name) => _queryMetas.Any(t => t.Name == name);
|
||||
public bool QueryExists(Type queryType) => _queryMetas.Any(t => t.QueryType == queryType);
|
||||
}
|
||||
|
||||
|
||||
@ -22,13 +22,13 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
public static IServiceCollection AddDefaultQueryDiscovery(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddSingleton<IQueryDiscovery, QueryDiscovery>();
|
||||
services.TryAddTransient<IQueryDiscovery, QueryDiscovery>();
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddDefaultCommandDiscovery(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddSingleton<ICommandDiscovery, CommandDiscovery>();
|
||||
services.TryAddTransient<ICommandDiscovery, CommandDiscovery>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,16 +24,10 @@ builder.Services.AddTransient<PoweredSoft.Data.Core.IAsyncQueryableService, Simp
|
||||
builder.Services.AddTransient<PoweredSoft.DynamicQuery.Core.IQueryHandlerAsync, PoweredSoft.DynamicQuery.QueryHandlerAsync>();
|
||||
builder.Services.AddDynamicQueryWithProvider<User, UserQueryableProvider>();
|
||||
|
||||
// Register commands and queries using assembly scanning (new feature!)
|
||||
// This automatically discovers and registers all handlers and validators in the assembly
|
||||
builder.Services.AddCommandsFromAssembly(typeof(Program).Assembly);
|
||||
builder.Services.AddQueriesFromAssembly(typeof(Program).Assembly);
|
||||
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
|
||||
|
||||
// Old manual registration (commented out - now using assembly scanning above)
|
||||
// builder.Services.AddCommand<AddUserCommand, int, AddUserCommandHandler, AddUserCommandValidator>();
|
||||
// builder.Services.AddCommand<RemoveUserCommand, RemoveUserCommandHandler>();
|
||||
// builder.Services.AddQuery<FetchUserQuery, User, FetchUserQueryHandler>();
|
||||
// Register commands and queries with validators
|
||||
builder.Services.AddCommand<AddUserCommand, int, AddUserCommandHandler, AddUserCommandValidator>();
|
||||
builder.Services.AddCommand<RemoveUserCommand, RemoveUserCommandHandler>();
|
||||
builder.Services.AddQuery<FetchUserQuery, User, FetchUserQueryHandler>();
|
||||
|
||||
// Configure CQRS with fluent API
|
||||
builder.Services.AddSvrntyCqrs(cqrs =>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user