Skip to content

Support OpenAPI summaries, descriptions, and examples for minimal APIs #40045

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/Http/Http.Abstractions/src/Metadata/IDescriptionMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Http.Metadata;

/// <summary>
/// Defines a contract used to specify a description in <see cref="Endpoint.Metadata"/>.
/// </summary>
public interface IDescriptionMetadata
{
/// <summary>
/// Gets the description associated with the endpoint.
/// </summary>
string Description { get; }
}
39 changes: 39 additions & 0 deletions src/Http/Http.Abstractions/src/Metadata/IExampleMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Http.Metadata;

/// <summary>
/// Defines a contract used to specify an example for a parameter, request body, or response
/// associated with an <see cref="Endpoint"/>.
/// </summary>
public interface IExampleMetadata
{
/// <summary>
/// Gets the summary associated with the example.
/// </summary>
string Summary { get; }

/// <summary>
/// Gets the description associated with the example.
/// </summary>
string Description { get; }

/// <summary>
/// Gets an example value associated with an example.
/// This property is mutually exclusibe with <see cref="ExternalValue"/>.
/// </summary>
object? Value { get; }

/// <summary>
/// Gets a reference to an external value associated with an example.
/// This property is mutually exclusibe with <see cref="Value"/>.
/// </summary>
string? ExternalValue { get; }

/// <summary>
/// If the example targets a parameter, gets
/// or sets the name of the parameter associated with the target.
/// </summary>
string? ParameterName { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,9 @@ public interface IProducesResponseTypeMetadata
/// Gets the content types supported by the metadata.
/// </summary>
IEnumerable<string> ContentTypes { get; }

/// <summary>
/// Gets the description of the response.
/// </summary>
string? Description { get; }
}
15 changes: 15 additions & 0 deletions src/Http/Http.Abstractions/src/Metadata/ISummaryMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Http.Metadata;

/// <summary>
/// Defines a contract used to specify a summary in <see cref="Endpoint.Metadata"/>.
/// </summary>
public interface ISummaryMetadata
{
/// <summary>
/// Gets the summary associated with the endpoint.
/// </summary>
string Summary { get; }
}
12 changes: 12 additions & 0 deletions src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,15 @@ Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata
Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string?
abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string?
Microsoft.AspNetCore.Http.Metadata.ISkipStatusCodePagesMetadata
Microsoft.AspNetCore.Http.Metadata.IDescriptionMetadata
Microsoft.AspNetCore.Http.Metadata.IDescriptionMetadata.Description.get -> string!
Microsoft.AspNetCore.Http.Metadata.IExampleMetadata
Microsoft.AspNetCore.Http.Metadata.IExampleMetadata.Summary.get -> string!
Microsoft.AspNetCore.Http.Metadata.IExampleMetadata.Description.get -> string!
Microsoft.AspNetCore.Http.Metadata.IExampleMetadata.Value.get -> object?
Microsoft.AspNetCore.Http.Metadata.IExampleMetadata.ExternalValue.get -> string?
Microsoft.AspNetCore.Http.Metadata.IExampleMetadata.ParameterName.get -> string?
Microsoft.AspNetCore.Http.Metadata.IExampleMetadata.ParameterName.set -> void
Microsoft.AspNetCore.Http.Metadata.ISummaryMetadata
Microsoft.AspNetCore.Http.Metadata.ISummaryMetadata.Summary.get -> string!
Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata.Description.get -> string?
30 changes: 30 additions & 0 deletions src/Http/Http.Extensions/src/DescriptionAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http.Metadata;

namespace Microsoft.AspNetCore.Http;

/// <summary>
/// Specifies a description for the endpoint in <see cref="Endpoint.Metadata"/>.
/// </summary>
/// <remarks>
/// The OpenAPI specification supports a description attribute on operations and parameters that
/// can be used to annotate endpoints with detailed, multiline descriptors of their behavior.
/// behavior.
/// </remarks>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate | AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
public sealed class DescriptionAttribute : Attribute, IDescriptionMetadata
{
/// <summary>
/// Initializes an instance of the <see cref="DescriptionAttribute"/>.
/// </summary>
/// <param name="description">The description associated with the endpoint or parameter.</param>
public DescriptionAttribute(string description)
{
Description = description;
}

/// <inheritdoc />
public string Description { get; }
}
55 changes: 55 additions & 0 deletions src/Http/Http.Extensions/src/ExampleAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http.Metadata;

namespace Microsoft.AspNetCore.Http;

/// <summary>
/// Specifies an example associated with a parameter, request body, or response of an <see cref="Endpoint"/>.
/// </summary>
/// <remarks>
/// The OpenAPI specification supports an examples property that can be used to annotate
/// request bodies, parameters, and responses with examples of the data type associated
/// with each element.
/// </remarks>
[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
public sealed class ExampleAttribute : Attribute, IExampleMetadata
{
/// <summary>
/// Initializes an instance of the <see cref="ExampleAttribute"/> given
/// a <see cref="Value"/>.
/// </summary>
public ExampleAttribute(string summary, string description, object value)
{
Summary = summary;
Description = description;
Value = value;
}

/// <summary>
/// Initializes an instance of the <see cref="ExampleAttribute"/> given
/// an <see cref="ExternalValue"/>.
/// </summary>
public ExampleAttribute(string summary, string description, string externalValue)
{
Summary = summary;
Description = description;
ExternalValue = externalValue;
}

/// <inheritdoc />
public string Description { get; }

/// <inheritdoc />
public string Summary { get; }

/// <inheritdoc />
public object? Value { get; }

/// <inheritdoc />
public string? ExternalValue { get; }

/// <inheritdoc />
public string? ParameterName { get; set; }
}
15 changes: 15 additions & 0 deletions src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
#nullable enable
Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions
static Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions.ConfigureRouteHandlerJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<Microsoft.AspNetCore.Http.Json.JsonOptions!>! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
Microsoft.AspNetCore.Http.DescriptionAttribute
Microsoft.AspNetCore.Http.DescriptionAttribute.DescriptionAttribute(string! description) -> void
Microsoft.AspNetCore.Http.DescriptionAttribute.Description.get -> string!
Microsoft.AspNetCore.Http.SummaryAttribute
Microsoft.AspNetCore.Http.SummaryAttribute.SummaryAttribute(string! summary) -> void
Microsoft.AspNetCore.Http.SummaryAttribute.Summary.get -> string!
Microsoft.AspNetCore.Http.ExampleAttribute
Microsoft.AspNetCore.Http.ExampleAttribute.ExampleAttribute(string! summary, string! description, object! value) -> void
Microsoft.AspNetCore.Http.ExampleAttribute.ExampleAttribute(string! summary, string! description, string! externalValue) -> void
Microsoft.AspNetCore.Http.ExampleAttribute.Description.get -> string!
Microsoft.AspNetCore.Http.ExampleAttribute.Summary.get -> string!
Microsoft.AspNetCore.Http.ExampleAttribute.ExternalValue.get -> string?
Microsoft.AspNetCore.Http.ExampleAttribute.Value.get -> object?
Microsoft.AspNetCore.Http.ExampleAttribute.ParameterName.get -> string?
Microsoft.AspNetCore.Http.ExampleAttribute.ParameterName.set -> void
25 changes: 25 additions & 0 deletions src/Http/Http.Extensions/src/SummaryAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http.Metadata;

namespace Microsoft.AspNetCore.Http;

/// <summary>
/// Specifies a summary in <see cref="Endpoint.Metadata"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate, Inherited = false, AllowMultiple = false)]
public sealed class SummaryAttribute : Attribute, ISummaryMetadata
{
/// <summary>
/// Initializes an instance of the <see cref="SummaryAttribute"/>.
/// </summary>
/// <param name="summary">The summary associated with the endpoint or parameter.</param>
public SummaryAttribute(string summary)
{
Summary = summary;
}

/// <inheritdoc />
public string Summary { get; }
}
104 changes: 100 additions & 4 deletions src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ public static RouteHandlerBuilder ExcludeFromDescription(this RouteHandlerBuilde
#pragma warning disable RS0026
public static RouteHandlerBuilder Produces<TResponse>(this RouteHandlerBuilder builder,
#pragma warning restore RS0026
int statusCode = StatusCodes.Status200OK,
int statusCode = StatusCodes.Status200OK,
string? contentType = null,
params string[] additionalContentTypes)
{
return Produces(builder, statusCode, typeof(TResponse), contentType, additionalContentTypes);
return Produces(builder, statusCode, null, typeof(TResponse), contentType, additionalContentTypes);
}

/// <summary>
Expand All @@ -55,14 +55,16 @@ public static RouteHandlerBuilder Produces<TResponse>(this RouteHandlerBuilder b
/// </summary>
/// <param name="builder">The <see cref="RouteHandlerBuilder"/>.</param>
/// <param name="statusCode">The response status code.</param>
/// <param name="description">The response status code.</param>
/// <param name="responseType">The type of the response. Defaults to null.</param>
/// <param name="contentType">The response content type. Defaults to "application/json" if responseType is not null, otherwise defaults to null.</param>
/// <param name="additionalContentTypes">Additional response content types the endpoint produces for the supplied status code.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
#pragma warning disable RS0026
public static RouteHandlerBuilder Produces(this RouteHandlerBuilder builder,
#pragma warning restore RS0026
int statusCode,
int statusCode,
string? description = null,
Type? responseType = null,
string? contentType = null,
params string[] additionalContentTypes)
Expand All @@ -74,7 +76,13 @@ public static RouteHandlerBuilder Produces(this RouteHandlerBuilder builder,

if (contentType is null)
{
builder.WithMetadata(new ProducesResponseTypeMetadata(responseType ?? typeof(void), statusCode));
builder.WithMetadata(new ProducesResponseTypeMetadata(responseType ?? typeof(void), statusCode, description));
return builder;
}

if (description is not null)
{
builder.WithMetadata(new ProducesResponseTypeMetadata(responseType ?? typeof(void), description, statusCode, contentType, additionalContentTypes));
return builder;
}

Expand Down Expand Up @@ -209,6 +217,94 @@ public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder,
return builder;
}

/// <summary>
/// Adds <see cref="IExampleMetadata"/> to <see cref="EndpointBuilder.Metadata"/> representing
/// an example of the parameter for all builders produced by <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="RouteHandlerBuilder"/>.</param>
/// <param name="parameterName">A string representing the name of the parameter associated with the example.</param>
/// <param name="summary">A string representing a summary of the example.</param>
/// <param name="description">A string representing a detailed description of the example.</param>
/// <param name="value">An object representing the example associated with a particualr type.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder WithParameterExample(this RouteHandlerBuilder builder, string parameterName, string summary, string description, object value)
{
builder.WithMetadata(new ExampleAttribute(summary, description, value) { ParameterName = parameterName });
return builder;
}

/// <summary>
/// Adds <see cref="IExampleMetadata"/> to <see cref="EndpointBuilder.Metadata"/> representing
/// an example of the parameter for all builders produced by <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="RouteHandlerBuilder"/>.</param>
/// <param name="parameterName">A string representing the name of the parameter associated with the example.</param>
/// <param name="summary">A string representing a summary of the example.</param>
/// <param name="description">A string representing a detailed description of the example.</param>
/// <param name="externalValue">A string pointing to a reference of the example.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder WithParameterExample(this RouteHandlerBuilder builder, string parameterName, string summary, string description, string externalValue)
{
builder.WithMetadata(new ExampleAttribute(summary, description, externalValue) { ParameterName = parameterName });
return builder;
}

/// <summary>
/// Adds <see cref="IExampleMetadata"/> to <see cref="EndpointBuilder.Metadata"/> representing
/// an example of the response type for all builders produced by <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="RouteHandlerBuilder"/>.</param>
/// <param name="summary">A string representing a summary of the example.</param>
/// <param name="description">A string representing a detailed description of the example.</param>
/// <param name="value">An object representing the example associated with a particualr type.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder WithResponseExample(this RouteHandlerBuilder builder, string summary, string description, object value)
{
builder.WithMetadata(new ExampleAttribute(summary, description, value));
return builder;
}

/// <summary>
/// Adds <see cref="IExampleMetadata"/> to <see cref="EndpointBuilder.Metadata"/> representing
/// an example of the response type for all builders produced by <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="RouteHandlerBuilder"/>.</param>
/// <param name="summary">A string representing a summary of the example.</param>
/// <param name="description">A string representing a detailed description of the example.</param>
/// <param name="externalValue">A string pointing to a reference of the example.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder WithResponseExample(this RouteHandlerBuilder builder, string summary, string description, string externalValue)
{
builder.WithMetadata(new ExampleAttribute(summary, description, externalValue));
return builder;
}

/// <summary>
/// Adds <see cref="IDescriptionMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all builders
/// produced by <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="RouteHandlerBuilder"/>.</param>
/// <param name="description">A string representing a detailed description of the endpoint.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder WithDescription(this RouteHandlerBuilder builder, string description)
{
builder.WithMetadata(new DescriptionAttribute(description));
return builder;
}

/// <summary>
/// Adds <see cref="ISummaryMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all builders
/// produced by <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="RouteHandlerBuilder"/>.</param>
/// <param name="summary">A string representation a brief description of the endpoint.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder WithSummary(this RouteHandlerBuilder builder, string summary)
{
builder.WithMetadata(new SummaryAttribute(summary));
return builder;
}

private static string[] GetAllContentTypes(string contentType, string[] additionalContentTypes)
{
var allContentTypes = new string[additionalContentTypes.Length + 1];
Expand Down
2 changes: 1 addition & 1 deletion src/Http/Routing/src/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.Accepts(th
static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.Accepts<TRequest>(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, bool isOptional, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.Accepts<TRequest>(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.ExcludeFromDescription(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
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!
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!
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!
static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.ProducesProblem(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, int statusCode, string? contentType = null) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.ProducesValidationProblem(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, int statusCode = 400, string? contentType = null) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
Expand Down
Loading