added dynamic queries for minimal api
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
#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<IQueryDiscovery>();
|
||||
var authorizationService = endpoints.ServiceProvider.GetService<IQueryAuthorizationService>();
|
||||
|
||||
foreach (var queryMeta in queryDiscovery.GetQueries())
|
||||
{
|
||||
// Only process dynamic queries
|
||||
if (queryMeta.Category != "DynamicQuery")
|
||||
continue;
|
||||
|
||||
var ignoreAttribute = queryMeta.QueryType.GetCustomAttribute<QueryControllerIgnoreAttribute>();
|
||||
if (ignoreAttribute != null)
|
||||
continue;
|
||||
|
||||
if (queryMeta is not DynamicQueryMeta dynamicQueryMeta)
|
||||
continue;
|
||||
|
||||
var route = $"{routePrefix}/{queryMeta.LowerCamelCaseName}";
|
||||
|
||||
if (dynamicQueryMeta.ParamsType == null)
|
||||
{
|
||||
// DynamicQuery<TSource, TDestination>
|
||||
MapDynamicQueryPost(endpoints, route, dynamicQueryMeta, authorizationService);
|
||||
MapDynamicQueryGet(endpoints, route, dynamicQueryMeta, authorizationService);
|
||||
}
|
||||
else
|
||||
{
|
||||
// DynamicQuery<TSource, TDestination, TParams>
|
||||
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<TSource, TDestination>(
|
||||
IEndpointRouteBuilder endpoints,
|
||||
string route,
|
||||
Type queryType,
|
||||
Type handlerType,
|
||||
IQueryAuthorizationService? authorizationService)
|
||||
where TSource : class
|
||||
where TDestination : class
|
||||
{
|
||||
return endpoints.MapPost(route, async (
|
||||
DynamicQuery<TSource, TDestination> 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<TSource, TDestination, TParams>(
|
||||
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<TSource, TDestination, TParams> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsAotCompatible>false</IsAotCompatible>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Company>Svrnty</Company>
|
||||
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://github.com/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
||||
<DebugType>portable</DebugType>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<IncludeSource>true</IncludeSource>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\icon.png" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
|
||||
<None Include="..\README.md" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery.Abstractions\Svrnty.CQRS.DynamicQuery.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery\Svrnty.CQRS.DynamicQuery.csproj" />
|
||||
<ProjectReference Include="..\Svrnty.CQRS.DynamicQuery.AspNetCore\Svrnty.CQRS.DynamicQuery.AspNetCore.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user