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..067f48f70411
--- /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 IEndpointDescriptionMetadata
+{
+ ///
+ /// Gets the description associated with the endpoint.
+ ///
+ 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..fa1083d52183
--- /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 IEndpointSummaryMetadata
+{
+ ///
+ /// 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..7e611715814c 100644
--- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
@@ -4,3 +4,7 @@ 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.IEndpointDescriptionMetadata
+Microsoft.AspNetCore.Http.Metadata.IEndpointDescriptionMetadata.Description.get -> string!
+Microsoft.AspNetCore.Http.Metadata.IEndpointSummaryMetadata
+Microsoft.AspNetCore.Http.Metadata.IEndpointSummaryMetadata.Summary.get -> string!
diff --git a/src/Http/Http.Extensions/src/EndpointDescriptionAttribute.cs b/src/Http/Http.Extensions/src/EndpointDescriptionAttribute.cs
new file mode 100644
index 000000000000..c8e7b51a2614
--- /dev/null
+++ b/src/Http/Http.Extensions/src/EndpointDescriptionAttribute.cs
@@ -0,0 +1,29 @@
+// 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.
+///
+[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
+public sealed class EndpointDescriptionAttribute : Attribute, IEndpointDescriptionMetadata
+{
+ ///
+ /// Initializes an instance of the .
+ ///
+ /// The description associated with the endpoint or parameter.
+ public EndpointDescriptionAttribute(string description)
+ {
+ Description = description;
+ }
+
+ ///
+ public string Description { get; }
+}
diff --git a/src/Http/Http.Extensions/src/EndpointSummaryAttribute.cs b/src/Http/Http.Extensions/src/EndpointSummaryAttribute.cs
new file mode 100644
index 000000000000..d5435365dc4e
--- /dev/null
+++ b/src/Http/Http.Extensions/src/EndpointSummaryAttribute.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, Inherited = false, AllowMultiple = false)]
+public sealed class EndpointSummaryAttribute : Attribute, IEndpointSummaryMetadata
+{
+ ///
+ /// Initializes an instance of the .
+ ///
+ /// The summary associated with the endpoint or parameter.
+ public EndpointSummaryAttribute(string summary)
+ {
+ Summary = summary;
+ }
+
+ ///
+ public string Summary { get; }
+}
diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
index 1030d0f0793e..28a2fa1553be 100644
--- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
@@ -1,3 +1,9 @@
#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.EndpointDescriptionAttribute
+Microsoft.AspNetCore.Http.EndpointDescriptionAttribute.EndpointDescriptionAttribute(string! description) -> void
+Microsoft.AspNetCore.Http.EndpointDescriptionAttribute.Description.get -> string!
+Microsoft.AspNetCore.Http.EndpointSummaryAttribute
+Microsoft.AspNetCore.Http.EndpointSummaryAttribute.EndpointSummaryAttribute(string! summary) -> void
+Microsoft.AspNetCore.Http.EndpointSummaryAttribute.Summary.get -> string!
diff --git a/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs b/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs
index 31696a3da7be..2456ed622c2d 100644
--- a/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs
+++ b/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs
@@ -17,7 +17,7 @@ public static class OpenApiRouteHandlerBuilderExtensions
private static readonly ExcludeFromDescriptionAttribute _excludeFromDescriptionMetadataAttribute = new();
///
- /// Adds the to for all builders
+ /// Adds the to for all endpoints
/// produced by .
///
/// The .
@@ -30,7 +30,7 @@ public static RouteHandlerBuilder ExcludeFromDescription(this RouteHandlerBuilde
}
///
- /// Adds an to for all builders
+ /// Adds an to for all endpoints
/// produced by .
///
/// The type of the response.
@@ -50,7 +50,7 @@ public static RouteHandlerBuilder Produces(this RouteHandlerBuilder b
}
///
- /// Adds an to for all builders
+ /// Adds an to for all endpoints
/// produced by .
///
/// The .
@@ -85,7 +85,7 @@ public static RouteHandlerBuilder Produces(this RouteHandlerBuilder builder,
///
/// Adds an with a type
- /// to for all builders produced by .
+ /// to for all endpoints produced by .
///
/// The .
/// The response status code.
@@ -105,7 +105,7 @@ public static RouteHandlerBuilder ProducesProblem(this RouteHandlerBuilder build
///
/// Adds an with a type
- /// to for all builders produced by .
+ /// to for all endpoints produced by .
///
/// The .
/// The response status code. Defaults to .
@@ -124,7 +124,7 @@ public static RouteHandlerBuilder ProducesValidationProblem(this RouteHandlerBui
}
///
- /// Adds the to for all builders
+ /// Adds the to for all endpoints
/// produced by .
///
///
@@ -142,7 +142,7 @@ public static RouteHandlerBuilder WithTags(this RouteHandlerBuilder builder, par
}
///
- /// Adds to for all builders
+ /// Adds to for all endpoints
/// produced by .
///
/// The type of the request body.
@@ -159,7 +159,7 @@ public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder bui
}
///
- /// Adds to for all builders
+ /// Adds to for all endpoints
/// produced by .
///
/// The type of the request body.
@@ -177,7 +177,7 @@ public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder bui
}
///
- /// Adds to for all builders
+ /// Adds to for all endpoints
/// produced by .
///
/// The .
@@ -193,7 +193,7 @@ public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder,
}
///
- /// Adds to for all builders
+ /// Adds to for all endpoints
/// produced by .
///
/// The .
@@ -209,6 +209,32 @@ public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder,
return builder;
}
+ ///
+ /// Adds to for all endpoints
+ /// 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 EndpointDescriptionAttribute(description));
+ return builder;
+ }
+
+ ///
+ /// Adds to for all endpoints
+ /// produced by .
+ ///
+ /// The .
+ /// A string representing 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 EndpointSummaryAttribute(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.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt
index 341f2446a1cf..17d11a018a8e 100644
--- a/src/Http/Routing/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt
@@ -6,3 +6,5 @@ static Microsoft.AspNetCore.Builder.EndpointRouteBuilderExtensions.MapPatch(this
override Microsoft.AspNetCore.Routing.RouteValuesAddress.ToString() -> string?
*REMOVED*~Microsoft.AspNetCore.Routing.DefaultInlineConstraintResolver.DefaultInlineConstraintResolver(Microsoft.Extensions.Options.IOptions! routeOptions, System.IServiceProvider! serviceProvider) -> void
Microsoft.AspNetCore.Routing.DefaultInlineConstraintResolver.DefaultInlineConstraintResolver(Microsoft.Extensions.Options.IOptions! routeOptions, System.IServiceProvider! serviceProvider) -> void
+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.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs
index f933be54ce6d..5a77ef5f9b55 100644
--- a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs
+++ b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs
@@ -6,6 +6,7 @@
using System.Security.Claims;
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;
@@ -1145,6 +1146,68 @@ 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}", [EndpointSummary("A summary")][EndpointDescription("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);
+ }
+
private static IEnumerable GetSortedMediaTypes(ApiResponseType apiResponseType)
{
return apiResponseType.ApiResponseFormats