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(