1
+ // Copyright (c) .NET Foundation and contributors. All rights reserved.
2
+
3
+ // NOTE: if the ASP.NET team fixes the design in .NET 7, then this entire file and class should go away
4
+ // REF: https://github.com/dotnet/aspnetcore/issues/39604
5
+ namespace Microsoft . AspNetCore . Http ;
6
+
7
+ using Asp . Versioning ;
8
+ using Asp . Versioning . Builder ;
9
+ using Microsoft . AspNetCore . Builder ;
10
+ using Microsoft . AspNetCore . Http . Metadata ;
11
+ using Microsoft . AspNetCore . Mvc ;
12
+ using Microsoft . AspNetCore . Routing ;
13
+ using static System . Linq . Expressions . Expression ;
14
+
15
+ /// <summary>
16
+ /// Provides API Explorer extension methods for <see cref="IEndpointConventionBuilder"/>.
17
+ /// </summary>
18
+ [ CLSCompliant ( false ) ]
19
+ public static class ApiExplorerBuilderExtensions
20
+ {
21
+ private static ExcludeFromDescriptionAttribute ? excludeFromDescriptionMetadataAttribute ;
22
+ private static Func < Type , int , IProducesResponseTypeMetadata > ? newProducesResponseTypeMetadata2 ;
23
+ private static Func < Type , int , string , string [ ] , IProducesResponseTypeMetadata > ? newProducesResponseTypeMetadata4 ;
24
+ private static Func < Type , bool , string [ ] , IAcceptsMetadata > ? newAcceptsMetadata3 ;
25
+
26
+ /// <summary>
27
+ /// Adds the <see cref="IExcludeFromDescriptionMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all endpoints.
28
+ /// </summary>
29
+ /// <typeparam name="TBuilder">The type of <see cref="IEndpointConventionBuilder"/>.</typeparam>
30
+ /// <param name="builder">The extended <see cref="IEndpointConventionBuilder">endpoint convention builder</see>.</param>
31
+ /// <returns>The original <paramref name="builder"/>.</returns>
32
+ public static TBuilder ExcludeFromDescription < TBuilder > ( this TBuilder builder )
33
+ where TBuilder : IEndpointConventionBuilder
34
+ {
35
+ excludeFromDescriptionMetadataAttribute ??= new ( ) ;
36
+ builder . WithMetadata ( excludeFromDescriptionMetadataAttribute ) ;
37
+ return builder ;
38
+ }
39
+
40
+ /// <summary>
41
+ /// Adds an <see cref="IProducesResponseTypeMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all endpoints.
42
+ /// </summary>
43
+ /// <typeparam name="TBuilder">The type of <see cref="IEndpointConventionBuilder"/>.</typeparam>
44
+ /// <param name="builder">The extended <see cref="IEndpointConventionBuilder">endpoint convention builder</see>.</param>
45
+ /// <param name="build">The <see cref="Action{T}"/> used to build the response metadata.</param>
46
+ /// <param name="statusCode">The response status code. Defaults to <see cref="StatusCodes.Status200OK"/>.</param>
47
+ /// <returns>The original <paramref name="builder"/>.</returns>
48
+ public static TBuilder Produces < TBuilder > (
49
+ this TBuilder builder ,
50
+ Action < ProducesResponseMetadataBuilder < TBuilder > > build ,
51
+ int statusCode = StatusCodes . Status200OK )
52
+ where TBuilder : IEndpointConventionBuilder
53
+ {
54
+ if ( build == null )
55
+ {
56
+ throw new ArgumentNullException ( nameof ( build ) ) ;
57
+ }
58
+
59
+ var metadata = new ProducesResponseMetadataBuilder < TBuilder > ( builder , statusCode ) ;
60
+ build ( metadata ) ;
61
+ metadata . Build ( ) ;
62
+ return builder ;
63
+ }
64
+
65
+ /// <summary>
66
+ /// Adds an <see cref="IProducesResponseTypeMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all endpoints.
67
+ /// </summary>
68
+ /// <typeparam name="TBuilder">The type of <see cref="IEndpointConventionBuilder"/>.</typeparam>
69
+ /// <param name="builder">The extended <see cref="IEndpointConventionBuilder">endpoint convention builder</see>.</param>
70
+ /// <param name="statusCode">The response status code.</param>
71
+ /// <param name="responseType">The type of the response. Defaults to null.</param>
72
+ /// <param name="contentType">The response content type. Defaults to "application/json" if responseType is not null, otherwise defaults to null.</param>
73
+ /// <param name="additionalContentTypes">Additional response content types the endpoint produces for the supplied status code.</param>
74
+ /// <returns>The original <paramref name="builder"/>.</returns>
75
+ public static TBuilder Produces < TBuilder > (
76
+ this TBuilder builder ,
77
+ int statusCode ,
78
+ Type ? responseType = null ,
79
+ string ? contentType = null ,
80
+ params string [ ] additionalContentTypes )
81
+ where TBuilder : IEndpointConventionBuilder
82
+ {
83
+ if ( responseType is not null && string . IsNullOrEmpty ( contentType ) )
84
+ {
85
+ contentType = "application/json" ;
86
+ }
87
+
88
+ responseType ??= typeof ( void ) ;
89
+ IProducesResponseTypeMetadata metadata ;
90
+
91
+ if ( contentType is null )
92
+ {
93
+ newProducesResponseTypeMetadata2 ??= NewProducesResponseTypeMetadataFunc2 ( ) ;
94
+ metadata = newProducesResponseTypeMetadata2 ( responseType , statusCode ) ;
95
+ }
96
+ else
97
+ {
98
+ newProducesResponseTypeMetadata4 ??= NewProducesResponseTypeMetadataFunc4 ( ) ;
99
+ metadata = newProducesResponseTypeMetadata4 ( responseType , statusCode , contentType , additionalContentTypes ) ;
100
+ }
101
+
102
+ builder . WithMetadata ( metadata ) ;
103
+ return builder ;
104
+ }
105
+
106
+ /// <summary>
107
+ /// Adds an <see cref="IProducesResponseTypeMetadata"/> with a <see cref="ProblemDetails"/> type
108
+ /// to <see cref="EndpointBuilder.Metadata"/> for all endpoints.
109
+ /// </summary>
110
+ /// <typeparam name="TBuilder">The type of <see cref="IEndpointConventionBuilder"/>.</typeparam>
111
+ /// <param name="builder">The extended <see cref="IEndpointConventionBuilder">endpoint convention builder</see>.</param>
112
+ /// <param name="statusCode">The response status code.</param>
113
+ /// <param name="contentType">The response content type. Defaults to "application/problem+json".</param>
114
+ /// <returns>The original <paramref name="builder"/>.</returns>
115
+ public static TBuilder ProducesProblem < TBuilder > (
116
+ this TBuilder builder ,
117
+ int statusCode ,
118
+ string ? contentType = null )
119
+ where TBuilder : IEndpointConventionBuilder
120
+ {
121
+ if ( string . IsNullOrEmpty ( contentType ) )
122
+ {
123
+ contentType = ProblemDetailsDefaults . MediaType . Json ;
124
+ }
125
+
126
+ return Produces ( builder , statusCode , typeof ( ProblemDetails ) , contentType ) ;
127
+ }
128
+
129
+ /// <summary>
130
+ /// Adds an <see cref="IProducesResponseTypeMetadata"/> with a <see cref="HttpValidationProblemDetails"/> type
131
+ /// to <see cref="EndpointBuilder.Metadata"/> for all endpoints.
132
+ /// </summary>
133
+ /// <typeparam name="TBuilder">The type of <see cref="IEndpointConventionBuilder"/>.</typeparam>
134
+ /// <param name="builder">The extended <see cref="IEndpointConventionBuilder">endpoint convention builder</see>.</param>
135
+ /// <param name="statusCode">The response status code. Defaults to <see cref="StatusCodes.Status400BadRequest"/>.</param>
136
+ /// <param name="contentType">The response content type. Defaults to "application/problem+json".</param>
137
+ /// <returns>The original <paramref name="builder"/>.</returns>
138
+ public static TBuilder ProducesValidationProblem < TBuilder > (
139
+ this TBuilder builder ,
140
+ int statusCode = StatusCodes . Status400BadRequest ,
141
+ string ? contentType = null )
142
+ where TBuilder : IEndpointConventionBuilder
143
+ {
144
+ if ( string . IsNullOrEmpty ( contentType ) )
145
+ {
146
+ contentType = ProblemDetailsDefaults . MediaType . Json ;
147
+ }
148
+
149
+ return Produces ( builder , statusCode , typeof ( HttpValidationProblemDetails ) , contentType ) ;
150
+ }
151
+
152
+ /// <summary>
153
+ /// Adds the <see cref="ITagsMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all endpoints.
154
+ /// </summary>
155
+ /// <typeparam name="TBuilder">The type of <see cref="IEndpointConventionBuilder"/>.</typeparam>
156
+ /// <param name="builder">The extended <see cref="IEndpointConventionBuilder">endpoint convention builder</see>.</param>
157
+ /// <param name="tags">A collection of tags to be associated with the endpoint.</param>
158
+ /// <returns>The original <paramref name="builder"/>.</returns>
159
+ /// <remarks>When used with OpenAPI, the specification supports a tags classification to categorize
160
+ /// operations into related groups. These tags are typically included in the generated specification
161
+ /// and are typically used to group operations by tags in the UI.</remarks>
162
+ public static TBuilder WithTags < TBuilder > ( this TBuilder builder , params string [ ] tags )
163
+ where TBuilder : IEndpointConventionBuilder => builder . WithMetadata ( new TagsAttribute ( tags ) ) ;
164
+
165
+ /// <summary>
166
+ /// Adds <see cref="IAcceptsMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all endpoints.
167
+ /// </summary>
168
+ /// <typeparam name="TBuilder">The type of <see cref="IEndpointConventionBuilder"/>.</typeparam>
169
+ /// <param name="builder">The extended <see cref="IEndpointConventionBuilder">endpoint convention builder</see>.</param>
170
+ /// <param name="build">The <see cref="Action{T}"/> used to build the request metadata.</param>
171
+ /// <param name="isOptional">Sets a value that determines if the request body is optional.</param>
172
+ /// <returns>The original <paramref name="builder"/>.</returns>
173
+ public static TBuilder Accepts < TBuilder > (
174
+ this TBuilder builder ,
175
+ Action < AcceptsMetadataBuilder < TBuilder > > build ,
176
+ bool isOptional = false )
177
+ where TBuilder : IEndpointConventionBuilder
178
+ {
179
+ if ( build == null )
180
+ {
181
+ throw new ArgumentNullException ( nameof ( build ) ) ;
182
+ }
183
+
184
+ var metadata = new AcceptsMetadataBuilder < TBuilder > ( builder , isOptional ) ;
185
+ build ( metadata ) ;
186
+ metadata . Build ( ) ;
187
+ return builder ;
188
+ }
189
+
190
+ /// <summary>
191
+ /// Adds <see cref="IAcceptsMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all endpoints.
192
+ /// </summary>
193
+ /// <typeparam name="TBuilder">The type of <see cref="IEndpointConventionBuilder"/>.</typeparam>
194
+ /// <param name="builder">The extended <see cref="IEndpointConventionBuilder">endpoint convention builder</see>.</param>
195
+ /// <param name="requestType">The type of the request body.</param>
196
+ /// <param name="contentType">The request content type that the endpoint accepts.</param>
197
+ /// <param name="additionalContentTypes">The list of additional request content types that the endpoint accepts.</param>
198
+ /// <returns>The original <paramref name="builder"/>.</returns>
199
+ public static TBuilder Accepts < TBuilder > (
200
+ this TBuilder builder ,
201
+ Type requestType ,
202
+ string contentType ,
203
+ params string [ ] additionalContentTypes )
204
+ where TBuilder : IEndpointConventionBuilder
205
+ {
206
+ newAcceptsMetadata3 ??= NewAcceptsMetadataFunc3 ( ) ;
207
+
208
+ var allContentTypes = GetAllContentTypes ( contentType , additionalContentTypes ?? Array . Empty < string > ( ) ) ;
209
+ var metadata = newAcceptsMetadata3 ( requestType , false , allContentTypes ) ;
210
+
211
+ return builder . WithMetadata ( metadata ) ;
212
+ }
213
+
214
+ /// <summary>
215
+ /// Adds <see cref="IAcceptsMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all endpoints
216
+ /// produced by <paramref name="builder"/>.
217
+ /// </summary>
218
+ /// <typeparam name="TBuilder">The type of <see cref="IEndpointConventionBuilder"/>.</typeparam>
219
+ /// <param name="builder">The extended <see cref="IEndpointConventionBuilder">endpoint convention builder</see>.</param>
220
+ /// <param name="requestType">The type of the request body.</param>
221
+ /// <param name="isOptional">Sets a value that determines if the request body is optional.</param>
222
+ /// <param name="contentType">The request content type that the endpoint accepts.</param>
223
+ /// <param name="additionalContentTypes">The list of additional request content types that the endpoint accepts.</param>
224
+ /// <returns>The original <paramref name="builder"/>.</returns>
225
+ public static TBuilder Accepts < TBuilder > (
226
+ this TBuilder builder ,
227
+ Type requestType ,
228
+ bool isOptional ,
229
+ string contentType ,
230
+ params string [ ] additionalContentTypes )
231
+ where TBuilder : IEndpointConventionBuilder
232
+ {
233
+ newAcceptsMetadata3 ??= NewAcceptsMetadataFunc3 ( ) ;
234
+
235
+ var allContentTypes = GetAllContentTypes ( contentType , additionalContentTypes ?? Array . Empty < string > ( ) ) ;
236
+ var metadata = newAcceptsMetadata3 ( requestType , isOptional , allContentTypes ) ;
237
+
238
+ return builder . WithMetadata ( metadata ) ;
239
+ }
240
+
241
+ private static string [ ] GetAllContentTypes ( string contentType , string [ ] additionalContentTypes )
242
+ {
243
+ var allContentTypes = new string [ additionalContentTypes . Length + 1 ] ;
244
+ allContentTypes [ 0 ] = contentType ;
245
+
246
+ for ( var i = 0 ; i < additionalContentTypes . Length ; i ++ )
247
+ {
248
+ allContentTypes [ i + 1 ] = additionalContentTypes [ i ] ;
249
+ }
250
+
251
+ return allContentTypes ;
252
+ }
253
+
254
+ // HACK: >_< these are internal types and can't be forked due to internal logic and members
255
+ // REF: https://github.com/dotnet/aspnetcore/blob/main/src/Shared/ApiExplorerTypes/ProducesResponseTypeMetadata.cs
256
+ // REF: https://github.com/dotnet/aspnetcore/blob/main/src/Shared/RoutingMetadata/AcceptsMetadata.cs
257
+ private static class TypeNames
258
+ {
259
+ private const string Assembly = "Microsoft.AspNetCore.Routing" ;
260
+ private const string Namespace = "Microsoft.AspNetCore.Http" ;
261
+
262
+ public const string ProducesResponseTypeMetadata = $ "{ Namespace } .{ nameof ( ProducesResponseTypeMetadata ) } , { Assembly } ";
263
+ public const string AcceptsMetadata = $ "{ Namespace } .Metadata.{ nameof ( AcceptsMetadata ) } , { Assembly } ";
264
+ }
265
+
266
+ private static Func < Type , int , IProducesResponseTypeMetadata > NewProducesResponseTypeMetadataFunc2 ( )
267
+ {
268
+ var @class = Type . GetType ( TypeNames . ProducesResponseTypeMetadata , throwOnError : true , ignoreCase : false ) ! ;
269
+ var type = Parameter ( typeof ( Type ) , "type" ) ;
270
+ var statusCode = Parameter ( typeof ( int ) , "statusCode" ) ;
271
+ var ctor = @class . GetConstructor ( new [ ] { typeof ( Type ) , typeof ( int ) } ) ! ;
272
+ var body = New ( ctor , type , statusCode ) ;
273
+ var lambda = Lambda < Func < Type , int , IProducesResponseTypeMetadata > > ( body , type , statusCode ) ;
274
+
275
+ return lambda . Compile ( ) ;
276
+ }
277
+
278
+ private static Func < Type , int , string , string [ ] , IProducesResponseTypeMetadata > NewProducesResponseTypeMetadataFunc4 ( )
279
+ {
280
+ var @class = Type . GetType ( TypeNames . ProducesResponseTypeMetadata , throwOnError : true , ignoreCase : false ) ! ;
281
+ var type = Parameter ( typeof ( Type ) , "type" ) ;
282
+ var statusCode = Parameter ( typeof ( int ) , "statusCode" ) ;
283
+ var contentType = Parameter ( typeof ( string ) , "contentType" ) ;
284
+ var additionalContentTypes = Parameter ( typeof ( string [ ] ) , "additionalContentTypes" ) ;
285
+ var ctor = @class . GetConstructor ( new [ ] { typeof ( Type ) , typeof ( int ) , typeof ( string ) , typeof ( string [ ] ) } ) ! ;
286
+ var body = New ( ctor , type , statusCode , contentType , additionalContentTypes ) ;
287
+ var lambda = Lambda < Func < Type , int , string , string [ ] , IProducesResponseTypeMetadata > > ( body , type , statusCode , contentType , additionalContentTypes ) ;
288
+
289
+ return lambda . Compile ( ) ;
290
+ }
291
+
292
+ private static Func < Type , bool , string [ ] , IAcceptsMetadata > NewAcceptsMetadataFunc3 ( )
293
+ {
294
+ var @class = Type . GetType ( TypeNames . AcceptsMetadata , throwOnError : true , ignoreCase : false ) ! ;
295
+ var type = Parameter ( typeof ( Type ) , "type" ) ;
296
+ var isOptional = Parameter ( typeof ( bool ) , "isOptional" ) ;
297
+ var contentTypes = Parameter ( typeof ( string [ ] ) , "contentTypes" ) ;
298
+ var ctor = @class . GetConstructor ( new [ ] { typeof ( Type ) , typeof ( bool ) , typeof ( string [ ] ) } ) ! ;
299
+ var body = New ( ctor , type , isOptional , contentTypes ) ;
300
+ var lambda = Lambda < Func < Type , bool , string [ ] , IAcceptsMetadata > > ( body , type , isOptional , contentTypes ) ;
301
+
302
+ return lambda . Compile ( ) ;
303
+ }
304
+ }
0 commit comments