1
1
using System . Diagnostics . CodeAnalysis ;
2
-
2
+ using System . Reflection ;
3
+ using System . Text . RegularExpressions ;
3
4
using Cnblogs . Architecture . Ddd . Cqrs . Abstractions ;
4
-
5
5
using Microsoft . AspNetCore . Builder ;
6
6
using Microsoft . AspNetCore . Http ;
7
7
using Microsoft . AspNetCore . Routing ;
8
+ using Microsoft . AspNetCore . Routing . Patterns ;
8
9
9
10
namespace Cnblogs . Architecture . Ddd . Cqrs . AspNetCore ;
10
11
@@ -22,35 +23,34 @@ public static class CqrsRouteMapper
22
23
/// </summary>
23
24
/// <param name="app"><see cref="IApplicationBuilder"/></param>
24
25
/// <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>
25
28
/// <typeparam name="T">The type of the query.</typeparam>
26
29
/// <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>
40
30
/// <example>
31
+ /// The following code:
41
32
/// <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
46
41
/// </code>
47
42
/// </example>
48
- /// <returns></returns>
49
- public static IEndpointConventionBuilder MapCommand < T > (
43
+ public static IEndpointConventionBuilder MapQuery < T > (
50
44
this IEndpointRouteBuilder app ,
51
- [ StringSyntax ( "Route" ) ] string route )
45
+ [ StringSyntax ( "Route" ) ] string route ,
46
+ bool mapNullableRouteParameters = false ,
47
+ string nullRouteParameterPattern = "-" )
52
48
{
53
- return app . MapCommand ( route , ( [ AsParameters ] T command ) => command ) ;
49
+ return app . MapQuery (
50
+ route ,
51
+ ( [ AsParameters ] T query ) => query ,
52
+ mapNullableRouteParameters ,
53
+ nullRouteParameterPattern ) ;
54
54
}
55
55
56
56
/// <summary>
@@ -59,11 +59,28 @@ public static IEndpointConventionBuilder MapCommand<T>(
59
59
/// <param name="app"><see cref="ApplicationBuilder"/></param>
60
60
/// <param name="route">The route template.</param>
61
61
/// <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>
62
64
/// <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>
63
78
public static IEndpointConventionBuilder MapQuery (
64
79
this IEndpointRouteBuilder app ,
65
80
[ StringSyntax ( "Route" ) ] string route ,
66
- Delegate handler )
81
+ Delegate handler ,
82
+ bool mapNullableRouteParameters = false ,
83
+ string nullRouteParameterPattern = "-" )
67
84
{
68
85
var isQuery = handler . Method . ReturnType . GetInterfaces ( ) . Where ( x => x . IsGenericType )
69
86
. Any ( x => QueryTypes . Contains ( x . GetGenericTypeDefinition ( ) ) ) ;
@@ -73,9 +90,69 @@ public static IEndpointConventionBuilder MapQuery(
73
90
"delegate does not return a query, please make sure it returns object that implement IQuery<> or IListQuery<> or interface that inherit from them" ) ;
74
91
}
75
92
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
+
76
131
return app . MapGet ( route , handler ) . AddEndpointFilter < QueryEndpointHandler > ( ) ;
77
132
}
78
133
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
+
79
156
/// <summary>
80
157
/// Map a command API, using different method based on type name prefix.
81
158
/// </summary>
@@ -174,4 +251,18 @@ private static void EnsureDelegateReturnTypeIsCommand(Delegate handler)
174
251
"handler does not return command, check if delegate returns type that implements ICommand<> or ICommand<,>" ) ;
175
252
}
176
253
}
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
+ }
177
268
}
0 commit comments