#nullable enable using System; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Svrnty.CQRS.Abstractions; using Svrnty.CQRS.Abstractions.Discovery; using Svrnty.CQRS.Abstractions.Security; using Svrnty.CQRS.AspNetCore.Abstractions.Attributes; using Svrnty.CQRS.DynamicQuery.Abstractions; using Svrnty.CQRS.DynamicQuery.AspNetCore; using Svrnty.CQRS.DynamicQuery.Discover; using PoweredSoft.DynamicQuery.Core; namespace Svrnty.CQRS.DynamicQuery.MinimalApi; public static class EndpointRouteBuilderExtensions { public static IEndpointRouteBuilder MapSvrntyDynamicQueries(this IEndpointRouteBuilder endpoints, string routePrefix = "api/query") { var queryDiscovery = endpoints.ServiceProvider.GetRequiredService(); var authorizationService = endpoints.ServiceProvider.GetService(); foreach (var queryMeta in queryDiscovery.GetQueries()) { // Only process dynamic queries if (queryMeta.Category != "DynamicQuery") continue; var ignoreAttribute = queryMeta.QueryType.GetCustomAttribute(); if (ignoreAttribute != null) continue; if (queryMeta is not DynamicQueryMeta dynamicQueryMeta) continue; var route = $"{routePrefix}/{queryMeta.LowerCamelCaseName}"; if (dynamicQueryMeta.ParamsType == null) { // DynamicQuery MapDynamicQueryPost(endpoints, route, dynamicQueryMeta, authorizationService); MapDynamicQueryGet(endpoints, route, dynamicQueryMeta, authorizationService); } else { // DynamicQuery MapDynamicQueryWithParamsPost(endpoints, route, dynamicQueryMeta, authorizationService); MapDynamicQueryWithParamsGet(endpoints, route, dynamicQueryMeta, authorizationService); } } return endpoints; } private static void MapDynamicQueryPost( IEndpointRouteBuilder endpoints, string route, DynamicQueryMeta dynamicQueryMeta, IQueryAuthorizationService? authorizationService) { var sourceType = dynamicQueryMeta.SourceType; var destinationType = dynamicQueryMeta.DestinationType; var queryType = typeof(IDynamicQuery<,>).MakeGenericType(sourceType, destinationType); var resultType = typeof(IQueryExecutionResult<>).MakeGenericType(destinationType); var handlerType = typeof(IQueryHandler<,>).MakeGenericType(queryType, resultType); var requestBodyType = typeof(DynamicQuery<,>).MakeGenericType(sourceType, destinationType); // Create a delegate that properly handles the typed deserialization var mapPostMethod = typeof(EndpointRouteBuilderExtensions) .GetMethod(nameof(MapDynamicQueryPostTyped), BindingFlags.NonPublic | BindingFlags.Static)! .MakeGenericMethod(sourceType, destinationType); var endpoint = (RouteHandlerBuilder)mapPostMethod.Invoke(null, [endpoints, route, queryType, handlerType, authorizationService])!; endpoint .WithName($"DynamicQuery_{dynamicQueryMeta.LowerCamelCaseName}_Post") .WithTags("DynamicQueries") .Accepts(requestBodyType, "application/json") .Produces(200, resultType) .Produces(400) .Produces(401) .Produces(403); } private static RouteHandlerBuilder MapDynamicQueryPostTyped( IEndpointRouteBuilder endpoints, string route, Type queryType, Type handlerType, IQueryAuthorizationService? authorizationService) where TSource : class where TDestination : class { return endpoints.MapPost(route, async ( DynamicQuery query, HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) => { if (authorizationService != null) { var authorizationResult = await authorizationService.IsAllowedAsync(queryType, cancellationToken); if (authorizationResult == AuthorizationResult.Forbidden) return Results.StatusCode(403); if (authorizationResult == AuthorizationResult.Unauthorized) return Results.Unauthorized(); } var handler = serviceProvider.GetRequiredService(handlerType); var handleMethod = handlerType.GetMethod("HandleAsync"); if (handleMethod == null) return Results.Problem("Handler method not found"); 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); }); } private static void MapDynamicQueryGet( IEndpointRouteBuilder endpoints, string route, DynamicQueryMeta dynamicQueryMeta, IQueryAuthorizationService? authorizationService) { var sourceType = dynamicQueryMeta.SourceType; var destinationType = dynamicQueryMeta.DestinationType; var queryType = typeof(IDynamicQuery<,>).MakeGenericType(sourceType, destinationType); var resultType = typeof(IQueryExecutionResult<>).MakeGenericType(destinationType); var handlerType = typeof(IQueryHandler<,>).MakeGenericType(queryType, resultType); var requestQueryType = typeof(DynamicQuery<,>).MakeGenericType(sourceType, destinationType); endpoints.MapGet(route, async (HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) => { if (authorizationService != null) { var authorizationResult = await authorizationService.IsAllowedAsync(queryType, cancellationToken); if (authorizationResult == AuthorizationResult.Forbidden) return Results.StatusCode(403); if (authorizationResult == AuthorizationResult.Unauthorized) return Results.Unauthorized(); } var query = Activator.CreateInstance(requestQueryType); if (query == null) return Results.BadRequest("Could not create query instance"); // Bind query string parameters to query object foreach (var property in requestQueryType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (!property.CanWrite) continue; var queryStringValue = context.Request.Query[property.Name].FirstOrDefault(); if (queryStringValue != null) { try { var convertedValue = Convert.ChangeType(queryStringValue, property.PropertyType); property.SetValue(query, convertedValue); } catch { // Skip properties that can't be converted } } } var handler = serviceProvider.GetRequiredService(handlerType); var handleMethod = handlerType.GetMethod("HandleAsync"); if (handleMethod == null) return Results.Problem("Handler method not found"); 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); }) .WithName($"DynamicQuery_{dynamicQueryMeta.LowerCamelCaseName}_Get") .WithTags("DynamicQueries") .Produces(200, resultType) .Produces(400) .Produces(401) .Produces(403); } private static void MapDynamicQueryWithParamsPost( IEndpointRouteBuilder endpoints, string route, DynamicQueryMeta dynamicQueryMeta, IQueryAuthorizationService? authorizationService) { var sourceType = dynamicQueryMeta.SourceType; var destinationType = dynamicQueryMeta.DestinationType; var paramsType = dynamicQueryMeta.ParamsType!; var queryType = typeof(IDynamicQuery<,,>).MakeGenericType(sourceType, destinationType, paramsType); var resultType = typeof(IQueryExecutionResult<>).MakeGenericType(destinationType); var handlerType = typeof(IQueryHandler<,>).MakeGenericType(queryType, resultType); var requestBodyType = typeof(DynamicQuery<,,>).MakeGenericType(sourceType, destinationType, paramsType); var mapPostMethod = typeof(EndpointRouteBuilderExtensions) .GetMethod(nameof(MapDynamicQueryWithParamsPostTyped), BindingFlags.NonPublic | BindingFlags.Static)! .MakeGenericMethod(sourceType, destinationType, paramsType); var endpoint = (RouteHandlerBuilder)mapPostMethod.Invoke(null, [endpoints, route, queryType, handlerType, authorizationService])!; endpoint .WithName($"DynamicQuery_{dynamicQueryMeta.LowerCamelCaseName}_WithParams_Post") .WithTags("DynamicQueries") .Accepts(requestBodyType, "application/json") .Produces(200, resultType) .Produces(400) .Produces(401) .Produces(403); } private static RouteHandlerBuilder MapDynamicQueryWithParamsPostTyped( IEndpointRouteBuilder endpoints, string route, Type queryType, Type handlerType, IQueryAuthorizationService? authorizationService) where TSource : class where TDestination : class where TParams : class { return endpoints.MapPost(route, async ( DynamicQuery query, HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) => { if (authorizationService != null) { var authorizationResult = await authorizationService.IsAllowedAsync(queryType, cancellationToken); if (authorizationResult == AuthorizationResult.Forbidden) return Results.StatusCode(403); if (authorizationResult == AuthorizationResult.Unauthorized) return Results.Unauthorized(); } var handler = serviceProvider.GetRequiredService(handlerType); var handleMethod = handlerType.GetMethod("HandleAsync"); if (handleMethod == null) return Results.Problem("Handler method not found"); 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); }); } private static void MapDynamicQueryWithParamsGet( IEndpointRouteBuilder endpoints, string route, DynamicQueryMeta dynamicQueryMeta, IQueryAuthorizationService? authorizationService) { var sourceType = dynamicQueryMeta.SourceType; var destinationType = dynamicQueryMeta.DestinationType; var paramsType = dynamicQueryMeta.ParamsType!; var queryType = typeof(IDynamicQuery<,,>).MakeGenericType(sourceType, destinationType, paramsType); var resultType = typeof(IQueryExecutionResult<>).MakeGenericType(destinationType); var handlerType = typeof(IQueryHandler<,>).MakeGenericType(queryType, resultType); var requestQueryType = typeof(DynamicQuery<,,>).MakeGenericType(sourceType, destinationType, paramsType); endpoints.MapGet(route, async (HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) => { if (authorizationService != null) { var authorizationResult = await authorizationService.IsAllowedAsync(queryType, cancellationToken); if (authorizationResult == AuthorizationResult.Forbidden) return Results.StatusCode(403); if (authorizationResult == AuthorizationResult.Unauthorized) return Results.Unauthorized(); } var query = Activator.CreateInstance(requestQueryType); if (query == null) return Results.BadRequest("Could not create query instance"); // Bind query string parameters to query object foreach (var property in requestQueryType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (!property.CanWrite) continue; var queryStringValue = context.Request.Query[property.Name].FirstOrDefault(); if (queryStringValue != null) { try { var convertedValue = Convert.ChangeType(queryStringValue, property.PropertyType); property.SetValue(query, convertedValue); } catch { // Skip properties that can't be converted } } } var handler = serviceProvider.GetRequiredService(handlerType); var handleMethod = handlerType.GetMethod("HandleAsync"); if (handleMethod == null) return Results.Problem("Handler method not found"); 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); }) .WithName($"DynamicQuery_{dynamicQueryMeta.LowerCamelCaseName}_WithParams_Get") .WithTags("DynamicQueries") .Produces(200, resultType) .Produces(400) .Produces(401) .Produces(403); } }