From 095f5327757aa4d7e090da428f609cc43b85629c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= <ikesnowy@outlook.com> Date: Thu, 16 May 2024 16:09:51 +0800 Subject: [PATCH 1/2] feat: support async query&command creation on minimal api mapping --- .../CqrsRouteMapper.cs | 75 ++++++++++++++----- ....Architecture.Ddd.Cqrs.ServiceAgent.csproj | 2 +- ...dd.Infrastructure.Dapper.Clickhouse.csproj | 2 +- ....Ddd.Infrastructure.EntityFramework.csproj | 2 +- ...Architecture.IntegrationTestProject.csproj | 2 +- .../Program.cs | 15 +++- ...blogs.Architecture.IntegrationTests.csproj | 2 +- .../Cnblogs.Architecture.UnitTests.csproj | 2 +- 8 files changed, 74 insertions(+), 28 deletions(-) diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsRouteMapper.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsRouteMapper.cs index 5b4ad3a..ab233a3 100644 --- a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsRouteMapper.cs +++ b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsRouteMapper.cs @@ -21,11 +21,29 @@ public static class CqrsRouteMapper private static readonly string[] GetAndHeadMethods = { "GET", "HEAD" }; - private static readonly List<string> PostCommandPrefixes = new() { "Create", "Add", "New" }; + private static readonly List<string> PostCommandPrefixes = new() + { + "Create", + "Add", + "New" + }; - private static readonly List<string> PutCommandPrefixes = new() { "Update", "Modify", "Replace", "Alter" }; + private static readonly List<string> PutCommandPrefixes = new() + { + "Update", + "Modify", + "Replace", + "Alter" + }; - private static readonly List<string> DeleteCommandPrefixes = new() { "Delete", "Remove", "Clean", "Clear", "Purge" }; + private static readonly List<string> DeleteCommandPrefixes = new() + { + "Delete", + "Remove", + "Clean", + "Clear", + "Purge" + }; /// <summary> /// Map a query API, using GET method. <typeparamref name="T"/> would been constructed from route and query string. @@ -96,14 +114,7 @@ public static IEndpointConventionBuilder MapQuery( string nullRouteParameterPattern = "-", bool enableHead = false) { - var isQuery = handler.Method.ReturnType.GetInterfaces().Where(x => x.IsGenericType) - .Any(x => QueryTypes.Contains(x.GetGenericTypeDefinition())); - if (isQuery == false) - { - throw new ArgumentException( - "delegate does not return a query, please make sure it returns object that implement IQuery<> or IListQuery<> or interface that inherit from them"); - } - + var returnType = EnsureReturnTypeIsQuery(handler); if (mapNullableRouteParameters is MapNullableRouteParameter.Disable) { return MapRoutes(route); @@ -118,7 +129,7 @@ public static IEndpointConventionBuilder MapQuery( var parsedRoute = RoutePatternFactory.Parse(route); var context = new NullabilityInfoContext(); - var nullableRouteProperties = handler.Method.ReturnType.GetProperties() + var nullableRouteProperties = returnType.GetProperties() .Where( p => p.GetMethod != null && p.SetMethod != null @@ -209,8 +220,7 @@ public static IEndpointConventionBuilder MapCommand( [StringSyntax("Route")] string route, Delegate handler) { - EnsureDelegateReturnTypeIsCommand(handler); - var commandTypeName = handler.Method.ReturnType.Name; + var commandTypeName = EnsureReturnTypeIsCommand(handler).Name; if (PostCommandPrefixes.Any(x => commandTypeName.StartsWith(x))) { return app.MapPostCommand(route, handler); @@ -255,7 +265,7 @@ public static IEndpointConventionBuilder MapPostCommand( [StringSyntax("Route")] string route, Delegate handler) { - EnsureDelegateReturnTypeIsCommand(handler); + EnsureReturnTypeIsCommand(handler); return app.MapPost(route, handler).AddEndpointFilter<CommandEndpointHandler>(); } @@ -285,7 +295,7 @@ public static IEndpointConventionBuilder MapPutCommand( [StringSyntax("Route")] string route, Delegate handler) { - EnsureDelegateReturnTypeIsCommand(handler); + EnsureReturnTypeIsCommand(handler); return app.MapPut(route, handler).AddEndpointFilter<CommandEndpointHandler>(); } @@ -315,7 +325,7 @@ public static IEndpointConventionBuilder MapDeleteCommand( [StringSyntax("Route")] string route, Delegate handler) { - EnsureDelegateReturnTypeIsCommand(handler); + EnsureReturnTypeIsCommand(handler); return app.MapDelete(route, handler).AddEndpointFilter<CommandEndpointHandler>(); } @@ -385,15 +395,42 @@ public static IEndpointRouteBuilder StopMappingPrefixToDelete(this IEndpointRout return app; } - private static void EnsureDelegateReturnTypeIsCommand(Delegate handler) + private static Type EnsureReturnTypeIsCommand(Delegate handler) { - var isCommand = handler.Method.ReturnType.GetInterfaces().Where(x => x.IsGenericType) + var returnType = handler.Method.ReturnType; + if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)) + { + returnType = returnType.GenericTypeArguments.First(); + } + + var isCommand = returnType.GetInterfaces().Where(x => x.IsGenericType) .Any(x => CommandTypes.Contains(x.GetGenericTypeDefinition())); if (isCommand == false) { throw new ArgumentException( "handler does not return command, check if delegate returns type that implements ICommand<> or ICommand<,>"); } + + return returnType; + } + + private static Type EnsureReturnTypeIsQuery(Delegate handler) + { + var returnType = handler.Method.ReturnType; + if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)) + { + returnType = returnType.GenericTypeArguments.First(); + } + + var isCommand = returnType.GetInterfaces().Where(x => x.IsGenericType) + .Any(x => QueryTypes.Contains(x.GetGenericTypeDefinition())); + if (isCommand == false) + { + throw new ArgumentException( + "handler does not return query, check if delegate returns type that implements IQuery<>"); + } + + return returnType; } private static List<T[]> GetNotEmptySubsets<T>(ICollection<T> items) diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent.csproj b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent.csproj index c8a0773..53f1d17 100644 --- a/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent.csproj +++ b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent.csproj @@ -9,7 +9,7 @@ </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" /> - <PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.4" /> + <PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.5" /> </ItemGroup> <ItemGroup> <Compile Include="..\Cnblogs.Architecture.Ddd.Cqrs.AspNetCore\CqrsHeaderNames.cs"> diff --git a/src/Cnblogs.Architecture.Ddd.Infrastructure.Dapper.Clickhouse/Cnblogs.Architecture.Ddd.Infrastructure.Dapper.Clickhouse.csproj b/src/Cnblogs.Architecture.Ddd.Infrastructure.Dapper.Clickhouse/Cnblogs.Architecture.Ddd.Infrastructure.Dapper.Clickhouse.csproj index 6cf13b9..c4357b8 100644 --- a/src/Cnblogs.Architecture.Ddd.Infrastructure.Dapper.Clickhouse/Cnblogs.Architecture.Ddd.Infrastructure.Dapper.Clickhouse.csproj +++ b/src/Cnblogs.Architecture.Ddd.Infrastructure.Dapper.Clickhouse/Cnblogs.Architecture.Ddd.Infrastructure.Dapper.Clickhouse.csproj @@ -13,7 +13,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="ClickHouse.Client" Version="7.4.1" /> + <PackageReference Include="ClickHouse.Client" Version="7.5.0" /> </ItemGroup> </Project> diff --git a/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework.csproj b/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework.csproj index f5f841d..47f95bf 100644 --- a/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework.csproj +++ b/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework.csproj @@ -9,7 +9,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.4" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.5" /> </ItemGroup> <ItemGroup> diff --git a/test/Cnblogs.Architecture.IntegrationTestProject/Cnblogs.Architecture.IntegrationTestProject.csproj b/test/Cnblogs.Architecture.IntegrationTestProject/Cnblogs.Architecture.IntegrationTestProject.csproj index 1b9b076..799840f 100644 --- a/test/Cnblogs.Architecture.IntegrationTestProject/Cnblogs.Architecture.IntegrationTestProject.csproj +++ b/test/Cnblogs.Architecture.IntegrationTestProject/Cnblogs.Architecture.IntegrationTestProject.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> - <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> + <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.1" /> </ItemGroup> <ItemGroup> diff --git a/test/Cnblogs.Architecture.IntegrationTestProject/Program.cs b/test/Cnblogs.Architecture.IntegrationTestProject/Program.cs index 3d5f4ed..e62ef74 100644 --- a/test/Cnblogs.Architecture.IntegrationTestProject/Program.cs +++ b/test/Cnblogs.Architecture.IntegrationTestProject/Program.cs @@ -7,6 +7,7 @@ using Cnblogs.Architecture.IntegrationTestProject.Application.Queries; using Cnblogs.Architecture.IntegrationTestProject.Payloads; using Cnblogs.Architecture.TestIntegrationEvents; +using Microsoft.AspNetCore.Mvc; var builder = WebApplication.CreateBuilder(args); @@ -36,10 +37,18 @@ var apis = app.NewVersionedApi(); var v1 = apis.MapGroup("/api/v{version:apiVersion}").HasApiVersion(1); -v1.MapQuery<GetStringQuery>("apps/{appId}/strings/{stringId:int}/value", MapNullableRouteParameter.Enable, enableHead: true); -v1.MapQuery<GetStringQuery>("strings/{id:int}"); +v1.MapQuery<GetStringQuery>( + "apps/{appId}/strings/{stringId:int}/value", + MapNullableRouteParameter.Enable, + enableHead: true); +v1.MapQuery( + "strings/{stringId:int}", + async (int stringId, [FromQuery] bool found) + => await Task.FromResult(new GetStringQuery(StringId: stringId, Found: found))); v1.MapQuery<ListStringsQuery>("strings"); -v1.MapCommand("strings", (CreatePayload payload) => new CreateCommand(payload.NeedError, payload.Data)); +v1.MapCommand( + "strings", + (CreatePayload payload) => Task.FromResult(new CreateCommand(payload.NeedError, payload.Data))); v1.MapCommand( "strings/{id:int}", (int id, UpdatePayload payload) => new UpdateCommand(id, payload.NeedValidationError, payload.NeedExecutionError)); diff --git a/test/Cnblogs.Architecture.IntegrationTests/Cnblogs.Architecture.IntegrationTests.csproj b/test/Cnblogs.Architecture.IntegrationTests/Cnblogs.Architecture.IntegrationTests.csproj index 11f45a0..2b6ad3f 100644 --- a/test/Cnblogs.Architecture.IntegrationTests/Cnblogs.Architecture.IntegrationTests.csproj +++ b/test/Cnblogs.Architecture.IntegrationTests/Cnblogs.Architecture.IntegrationTests.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <PackageReference Include="Cnblogs.Serilog.Extensions" Version="1.1.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.4" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.5" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" /> <PackageReference Include="xunit" Version="2.8.0" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.8.0"> diff --git a/test/Cnblogs.Architecture.UnitTests/Cnblogs.Architecture.UnitTests.csproj b/test/Cnblogs.Architecture.UnitTests/Cnblogs.Architecture.UnitTests.csproj index 6545a31..6db550f 100644 --- a/test/Cnblogs.Architecture.UnitTests/Cnblogs.Architecture.UnitTests.csproj +++ b/test/Cnblogs.Architecture.UnitTests/Cnblogs.Architecture.UnitTests.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.4" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.5" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" /> <PackageReference Include="xunit" Version="2.8.0" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.8.0"> From b3078851f53bbfdd1a72d5c801548d4b8bae25f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= <ikesnowy@outlook.com> Date: Thu, 16 May 2024 16:41:44 +0800 Subject: [PATCH 2/2] fix: add default value for query param --- test/Cnblogs.Architecture.IntegrationTestProject/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Cnblogs.Architecture.IntegrationTestProject/Program.cs b/test/Cnblogs.Architecture.IntegrationTestProject/Program.cs index e62ef74..6c9aa95 100644 --- a/test/Cnblogs.Architecture.IntegrationTestProject/Program.cs +++ b/test/Cnblogs.Architecture.IntegrationTestProject/Program.cs @@ -43,7 +43,7 @@ enableHead: true); v1.MapQuery( "strings/{stringId:int}", - async (int stringId, [FromQuery] bool found) + async (int stringId, [FromQuery] bool found = true) => await Task.FromResult(new GetStringQuery(StringId: stringId, Found: found))); v1.MapQuery<ListStringsQuery>("strings"); v1.MapCommand(