dotnet-cqrs/Svrnty.CQRS.Abstractions/Discovery/QueryMeta.cs

108 lines
3.9 KiB
C#

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)
{
// 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)
{
var result = await task;
return result;
}
public virtual string Name => _name;
public virtual Type QueryType { get; }
public virtual Type ServiceType { get; }
public virtual Type QueryResultType { get; }
public virtual string Category => "BasicQuery";
public string LowerCamelCaseName
{
get
{
// Use virtual Name property so derived classes can override
var name = Name;
if (string.IsNullOrEmpty(name))
return name;
var firstLetter = char.ToLowerInvariant(name[0]);
return $"{firstLetter}{name[1..]}";
}
}
/// <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; }
}