11using System . Diagnostics . CodeAnalysis ;
2-
2+ using System . Reflection ;
3+ using System . Text . RegularExpressions ;
34using Cnblogs . Architecture . Ddd . Cqrs . Abstractions ;
4-
55using Microsoft . AspNetCore . Builder ;
66using Microsoft . AspNetCore . Http ;
77using Microsoft . AspNetCore . Routing ;
8+ using Microsoft . AspNetCore . Routing . Patterns ;
89
910namespace Cnblogs . Architecture . Ddd . Cqrs . AspNetCore ;
1011
@@ -22,35 +23,34 @@ public static class CqrsRouteMapper
2223 /// </summary>
2324 /// <param name="app"><see cref="IApplicationBuilder"/></param>
2425 /// <param name="route">The route template for API.</param>
26+ /// <param name="mapNullableRouteParameters">Multiple routes should be mapped when for nullable route parameters.</param>
27+ /// <param name="nullRouteParameterPattern">Replace route parameter with given string to represent null.</param>
2528 /// <typeparam name="T">The type of the query.</typeparam>
2629 /// <returns></returns>
27- public static IEndpointConventionBuilder MapQuery < T > (
28- this IEndpointRouteBuilder app ,
29- [ StringSyntax ( "Route" ) ] string route )
30- {
31- return app . MapQuery ( route , ( [ AsParameters ] T query ) => query ) ;
32- }
33-
34- /// <summary>
35- /// Map a command API, using different HTTP methods based on prefix. See example for details.
36- /// </summary>
37- /// <param name="app"><see cref="ApplicationBuilder"/></param>
38- /// <param name="route">The route template.</param>
39- /// <typeparam name="T">The type of the command.</typeparam>
4030 /// <example>
31+ /// The following code:
4132 /// <code>
42- /// app.MapCommand<CreateItemCommand>("/items"); // Starts with 'Create' or 'Add' - POST
43- /// app.MapCommand<UpdateItemCommand>("/items/{id:int}") // Starts with 'Update' or 'Replace' - PUT
44- /// app.MapCommand<DeleteCommand>("/items/{id:int}") // Starts with 'Delete' or 'Remove' - DELETE
45- /// app.MapCommand<ResetItemCommand>("/items/{id:int}:reset) // Others - PUT
33+ /// app.MapQuery<ItemQuery>("apps/{appName}/instance/{instanceId}/roles", true);
34+ /// </code>
35+ /// would register following routes:
36+ /// <code>
37+ /// apps/-/instance/-/roles
38+ /// apps/{appName}/instance/-/roles
39+ /// apps/-/instance/{instanceId}/roles
40+ /// apps/{appName}/instance/{instanceId}/roles
4641 /// </code>
4742 /// </example>
48- /// <returns></returns>
49- public static IEndpointConventionBuilder MapCommand < T > (
43+ public static IEndpointConventionBuilder MapQuery < T > (
5044 this IEndpointRouteBuilder app ,
51- [ StringSyntax ( "Route" ) ] string route )
45+ [ StringSyntax ( "Route" ) ] string route ,
46+ bool mapNullableRouteParameters = false ,
47+ string nullRouteParameterPattern = "-" )
5248 {
53- return app . MapCommand ( route , ( [ AsParameters ] T command ) => command ) ;
49+ return app . MapQuery (
50+ route ,
51+ ( [ AsParameters ] T query ) => query ,
52+ mapNullableRouteParameters ,
53+ nullRouteParameterPattern ) ;
5454 }
5555
5656 /// <summary>
@@ -59,11 +59,28 @@ public static IEndpointConventionBuilder MapCommand<T>(
5959 /// <param name="app"><see cref="ApplicationBuilder"/></param>
6060 /// <param name="route">The route template.</param>
6161 /// <param name="handler">The delegate that returns a <see cref="IQuery{TView}"/> instance.</param>
62+ /// <param name="mapNullableRouteParameters">Multiple routes should be mapped when for nullable route parameters.</param>
63+ /// <param name="nullRouteParameterPattern">Replace route parameter with given string to represent null.</param>
6264 /// <returns></returns>
65+ /// <example>
66+ /// The following code:
67+ /// <code>
68+ /// app.MapQuery("apps/{appName}/instance/{instanceId}/roles", (string? appName, string? instanceId) => new ItemQuery(appName, instanceId), true);
69+ /// </code>
70+ /// would register following routes:
71+ /// <code>
72+ /// apps/-/instance/-/roles
73+ /// apps/{appName}/instance/-/roles
74+ /// apps/-/instance/{instanceId}/roles
75+ /// apps/{appName}/instance/{instanceId}/roles
76+ /// </code>
77+ /// </example>
6378 public static IEndpointConventionBuilder MapQuery (
6479 this IEndpointRouteBuilder app ,
6580 [ StringSyntax ( "Route" ) ] string route ,
66- Delegate handler )
81+ Delegate handler ,
82+ bool mapNullableRouteParameters = false ,
83+ string nullRouteParameterPattern = "-" )
6784 {
6885 var isQuery = handler . Method . ReturnType . GetInterfaces ( ) . Where ( x => x . IsGenericType )
6986 . Any ( x => QueryTypes . Contains ( x . GetGenericTypeDefinition ( ) ) ) ;
@@ -73,9 +90,69 @@ public static IEndpointConventionBuilder MapQuery(
7390 "delegate does not return a query, please make sure it returns object that implement IQuery<> or IListQuery<> or interface that inherit from them" ) ;
7491 }
7592
93+ if ( mapNullableRouteParameters == false )
94+ {
95+ return app . MapGet ( route , handler ) . AddEndpointFilter < QueryEndpointHandler > ( ) ;
96+ }
97+
98+ if ( string . IsNullOrWhiteSpace ( nullRouteParameterPattern ) )
99+ {
100+ throw new ArgumentNullException (
101+ nameof ( nullRouteParameterPattern ) ,
102+ "argument must not be null or empty" ) ;
103+ }
104+
105+ var parsedRoute = RoutePatternFactory . Parse ( route ) ;
106+ var context = new NullabilityInfoContext ( ) ;
107+ var nullableRouteProperties = handler . Method . ReturnType . GetProperties ( )
108+ . Where (
109+ p => p . GetMethod != null
110+ && p . SetMethod != null
111+ && context . Create ( p . GetMethod . ReturnParameter ) . ReadState == NullabilityState . Nullable )
112+ . ToList ( ) ;
113+ var nullableRoutePattern = parsedRoute . Parameters
114+ . Where (
115+ x => nullableRouteProperties . Any (
116+ y => string . Equals ( x . Name , y . Name , StringComparison . OrdinalIgnoreCase ) ) )
117+ . ToList ( ) ;
118+ var subsets = GetNotEmptySubsets ( nullableRoutePattern ) ;
119+ foreach ( var subset in subsets )
120+ {
121+ var newRoute = subset . Aggregate (
122+ route ,
123+ ( r , x ) =>
124+ {
125+ var regex = new Regex ( "{" + x . Name + "[^}]*?}" , RegexOptions . IgnoreCase ) ;
126+ return regex . Replace ( r , nullRouteParameterPattern ) ;
127+ } ) ;
128+ app . MapGet ( newRoute , handler ) . AddEndpointFilter < QueryEndpointHandler > ( ) ;
129+ }
130+
76131 return app . MapGet ( route , handler ) . AddEndpointFilter < QueryEndpointHandler > ( ) ;
77132 }
78133
134+ /// <summary>
135+ /// Map a command API, using different HTTP methods based on prefix. See example for details.
136+ /// </summary>
137+ /// <param name="app"><see cref="ApplicationBuilder"/></param>
138+ /// <param name="route">The route template.</param>
139+ /// <typeparam name="T">The type of the command.</typeparam>
140+ /// <example>
141+ /// <code>
142+ /// app.MapCommand<CreateItemCommand>("/items"); // Starts with 'Create' or 'Add' - POST
143+ /// app.MapCommand<UpdateItemCommand>("/items/{id:int}") // Starts with 'Update' or 'Replace' - PUT
144+ /// app.MapCommand<DeleteCommand>("/items/{id:int}") // Starts with 'Delete' or 'Remove' - DELETE
145+ /// app.MapCommand<ResetItemCommand>("/items/{id:int}:reset) // Others - PUT
146+ /// </code>
147+ /// </example>
148+ /// <returns></returns>
149+ public static IEndpointConventionBuilder MapCommand < T > (
150+ this IEndpointRouteBuilder app ,
151+ [ StringSyntax ( "Route" ) ] string route )
152+ {
153+ return app . MapCommand ( route , ( [ AsParameters ] T command ) => command ) ;
154+ }
155+
79156 /// <summary>
80157 /// Map a command API, using different method based on type name prefix.
81158 /// </summary>
@@ -174,4 +251,18 @@ private static void EnsureDelegateReturnTypeIsCommand(Delegate handler)
174251 "handler does not return command, check if delegate returns type that implements ICommand<> or ICommand<,>" ) ;
175252 }
176253 }
254+
255+ private static List < T [ ] > GetNotEmptySubsets < T > ( ICollection < T > items )
256+ {
257+ var subsetCount = 1 << items . Count ;
258+ var results = new List < T [ ] > ( subsetCount ) ;
259+ for ( var i = 1 ; i < subsetCount ; i ++ )
260+ {
261+ var index = i ;
262+ var subset = items . Where ( ( _ , j ) => ( index & ( 1 << j ) ) > 0 ) . ToArray ( ) ;
263+ results . Add ( subset ) ;
264+ }
265+
266+ return results ;
267+ }
177268}
0 commit comments