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