diff --git a/src/Http/Http.Abstractions/src/Metadata/IDescriptionMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IDescriptionMetadata.cs
new file mode 100644
index 000000000000..0d8fa7b6648e
--- /dev/null
+++ b/src/Http/Http.Abstractions/src/Metadata/IDescriptionMetadata.cs
@@ -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;
+
+///
+/// Defines a contract used to specify a description in .
+///
+public interface IDescriptionMetadata
+{
+ ///
+ /// Gets the description associated with the endpoint.
+ ///
+ string Description { get; }
+}
diff --git a/src/Http/Http.Abstractions/src/Metadata/IExampleMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IExampleMetadata.cs
new file mode 100644
index 000000000000..e7fe3de1d4c8
--- /dev/null
+++ b/src/Http/Http.Abstractions/src/Metadata/IExampleMetadata.cs
@@ -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;
+
+///
+/// Defines a contract used to specify an example for a parameter, request body, or response
+/// associated with an .
+///
+public interface IExampleMetadata
+{
+ ///
+ /// Gets the summary associated with the example.
+ ///
+ string Summary { get; }
+
+ ///
+ /// Gets the description associated with the example.
+ ///
+ string Description { get; }
+
+ ///
+ /// Gets an example value associated with an example.
+ /// This property is mutually exclusibe with .
+ ///
+ object? Value { get; }
+
+ ///
+ /// Gets a reference to an external value associated with an example.
+ /// This property is mutually exclusibe with .
+ ///
+ string? ExternalValue { get; }
+
+ ///
+ /// If the example targets a parameter, gets
+ /// or sets the name of the parameter associated with the target.
+ ///
+ string? ParameterName { get; set; }
+}
diff --git a/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs
index 92bc264271ba..cd9cc52ff732 100644
--- a/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs
+++ b/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs
@@ -22,4 +22,9 @@ public interface IProducesResponseTypeMetadata
/// Gets the content types supported by the metadata.
///
IEnumerable ContentTypes { get; }
+
+ ///
+ /// Gets the description of the response.
+ ///
+ string? Description { get; }
}
diff --git a/src/Http/Http.Abstractions/src/Metadata/ISummaryMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/ISummaryMetadata.cs
new file mode 100644
index 000000000000..e7bdb78a202e
--- /dev/null
+++ b/src/Http/Http.Abstractions/src/Metadata/ISummaryMetadata.cs
@@ -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;
+
+///
+/// Defines a contract used to specify a summary in .
+///
+public interface ISummaryMetadata
+{
+ ///
+ /// Gets the summary associated with the endpoint.
+ ///
+ string Summary { get; }
+}
diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
index 92c400c0bac6..19ce9f1a1480 100644
--- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
@@ -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?
diff --git a/src/Http/Http.Extensions/src/DescriptionAttribute.cs b/src/Http/Http.Extensions/src/DescriptionAttribute.cs
new file mode 100644
index 000000000000..ee840710b578
--- /dev/null
+++ b/src/Http/Http.Extensions/src/DescriptionAttribute.cs
@@ -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;
+
+///
+/// Specifies a description for the endpoint in .
+///
+///
+/// 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.
+///
+[AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate | AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
+public sealed class DescriptionAttribute : Attribute, IDescriptionMetadata
+{
+ ///
+ /// Initializes an instance of the .
+ ///
+ /// The description associated with the endpoint or parameter.
+ public DescriptionAttribute(string description)
+ {
+ Description = description;
+ }
+
+ ///
+ public string Description { get; }
+}
diff --git a/src/Http/Http.Extensions/src/ExampleAttribute.cs b/src/Http/Http.Extensions/src/ExampleAttribute.cs
new file mode 100644
index 000000000000..898fccd7cc59
--- /dev/null
+++ b/src/Http/Http.Extensions/src/ExampleAttribute.cs
@@ -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;
+
+///
+/// Specifies an example associated with a parameter, request body, or response of an .
+///
+///
+/// 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.
+///
+[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
+public sealed class ExampleAttribute : Attribute, IExampleMetadata
+{
+ ///
+ /// Initializes an instance of the given
+ /// a .
+ ///
+ public ExampleAttribute(string summary, string description, object value)
+ {
+ Summary = summary;
+ Description = description;
+ Value = value;
+ }
+
+ ///
+ /// Initializes an instance of the given
+ /// an .
+ ///
+ public ExampleAttribute(string summary, string description, string externalValue)
+ {
+ Summary = summary;
+ Description = description;
+ ExternalValue = externalValue;
+ }
+
+ ///
+ public string Description { get; }
+
+ ///
+ public string Summary { get; }
+
+ ///
+ public object? Value { get; }
+
+ ///
+ public string? ExternalValue { get; }
+
+ ///
+ public string? ParameterName { get; set; }
+}
diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
index 1030d0f0793e..92de37bd91e8 100644
--- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
@@ -1,3 +1,18 @@
#nullable enable
Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions
static Microsoft.Extensions.DependencyInjection.RouteHandlerJsonServiceExtensions.ConfigureRouteHandlerJsonOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! 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
diff --git a/src/Http/Http.Extensions/src/SummaryAttribute.cs b/src/Http/Http.Extensions/src/SummaryAttribute.cs
new file mode 100644
index 000000000000..f32ea74128f5
--- /dev/null
+++ b/src/Http/Http.Extensions/src/SummaryAttribute.cs
@@ -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;
+
+///
+/// Specifies a summary in .
+///
+[AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate, Inherited = false, AllowMultiple = false)]
+public sealed class SummaryAttribute : Attribute, ISummaryMetadata
+{
+ ///
+ /// Initializes an instance of the .
+ ///
+ /// The summary associated with the endpoint or parameter.
+ public SummaryAttribute(string summary)
+ {
+ Summary = summary;
+ }
+
+ ///
+ public string Summary { get; }
+}
diff --git a/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs b/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs
index 31696a3da7be..9ecbe58f8ac5 100644
--- a/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs
+++ b/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs
@@ -42,11 +42,11 @@ public static RouteHandlerBuilder ExcludeFromDescription(this RouteHandlerBuilde
#pragma warning disable RS0026
public static RouteHandlerBuilder Produces(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);
}
///
@@ -55,6 +55,7 @@ public static RouteHandlerBuilder Produces(this RouteHandlerBuilder b
///
/// The .
/// The response status code.
+ /// The response status code.
/// The type of the response. Defaults to null.
/// The response content type. Defaults to "application/json" if responseType is not null, otherwise defaults to null.
/// Additional response content types the endpoint produces for the supplied status code.
@@ -62,7 +63,8 @@ public static RouteHandlerBuilder Produces(this RouteHandlerBuilder b
#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)
@@ -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;
}
@@ -209,6 +217,94 @@ public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder,
return builder;
}
+ ///
+ /// Adds to representing
+ /// an example of the parameter for all builders produced by .
+ ///
+ /// The .
+ /// A string representing the name of the parameter associated with the example.
+ /// A string representing a summary of the example.
+ /// A string representing a detailed description of the example.
+ /// An object representing the example associated with a particualr type.
+ /// A that can be used to further customize the endpoint.
+ 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;
+ }
+
+ ///
+ /// Adds to representing
+ /// an example of the parameter for all builders produced by .
+ ///
+ /// The .
+ /// A string representing the name of the parameter associated with the example.
+ /// A string representing a summary of the example.
+ /// A string representing a detailed description of the example.
+ /// A string pointing to a reference of the example.
+ /// A that can be used to further customize the endpoint.
+ 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;
+ }
+
+ ///
+ /// Adds to representing
+ /// an example of the response type for all builders produced by .
+ ///
+ /// The .
+ /// A string representing a summary of the example.
+ /// A string representing a detailed description of the example.
+ /// An object representing the example associated with a particualr type.
+ /// A that can be used to further customize the endpoint.
+ public static RouteHandlerBuilder WithResponseExample(this RouteHandlerBuilder builder, string summary, string description, object value)
+ {
+ builder.WithMetadata(new ExampleAttribute(summary, description, value));
+ return builder;
+ }
+
+ ///
+ /// Adds to representing
+ /// an example of the response type for all builders produced by .
+ ///
+ /// The .
+ /// A string representing a summary of the example.
+ /// A string representing a detailed description of the example.
+ /// A string pointing to a reference of the example.
+ /// A that can be used to further customize the endpoint.
+ public static RouteHandlerBuilder WithResponseExample(this RouteHandlerBuilder builder, string summary, string description, string externalValue)
+ {
+ builder.WithMetadata(new ExampleAttribute(summary, description, externalValue));
+ return builder;
+ }
+
+ ///
+ /// Adds to for all builders
+ /// produced by .
+ ///
+ /// The .
+ /// A string representing a detailed description of the endpoint.
+ /// A that can be used to further customize the endpoint.
+ public static RouteHandlerBuilder WithDescription(this RouteHandlerBuilder builder, string description)
+ {
+ builder.WithMetadata(new DescriptionAttribute(description));
+ return builder;
+ }
+
+ ///
+ /// Adds to for all builders
+ /// produced by .
+ ///
+ /// The .
+ /// A string representation a brief description of the endpoint.
+ /// A that can be used to further customize the endpoint.
+ 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];
diff --git a/src/Http/Routing/src/PublicAPI.Shipped.txt b/src/Http/Routing/src/PublicAPI.Shipped.txt
index 6d681236b30b..9945a2c3a78a 100644
--- a/src/Http/Routing/src/PublicAPI.Shipped.txt
+++ b/src/Http/Routing/src/PublicAPI.Shipped.txt
@@ -563,7 +563,7 @@ static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.Accepts(th
static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.Accepts(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, bool isOptional, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.Accepts(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(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!
diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt
index ecbe9daba8b5..881edd9bff9a 100644
--- a/src/Http/Routing/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt
@@ -3,3 +3,9 @@ Microsoft.AspNetCore.Routing.RouteOptions.SetParameterPolicy(string! token, Syst
Microsoft.AspNetCore.Routing.RouteOptions.SetParameterPolicy(string! token) -> void
static Microsoft.AspNetCore.Builder.EndpointRouteBuilderExtensions.MapPatch(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! handler) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
static Microsoft.AspNetCore.Builder.EndpointRouteBuilderExtensions.MapPatch(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, Microsoft.AspNetCore.Http.RequestDelegate! requestDelegate) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
+static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.WithResponseExample(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, string! summary, string! description, object! value) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
+static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.WithResponseExample(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, string! summary, string! description, string! externalValue) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
+static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.WithParameterExample(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, string! parameterName, string! summary, string! description, object! value) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
+static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.WithParameterExample(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, string! parameterName, string! summary, string! description, string! externalValue) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
+static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.WithDescription(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, string! description) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
+static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.WithSummary(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, string! summary) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
diff --git a/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiParameterDescription.cs b/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiParameterDescription.cs
index a06e7e5f18c6..fbd4471e394e 100644
--- a/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiParameterDescription.cs
+++ b/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiParameterDescription.cs
@@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Http.Metadata;
namespace Microsoft.AspNetCore.Mvc.ApiExplorer;
@@ -63,4 +64,14 @@ public class ApiParameterDescription
/// Gets or sets the default value for a parameter.
///
public object? DefaultValue { get; set; }
+
+ ///
+ /// Gets or sets a collection of examples associated with the parameter.
+ ///
+ public IEnumerable? Examples { get; set; } = default!;
+
+ ///
+ /// Gets or sets the description associated with the parameter.
+ ///
+ public string? Description { get; set; } = default!;
}
diff --git a/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiResponseType.cs b/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiResponseType.cs
index 71fc8ecf46d9..ff3d150d6954 100644
--- a/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiResponseType.cs
+++ b/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiResponseType.cs
@@ -1,6 +1,7 @@
// 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;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Microsoft.AspNetCore.Mvc.ApiExplorer;
@@ -47,4 +48,14 @@ public class ApiResponseType
/// for communicating error conditions.
///
public bool IsDefaultResponse { get; set; }
+
+ ///
+ /// Gets or sets a collection of elements representing examples for the response type.
+ ///
+ public IEnumerable? Examples { get; set; }
+
+ ///
+ /// Gets or sets the description associated with the response.
+ ///
+ public string? Description { get; set; } = default!;
}
diff --git a/src/Mvc/Mvc.Abstractions/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Abstractions/src/PublicAPI.Unshipped.txt
index 7dc5c58110bf..76d37fcd6f68 100644
--- a/src/Mvc/Mvc.Abstractions/src/PublicAPI.Unshipped.txt
+++ b/src/Mvc/Mvc.Abstractions/src/PublicAPI.Unshipped.txt
@@ -1 +1,9 @@
#nullable enable
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiParameterDescription.Examples.get -> System.Collections.Generic.IEnumerable?
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiParameterDescription.Examples.set -> void
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiResponseType.Examples.get -> System.Collections.Generic.IEnumerable?
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiResponseType.Examples.set -> void
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiParameterDescription.Description.get -> string?
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiParameterDescription.Description.set -> void
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiResponseType.Description.get -> string?
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiResponseType.Description.set -> void
diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs
index 9b22fd7c6e6f..b18533bfdb3c 100644
--- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs
+++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs
@@ -102,7 +102,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
foreach (var parameter in methodInfo.GetParameters())
{
- var parameterDescription = CreateApiParameterDescription(parameter, routeEndpoint.RoutePattern);
+ var parameterDescription = CreateApiParameterDescription(parameter, routeEndpoint);
if (parameterDescription is null)
{
@@ -155,8 +155,9 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
return apiDescription;
}
- private ApiParameterDescription? CreateApiParameterDescription(ParameterInfo parameter, RoutePattern pattern)
+ private ApiParameterDescription? CreateApiParameterDescription(ParameterInfo parameter, RouteEndpoint endpoint)
{
+ var pattern = endpoint.RoutePattern;
var (source, name, allowEmpty, paramType) = GetBindingSourceAndName(parameter, pattern);
// Services are ignored because they are not request parameters.
@@ -171,6 +172,11 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
var isOptional = parameter.HasDefaultValue || nullability.ReadState != NullabilityState.NotNull || allowEmpty;
var parameterDescriptor = CreateParameterDescriptor(parameter);
var routeInfo = CreateParameterRouteInfo(pattern, parameter, isOptional);
+ // Process any value examples for this parameter that are marked as attributes
+ var examplesInAttributes = parameter.GetCustomAttributes().OfType();
+ // Process any example values for this parameter that are registered via an extension method
+ var examplesInMetadata = endpoint.Metadata.OfType().Where(metadata => metadata.ParameterName == name);
+ var parameterDescriptionAttribute = parameter.GetCustomAttribute();
return new ApiParameterDescription
{
@@ -181,7 +187,9 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
Type = parameter.ParameterType,
IsRequired = !isOptional,
ParameterDescriptor = parameterDescriptor,
- RouteInfo = routeInfo
+ RouteInfo = routeInfo,
+ Examples = examplesInMetadata.Concat(examplesInAttributes),
+ Description = parameterDescriptionAttribute?.Description
};
}
@@ -312,6 +320,7 @@ private static void AddSupportedResponseTypes(
// and types added via the extension methods (which implement IProducesResponseTypeMetadata).
var responseProviderMetadata = endpointMetadata.GetOrderedMetadata();
var producesResponseMetadata = endpointMetadata.GetOrderedMetadata();
+ var examplesMetadata = endpointMetadata.GetOrderedMetadata();
var errorMetadata = endpointMetadata.GetMetadata();
var defaultErrorType = errorMetadata?.Type ?? typeof(void);
var contentTypes = new MediaTypeCollection();
@@ -335,6 +344,11 @@ private static void AddSupportedResponseTypes(
apiResponseType.Type = responseType;
}
+ if (examplesMetadata is not null)
+ {
+ apiResponseType.Examples = examplesMetadata.ToArray();
+ }
+
apiResponseType.ModelMetadata = CreateModelMetadata(apiResponseType.Type);
if (contentTypes.Count > 0)
@@ -359,6 +373,11 @@ private static void AddSupportedResponseTypes(
// Set the default response type only when none has already been set explicitly with metadata.
var defaultApiResponseType = CreateDefaultApiResponseType(responseType);
+ if (examplesMetadata is not null)
+ {
+ defaultApiResponseType.Examples = examplesMetadata.ToArray();
+ }
+
if (contentTypes.Count > 0)
{
// If metadata provided us with response formats, use that instead of the default.
@@ -383,6 +402,7 @@ private static Dictionary ReadResponseMetadata(
var apiResponseType = new ApiResponseType
{
Type = metadata.Type,
+ Description = metadata.Description,
StatusCode = statusCode,
};
diff --git a/src/Mvc/Mvc.ApiExplorer/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.ApiExplorer/src/PublicAPI.Unshipped.txt
index 7dc5c58110bf..7b95681f93ad 100644
--- a/src/Mvc/Mvc.ApiExplorer/src/PublicAPI.Unshipped.txt
+++ b/src/Mvc/Mvc.ApiExplorer/src/PublicAPI.Unshipped.txt
@@ -1 +1,9 @@
#nullable enable
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiParameterDescription.Examples.get -> System.Collections.Generic.IEnumerable? (forwarded, contained in Microsoft.AspNetCore.Mvc.Abstractions)
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiParameterDescription.Examples.set -> void (forwarded, contained in Microsoft.AspNetCore.Mvc.Abstractions)
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiResponseType.Examples.get -> System.Collections.Generic.IEnumerable? (forwarded, contained in Microsoft.AspNetCore.Mvc.Abstractions)
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiResponseType.Examples.set -> void (forwarded, contained in Microsoft.AspNetCore.Mvc.Abstractions)
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiParameterDescription.Description.get -> string? (forwarded, contained in Microsoft.AspNetCore.Mvc.Abstractions)
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiParameterDescription.Description.set -> void (forwarded, contained in Microsoft.AspNetCore.Mvc.Abstractions)
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiResponseType.Description.get -> string? (forwarded, contained in Microsoft.AspNetCore.Mvc.Abstractions)
+Microsoft.AspNetCore.Mvc.ApiExplorer.ApiResponseType.Description.set -> void (forwarded, contained in Microsoft.AspNetCore.Mvc.Abstractions)
diff --git a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs
index aef0b7ba2e4b..4f3a13cd29df 100644
--- a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs
+++ b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs
@@ -4,8 +4,10 @@
using System.ComponentModel;
using System.Reflection;
using System.Security.Claims;
+using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
@@ -488,7 +490,7 @@ public void TestParameterIsRequiredForObliviousNullabilityContext()
[Fact]
public void TestParameterAttributesCanBeInspected()
{
- var apiDescription = GetApiDescription(([Description("The name.")] string name) => { });
+ var apiDescription = GetApiDescription(([System.ComponentModel.Description("The name.")] string name) => { });
Assert.Equal(1, apiDescription.ParameterDescriptions.Count);
var nameParam = apiDescription.ParameterDescriptions[0];
@@ -505,7 +507,7 @@ public void TestParameterAttributesCanBeInspected()
Assert.NotNull(descriptor.ParameterInfo);
- var description = Assert.Single(descriptor.ParameterInfo.GetCustomAttributes());
+ var description = Assert.Single(descriptor.ParameterInfo.GetCustomAttributes());
Assert.NotNull(description);
Assert.Equal("The name.", description.Description);
@@ -1121,6 +1123,263 @@ public void HandlesEndpointWithRouteConstraints()
constraint => Assert.IsType(constraint));
}
+ [Fact]
+ public void HandlesEndpointWithDescriptionAndSummary_WithExtensionMethods()
+ {
+ var builder = CreateBuilder();
+ builder.MapGet("/api/todos/{id}", (int id) => "").WithDescription("A description").WithSummary("A summary");
+
+ var context = new ApiDescriptionProviderContext(Array.Empty());
+
+ var endpointDataSource = builder.DataSources.OfType().Single();
+ var hostEnvironment = new HostEnvironment
+ {
+ ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
+ };
+ var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
+
+ // Act
+ provider.OnProvidersExecuting(context);
+
+ // Assert
+ var apiDescription = Assert.Single(context.Results);
+ Assert.NotEmpty(apiDescription.ActionDescriptor.EndpointMetadata);
+
+ var descriptionMetadata = apiDescription.ActionDescriptor.EndpointMetadata.OfType().SingleOrDefault();
+ Assert.NotNull(descriptionMetadata);
+ Assert.Equal("A description", descriptionMetadata.Description);
+
+ var summaryMetadata = apiDescription.ActionDescriptor.EndpointMetadata.OfType().SingleOrDefault();
+ Assert.NotNull(summaryMetadata);
+ Assert.Equal("A summary", summaryMetadata.Summary);
+ }
+
+ [Fact]
+ public void HandlesEndpointWithDescriptionAndSummary_WithAttributes()
+ {
+ var builder = CreateBuilder();
+ builder.MapGet("/api/todos/{id}", [Summary("A summary")] [Http.Description("A description")] (int id) => "");
+
+ var context = new ApiDescriptionProviderContext(Array.Empty());
+
+ var endpointDataSource = builder.DataSources.OfType().Single();
+ var hostEnvironment = new HostEnvironment
+ {
+ ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
+ };
+ var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
+
+ // Act
+ provider.OnProvidersExecuting(context);
+
+ // Assert
+ var apiDescription = Assert.Single(context.Results);
+ Assert.NotEmpty(apiDescription.ActionDescriptor.EndpointMetadata);
+
+ var descriptionMetadata = apiDescription.ActionDescriptor.EndpointMetadata.OfType().SingleOrDefault();
+ Assert.NotNull(descriptionMetadata);
+ Assert.Equal("A description", descriptionMetadata.Description);
+
+ var summaryMetadata = apiDescription.ActionDescriptor.EndpointMetadata.OfType().SingleOrDefault();
+ Assert.NotNull(summaryMetadata);
+ Assert.Equal("A summary", summaryMetadata.Summary);
+ }
+
+ [Fact]
+ public void HandlesResponseWithDescription_ViaExtensionMethod()
+ {
+ var builder = CreateBuilder();
+ builder.MapGet("/api/todos/{id}", (int id) => "").Produces(StatusCodes.Status200OK, "This is a response description", typeof(Todo));
+
+ var context = new ApiDescriptionProviderContext(Array.Empty());
+
+ var endpointDataSource = builder.DataSources.OfType().Single();
+ var hostEnvironment = new HostEnvironment
+ {
+ ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
+ };
+ var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
+
+ // Act
+ provider.OnProvidersExecuting(context);
+
+ // Assert
+ var apiDescription = Assert.Single(context.Results);
+ var apiResponse = Assert.Single(apiDescription.SupportedResponseTypes);
+ Assert.Equal("This is a response description", apiResponse.Description);
+ }
+
+ [Fact]
+ public void HandlesParameterWithDescription_ViaAttributes()
+ {
+ var builder = CreateBuilder();
+ builder.MapGet("/api/todos/{id}", ([Http.Description("A description for the parameter")] int id) => "");
+
+ var context = new ApiDescriptionProviderContext(Array.Empty());
+
+ var endpointDataSource = builder.DataSources.OfType().Single();
+ var hostEnvironment = new HostEnvironment
+ {
+ ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
+ };
+ var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
+
+ // Act
+ provider.OnProvidersExecuting(context);
+
+ // Assert
+ var apiDescription = Assert.Single(context.Results);
+ var apiParameterDescription = Assert.Single(apiDescription.ParameterDescriptions);
+ Assert.Equal("A description for the parameter", apiParameterDescription.Description);
+ }
+
+ [Fact]
+ public void HandleRequestBodyWithExample_ViaExtensionMethod()
+ {
+ var builder = CreateBuilder();
+ builder
+ .MapPost("/api/todos", (Todo todo) => "")
+ .WithParameterExample("todo", "A todo", "A quick description of the todo", new Todo() { Id = 0, Title = "foo" });
+
+ var context = new ApiDescriptionProviderContext(Array.Empty());
+
+ var endpointDataSource = builder.DataSources.OfType().Single();
+ var hostEnvironment = new HostEnvironment
+ {
+ ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
+ };
+ var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
+
+ // Act
+ provider.OnProvidersExecuting(context);
+
+ // Assert
+ var apiDescription = Assert.Single(context.Results);
+ var parameter = Assert.Single(apiDescription.ParameterDescriptions);
+ var example = Assert.Single(parameter.Examples);
+ Assert.Equal("A quick description of the todo", example.Description);
+ Assert.Equal("A todo", example.Summary);
+ var todo = Assert.IsType(example.Value);
+ Assert.Equal(0, todo.Id);
+ }
+
+ [Fact]
+ public void CanHandleParameterWithExample_WithAttribute()
+ {
+ var builder = CreateBuilder();
+ builder.MapPost("/api/todos", (
+ [Example("A number", "A detailed description of number", 2)]
+ int id,
+ [Example("A bool", "A detailed description of bool", true)]
+ bool name
+ ) => "");
+
+ var context = new ApiDescriptionProviderContext(Array.Empty());
+
+ var endpointDataSource = builder.DataSources.OfType().Single();
+ var hostEnvironment = new HostEnvironment
+ {
+ ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
+ };
+ var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
+
+ // Act
+ provider.OnProvidersExecuting(context);
+
+ // Assert
+ var apiDescription = Assert.Single(context.Results);
+ Assert.Collection(apiDescription.ParameterDescriptions,
+ intParam =>
+ {
+ var example = Assert.Single(intParam.Examples);
+ Assert.Equal("A detailed description of number", example.Description);
+ Assert.Equal("A number", example.Summary);
+ var value = Assert.IsType(example.Value);
+ Assert.Equal(2, value);
+ },
+ boolParam =>
+ {
+ var example = Assert.Single(boolParam.Examples);
+ Assert.Equal("A detailed description of bool", example.Description);
+ Assert.Equal("A bool", example.Summary);
+ var value = Assert.IsType(example.Value);
+ Assert.True(value);
+ }
+ );
+ }
+
+ [Fact]
+ public void CanHandleParameterWithExample_ExtensionMethod()
+ {
+ var date = new DateTime();
+ var guid = new Guid();
+ var builder = CreateBuilder();
+ builder.MapPost("/api/todos", (DateTime startDate, Guid id) => "")
+ .WithParameterExample("startDate", "A date", "A description of a date", date)
+ .WithParameterExample("id", "An ID", "A GUID", guid);
+
+ var context = new ApiDescriptionProviderContext(Array.Empty());
+
+ var endpointDataSource = builder.DataSources.OfType().Single();
+ var hostEnvironment = new HostEnvironment
+ {
+ ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
+ };
+ var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
+
+ // Act
+ provider.OnProvidersExecuting(context);
+
+ // Assert
+ var apiDescription = Assert.Single(context.Results);
+ Assert.Collection(apiDescription.ParameterDescriptions,
+ intParam =>
+ {
+ var example = Assert.Single(intParam.Examples);
+ Assert.Equal("A date", example.Summary);
+ Assert.Equal("A description of a date", example.Description);
+ var value = Assert.IsType(example.Value);
+ Assert.Equal(date, value);
+ },
+ boolParam =>
+ {
+ var example = Assert.Single(boolParam.Examples);
+ Assert.Equal("An ID", example.Summary);
+ Assert.Equal("A GUID", example.Description);
+ var value = Assert.IsType(example.Value);
+ Assert.Equal(guid, value);
+ }
+ );
+ }
+
+ [Fact]
+ public void CanHandleResponseWithExample()
+ {
+ var builder = CreateBuilder();
+ builder.MapGet("/api/todos", (int id) => "").WithResponseExample("A todo", "A todo description", new Todo() { Id = 1, Title = "foo" });
+
+ var context = new ApiDescriptionProviderContext(Array.Empty());
+
+ var endpointDataSource = builder.DataSources.OfType().Single();
+ var hostEnvironment = new HostEnvironment
+ {
+ ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
+ };
+ var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
+
+ // Act
+ provider.OnProvidersExecuting(context);
+
+ // Assert
+ var apiDescription = Assert.Single(context.Results);
+ var response = Assert.Single(apiDescription.SupportedResponseTypes);
+ var example = Assert.Single(response.Examples);
+ Assert.Equal("A todo description", example.Description);
+ Assert.Equal("A todo", example.Summary);
+ var todo = Assert.IsType(example.Value);
+ Assert.Equal(1, todo.Id);
+ }
+
private static IEnumerable GetSortedMediaTypes(ApiResponseType apiResponseType)
{
return apiResponseType.ApiResponseFormats
@@ -1186,6 +1445,12 @@ private interface IInferredJsonInterface
{
}
+ private class Todo
+ {
+ public int Id { get; set; }
+ public string Title { get; set; }
+ }
+
private class ServiceProviderIsService : IServiceProviderIsService
{
public bool IsService(Type serviceType) => serviceType == typeof(IInferredServiceInterface);
diff --git a/src/Shared/ApiExplorerTypes/ProducesResponseTypeMetadata.cs b/src/Shared/ApiExplorerTypes/ProducesResponseTypeMetadata.cs
index 11b2de096f35..ae24fd551d29 100644
--- a/src/Shared/ApiExplorerTypes/ProducesResponseTypeMetadata.cs
+++ b/src/Shared/ApiExplorerTypes/ProducesResponseTypeMetadata.cs
@@ -30,14 +30,29 @@ public ProducesResponseTypeMetadata(int statusCode)
///
/// The of object that is going to be written in the response.
/// The HTTP response status code.
- public ProducesResponseTypeMetadata(Type type, int statusCode)
+ /// A description of the response.
+ public ProducesResponseTypeMetadata(Type type, int statusCode, string? description = null)
{
Type = type ?? throw new ArgumentNullException(nameof(type));
StatusCode = statusCode;
IsResponseTypeSetByDefault = false;
+ Description = description;
_contentTypes = Enumerable.Empty();
}
+ ///
+ /// Initializes an instance of .
+ ///
+ /// The of object that is going to be written in the response.
+ /// A description of the response.
+ /// The HTTP response status code.
+ /// The content type associated with the response.
+ /// Additional content types supported by the response.
+ public ProducesResponseTypeMetadata(Type type, string description, int statusCode, string contentType, params string[] additionalContentTypes) : this(type, statusCode, contentType, additionalContentTypes)
+ {
+ Description = description;
+ }
+
///
/// Initializes an instance of .
///
@@ -87,6 +102,9 @@ public ProducesResponseTypeMetadata(Type type, int statusCode, string contentTyp
///
internal bool IsResponseTypeSetByDefault { get; }
+ ///
+ public string? Description { get; }
+
public IEnumerable ContentTypes => _contentTypes;
private static List GetContentTypes(string contentType, string[] additionalContentTypes)