Skip to content

Commit fc3f941

Browse files
authored
Support OpenAPI summaries, descriptions, and examples for minimal APIs
1 parent d4e70bd commit fc3f941

19 files changed

+665
-11
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Http.Metadata;
5+
6+
/// <summary>
7+
/// Defines a contract used to specify a description in <see cref="Endpoint.Metadata"/>.
8+
/// </summary>
9+
public interface IDescriptionMetadata
10+
{
11+
/// <summary>
12+
/// Gets the description associated with the endpoint.
13+
/// </summary>
14+
string Description { get; }
15+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Http.Metadata;
5+
6+
/// <summary>
7+
/// Defines a contract used to specify an example for a parameter, request body, or response
8+
/// associated with an <see cref="Endpoint"/>.
9+
/// </summary>
10+
public interface IExampleMetadata
11+
{
12+
/// <summary>
13+
/// Gets the summary associated with the example.
14+
/// </summary>
15+
string Summary { get; }
16+
17+
/// <summary>
18+
/// Gets the description associated with the example.
19+
/// </summary>
20+
string Description { get; }
21+
22+
/// <summary>
23+
/// Gets an example value associated with an example.
24+
/// This property is mutually exclusibe with <see cref="ExternalValue"/>.
25+
/// </summary>
26+
object? Value { get; }
27+
28+
/// <summary>
29+
/// Gets a reference to an external value associated with an example.
30+
/// This property is mutually exclusibe with <see cref="Value"/>.
31+
/// </summary>
32+
string? ExternalValue { get; }
33+
34+
/// <summary>
35+
/// If the example targets a parameter, gets
36+
/// or sets the name of the parameter associated with the target.
37+
/// </summary>
38+
string? ParameterName { get; set; }
39+
}

src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,9 @@ public interface IProducesResponseTypeMetadata
2222
/// Gets the content types supported by the metadata.
2323
/// </summary>
2424
IEnumerable<string> ContentTypes { get; }
25+
26+
/// <summary>
27+
/// Gets the description of the response.
28+
/// </summary>
29+
string? Description { get; }
2530
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Http.Metadata;
5+
6+
/// <summary>
7+
/// Defines a contract used to specify a summary in <see cref="Endpoint.Metadata"/>.
8+
/// </summary>
9+
public interface ISummaryMetadata
10+
{
11+
/// <summary>
12+
/// Gets the summary associated with the endpoint.
13+
/// </summary>
14+
string Summary { get; }
15+
}

src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,15 @@ Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata
44
Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string?
55
abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string?
66
Microsoft.AspNetCore.Http.Metadata.ISkipStatusCodePagesMetadata
7+
Microsoft.AspNetCore.Http.Metadata.IDescriptionMetadata
8+
Microsoft.AspNetCore.Http.Metadata.IDescriptionMetadata.Description.get -> string!
9+
Microsoft.AspNetCore.Http.Metadata.IExampleMetadata
10+
Microsoft.AspNetCore.Http.Metadata.IExampleMetadata.Summary.get -> string!
11+
Microsoft.AspNetCore.Http.Metadata.IExampleMetadata.Description.get -> string!
12+
Microsoft.AspNetCore.Http.Metadata.IExampleMetadata.Value.get -> object?
13+
Microsoft.AspNetCore.Http.Metadata.IExampleMetadata.ExternalValue.get -> string?
14+
Microsoft.AspNetCore.Http.Metadata.IExampleMetadata.ParameterName.get -> string?
15+
Microsoft.AspNetCore.Http.Metadata.IExampleMetadata.ParameterName.set -> void
16+
Microsoft.AspNetCore.Http.Metadata.ISummaryMetadata
17+
Microsoft.AspNetCore.Http.Metadata.ISummaryMetadata.Summary.get -> string!
18+
Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata.Description.get -> string?
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Http.Metadata;
5+
6+
namespace Microsoft.AspNetCore.Http;
7+
8+
/// <summary>
9+
/// Specifies a description for the endpoint in <see cref="Endpoint.Metadata"/>.
10+
/// </summary>
11+
/// <remarks>
12+
/// The OpenAPI specification supports a description attribute on operations and parameters that
13+
/// can be used to annotate endpoints with detailed, multiline descriptors of their behavior.
14+
/// behavior.
15+
/// </remarks>
16+
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate | AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
17+
public sealed class DescriptionAttribute : Attribute, IDescriptionMetadata
18+
{
19+
/// <summary>
20+
/// Initializes an instance of the <see cref="DescriptionAttribute"/>.
21+
/// </summary>
22+
/// <param name="description">The description associated with the endpoint or parameter.</param>
23+
public DescriptionAttribute(string description)
24+
{
25+
Description = description;
26+
}
27+
28+
/// <inheritdoc />
29+
public string Description { get; }
30+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Http.Metadata;
5+
6+
namespace Microsoft.AspNetCore.Http;
7+
8+
/// <summary>
9+
/// Specifies an example associated with a parameter, request body, or response of an <see cref="Endpoint"/>.
10+
/// </summary>
11+
/// <remarks>
12+
/// The OpenAPI specification supports an examples property that can be used to annotate
13+
/// request bodies, parameters, and responses with examples of the data type associated
14+
/// with each element.
15+
/// </remarks>
16+
[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
17+
public sealed class ExampleAttribute : Attribute, IExampleMetadata
18+
{
19+
/// <summary>
20+
/// Initializes an instance of the <see cref="ExampleAttribute"/> given
21+
/// a <see cref="Value"/>.
22+
/// </summary>
23+
public ExampleAttribute(string summary, string description, object value)
24+
{
25+
Summary = summary;
26+
Description = description;
27+
Value = value;
28+
}
29+
30+
/// <summary>
31+
/// Initializes an instance of the <see cref="ExampleAttribute"/> given
32+
/// an <see cref="ExternalValue"/>.
33+
/// </summary>
34+
public ExampleAttribute(string summary, string description, string externalValue)
35+
{
36+
Summary = summary;
37+
Description = description;
38+
ExternalValue = externalValue;
39+
}
40+
41+
/// <inheritdoc />
42+
public string Description { get; }
43+
44+
/// <inheritdoc />
45+
public string Summary { get; }
46+
47+
/// <inheritdoc />
48+
public object? Value { get; }
49+
50+
/// <inheritdoc />
51+
public string? ExternalValue { get; }
52+
53+
/// <inheritdoc />
54+
public string? ParameterName { get; set; }
55+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
11
#nullable enable
22
Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions
33
static Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions.ConfigureRouteHandlerJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<Microsoft.AspNetCore.Http.Json.JsonOptions!>! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
4+
Microsoft.AspNetCore.Http.DescriptionAttribute
5+
Microsoft.AspNetCore.Http.DescriptionAttribute.DescriptionAttribute(string! description) -> void
6+
Microsoft.AspNetCore.Http.DescriptionAttribute.Description.get -> string!
7+
Microsoft.AspNetCore.Http.SummaryAttribute
8+
Microsoft.AspNetCore.Http.SummaryAttribute.SummaryAttribute(string! summary) -> void
9+
Microsoft.AspNetCore.Http.SummaryAttribute.Summary.get -> string!
10+
Microsoft.AspNetCore.Http.ExampleAttribute
11+
Microsoft.AspNetCore.Http.ExampleAttribute.ExampleAttribute(string! summary, string! description, object! value) -> void
12+
Microsoft.AspNetCore.Http.ExampleAttribute.ExampleAttribute(string! summary, string! description, string! externalValue) -> void
13+
Microsoft.AspNetCore.Http.ExampleAttribute.Description.get -> string!
14+
Microsoft.AspNetCore.Http.ExampleAttribute.Summary.get -> string!
15+
Microsoft.AspNetCore.Http.ExampleAttribute.ExternalValue.get -> string?
16+
Microsoft.AspNetCore.Http.ExampleAttribute.Value.get -> object?
17+
Microsoft.AspNetCore.Http.ExampleAttribute.ParameterName.get -> string?
18+
Microsoft.AspNetCore.Http.ExampleAttribute.ParameterName.set -> void
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Http.Metadata;
5+
6+
namespace Microsoft.AspNetCore.Http;
7+
8+
/// <summary>
9+
/// Specifies a summary in <see cref="Endpoint.Metadata"/>.
10+
/// </summary>
11+
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate, Inherited = false, AllowMultiple = false)]
12+
public sealed class SummaryAttribute : Attribute, ISummaryMetadata
13+
{
14+
/// <summary>
15+
/// Initializes an instance of the <see cref="SummaryAttribute"/>.
16+
/// </summary>
17+
/// <param name="summary">The summary associated with the endpoint or parameter.</param>
18+
public SummaryAttribute(string summary)
19+
{
20+
Summary = summary;
21+
}
22+
23+
/// <inheritdoc />
24+
public string Summary { get; }
25+
}

src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ public static RouteHandlerBuilder ExcludeFromDescription(this RouteHandlerBuilde
4242
#pragma warning disable RS0026
4343
public static RouteHandlerBuilder Produces<TResponse>(this RouteHandlerBuilder builder,
4444
#pragma warning restore RS0026
45-
int statusCode = StatusCodes.Status200OK,
45+
int statusCode = StatusCodes.Status200OK,
4646
string? contentType = null,
4747
params string[] additionalContentTypes)
4848
{
49-
return Produces(builder, statusCode, typeof(TResponse), contentType, additionalContentTypes);
49+
return Produces(builder, statusCode, null, typeof(TResponse), contentType, additionalContentTypes);
5050
}
5151

5252
/// <summary>
@@ -55,14 +55,16 @@ public static RouteHandlerBuilder Produces<TResponse>(this RouteHandlerBuilder b
5555
/// </summary>
5656
/// <param name="builder">The <see cref="RouteHandlerBuilder"/>.</param>
5757
/// <param name="statusCode">The response status code.</param>
58+
/// <param name="description">The response status code.</param>
5859
/// <param name="responseType">The type of the response. Defaults to null.</param>
5960
/// <param name="contentType">The response content type. Defaults to "application/json" if responseType is not null, otherwise defaults to null.</param>
6061
/// <param name="additionalContentTypes">Additional response content types the endpoint produces for the supplied status code.</param>
6162
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
6263
#pragma warning disable RS0026
6364
public static RouteHandlerBuilder Produces(this RouteHandlerBuilder builder,
6465
#pragma warning restore RS0026
65-
int statusCode,
66+
int statusCode,
67+
string? description = null,
6668
Type? responseType = null,
6769
string? contentType = null,
6870
params string[] additionalContentTypes)
@@ -74,7 +76,13 @@ public static RouteHandlerBuilder Produces(this RouteHandlerBuilder builder,
7476

7577
if (contentType is null)
7678
{
77-
builder.WithMetadata(new ProducesResponseTypeMetadata(responseType ?? typeof(void), statusCode));
79+
builder.WithMetadata(new ProducesResponseTypeMetadata(responseType ?? typeof(void), statusCode, description));
80+
return builder;
81+
}
82+
83+
if (description is not null)
84+
{
85+
builder.WithMetadata(new ProducesResponseTypeMetadata(responseType ?? typeof(void), description, statusCode, contentType, additionalContentTypes));
7886
return builder;
7987
}
8088

@@ -209,6 +217,94 @@ public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder,
209217
return builder;
210218
}
211219

220+
/// <summary>
221+
/// Adds <see cref="IExampleMetadata"/> to <see cref="EndpointBuilder.Metadata"/> representing
222+
/// an example of the parameter for all builders produced by <paramref name="builder"/>.
223+
/// </summary>
224+
/// <param name="builder">The <see cref="RouteHandlerBuilder"/>.</param>
225+
/// <param name="parameterName">A string representing the name of the parameter associated with the example.</param>
226+
/// <param name="summary">A string representing a summary of the example.</param>
227+
/// <param name="description">A string representing a detailed description of the example.</param>
228+
/// <param name="value">An object representing the example associated with a particualr type.</param>
229+
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
230+
public static RouteHandlerBuilder WithParameterExample(this RouteHandlerBuilder builder, string parameterName, string summary, string description, object value)
231+
{
232+
builder.WithMetadata(new ExampleAttribute(summary, description, value) { ParameterName = parameterName });
233+
return builder;
234+
}
235+
236+
/// <summary>
237+
/// Adds <see cref="IExampleMetadata"/> to <see cref="EndpointBuilder.Metadata"/> representing
238+
/// an example of the parameter for all builders produced by <paramref name="builder"/>.
239+
/// </summary>
240+
/// <param name="builder">The <see cref="RouteHandlerBuilder"/>.</param>
241+
/// <param name="parameterName">A string representing the name of the parameter associated with the example.</param>
242+
/// <param name="summary">A string representing a summary of the example.</param>
243+
/// <param name="description">A string representing a detailed description of the example.</param>
244+
/// <param name="externalValue">A string pointing to a reference of the example.</param>
245+
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
246+
public static RouteHandlerBuilder WithParameterExample(this RouteHandlerBuilder builder, string parameterName, string summary, string description, string externalValue)
247+
{
248+
builder.WithMetadata(new ExampleAttribute(summary, description, externalValue) { ParameterName = parameterName });
249+
return builder;
250+
}
251+
252+
/// <summary>
253+
/// Adds <see cref="IExampleMetadata"/> to <see cref="EndpointBuilder.Metadata"/> representing
254+
/// an example of the response type for all builders produced by <paramref name="builder"/>.
255+
/// </summary>
256+
/// <param name="builder">The <see cref="RouteHandlerBuilder"/>.</param>
257+
/// <param name="summary">A string representing a summary of the example.</param>
258+
/// <param name="description">A string representing a detailed description of the example.</param>
259+
/// <param name="value">An object representing the example associated with a particualr type.</param>
260+
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
261+
public static RouteHandlerBuilder WithResponseExample(this RouteHandlerBuilder builder, string summary, string description, object value)
262+
{
263+
builder.WithMetadata(new ExampleAttribute(summary, description, value));
264+
return builder;
265+
}
266+
267+
/// <summary>
268+
/// Adds <see cref="IExampleMetadata"/> to <see cref="EndpointBuilder.Metadata"/> representing
269+
/// an example of the response type for all builders produced by <paramref name="builder"/>.
270+
/// </summary>
271+
/// <param name="builder">The <see cref="RouteHandlerBuilder"/>.</param>
272+
/// <param name="summary">A string representing a summary of the example.</param>
273+
/// <param name="description">A string representing a detailed description of the example.</param>
274+
/// <param name="externalValue">A string pointing to a reference of the example.</param>
275+
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
276+
public static RouteHandlerBuilder WithResponseExample(this RouteHandlerBuilder builder, string summary, string description, string externalValue)
277+
{
278+
builder.WithMetadata(new ExampleAttribute(summary, description, externalValue));
279+
return builder;
280+
}
281+
282+
/// <summary>
283+
/// Adds <see cref="IDescriptionMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all builders
284+
/// produced by <paramref name="builder"/>.
285+
/// </summary>
286+
/// <param name="builder">The <see cref="RouteHandlerBuilder"/>.</param>
287+
/// <param name="description">A string representing a detailed description of the endpoint.</param>
288+
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
289+
public static RouteHandlerBuilder WithDescription(this RouteHandlerBuilder builder, string description)
290+
{
291+
builder.WithMetadata(new DescriptionAttribute(description));
292+
return builder;
293+
}
294+
295+
/// <summary>
296+
/// Adds <see cref="ISummaryMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all builders
297+
/// produced by <paramref name="builder"/>.
298+
/// </summary>
299+
/// <param name="builder">The <see cref="RouteHandlerBuilder"/>.</param>
300+
/// <param name="summary">A string representation a brief description of the endpoint.</param>
301+
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
302+
public static RouteHandlerBuilder WithSummary(this RouteHandlerBuilder builder, string summary)
303+
{
304+
builder.WithMetadata(new SummaryAttribute(summary));
305+
return builder;
306+
}
307+
212308
private static string[] GetAllContentTypes(string contentType, string[] additionalContentTypes)
213309
{
214310
var allContentTypes = new string[additionalContentTypes.Length + 1];

src/Http/Routing/src/PublicAPI.Shipped.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@ static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.Accepts(th
563563
static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.Accepts<TRequest>(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, bool isOptional, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
564564
static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.Accepts<TRequest>(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
565565
static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.ExcludeFromDescription(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
566-
static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.Produces(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, int statusCode, System.Type? responseType = null, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
566+
static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.Produces(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, int statusCode, string? description = null, System.Type? responseType = null, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
567567
static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.Produces<TResponse>(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, int statusCode = 200, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
568568
static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.ProducesProblem(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, int statusCode, string? contentType = null) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
569569
static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.ProducesValidationProblem(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, int statusCode = 400, string? contentType = null) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!

0 commit comments

Comments
 (0)