diff --git a/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs b/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs
index 2456ed622c2d..2615e70e1ac9 100644
--- a/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs
+++ b/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs
@@ -16,6 +16,15 @@ public static class OpenApiRouteHandlerBuilderExtensions
{
private static readonly ExcludeFromDescriptionAttribute _excludeFromDescriptionMetadataAttribute = new();
+ ///
+ /// Adds the to for all endpoints
+ /// produced by .
+ ///
+ /// The .
+ /// A that can be used to further customize the endpoint.
+ public static TBuilder ExcludeFromDescription(this TBuilder builder) where TBuilder : IEndpointConventionBuilder
+ => builder.WithMetadata(_excludeFromDescriptionMetadataAttribute);
+
///
/// Adds the to for all endpoints
/// produced by .
@@ -23,11 +32,7 @@ public static class OpenApiRouteHandlerBuilderExtensions
/// The .
/// A that can be used to further customize the endpoint.
public static RouteHandlerBuilder ExcludeFromDescription(this RouteHandlerBuilder builder)
- {
- builder.WithMetadata(_excludeFromDescriptionMetadataAttribute);
-
- return builder;
- }
+ => ExcludeFromDescription(builder);
///
/// Adds an to for all endpoints
@@ -40,9 +45,10 @@ public static RouteHandlerBuilder ExcludeFromDescription(this RouteHandlerBuilde
/// Additional response content types the endpoint produces for the supplied status code.
/// A that can be used to further customize the endpoint.
#pragma warning disable RS0026
- public static RouteHandlerBuilder Produces(this RouteHandlerBuilder builder,
+ public static RouteHandlerBuilder Produces(
#pragma warning restore RS0026
- int statusCode = StatusCodes.Status200OK,
+ this RouteHandlerBuilder builder,
+ int statusCode = StatusCodes.Status200OK,
string? contentType = null,
params string[] additionalContentTypes)
{
@@ -60,9 +66,10 @@ public static RouteHandlerBuilder Produces(this RouteHandlerBuilder b
/// Additional response content types the endpoint produces for the supplied status code.
/// A that can be used to further customize the endpoint.
#pragma warning disable RS0026
- public static RouteHandlerBuilder Produces(this RouteHandlerBuilder builder,
+ public static RouteHandlerBuilder Produces(
#pragma warning restore RS0026
- int statusCode,
+ this RouteHandlerBuilder builder,
+ int statusCode,
Type? responseType = null,
string? contentType = null,
params string[] additionalContentTypes)
@@ -74,13 +81,10 @@ public static RouteHandlerBuilder Produces(this RouteHandlerBuilder builder,
if (contentType is null)
{
- builder.WithMetadata(new ProducesResponseTypeMetadata(responseType ?? typeof(void), statusCode));
- return builder;
+ return builder.WithMetadata(new ProducesResponseTypeMetadata(responseType ?? typeof(void), statusCode));
}
- builder.WithMetadata(new ProducesResponseTypeMetadata(responseType ?? typeof(void), statusCode, contentType, additionalContentTypes));
-
- return builder;
+ return builder.WithMetadata(new ProducesResponseTypeMetadata(responseType ?? typeof(void), statusCode, contentType, additionalContentTypes));
}
///
@@ -91,16 +95,14 @@ public static RouteHandlerBuilder Produces(this RouteHandlerBuilder builder,
/// The response status code.
/// The response content type. Defaults to "application/problem+json".
/// A that can be used to further customize the endpoint.
- public static RouteHandlerBuilder ProducesProblem(this RouteHandlerBuilder builder,
- int statusCode,
- string? contentType = null)
+ public static RouteHandlerBuilder ProducesProblem(this RouteHandlerBuilder builder, int statusCode, string? contentType = null)
{
if (string.IsNullOrEmpty(contentType))
{
contentType = "application/problem+json";
}
- return Produces(builder, statusCode, contentType);
+ return Produces(builder, statusCode, typeof(ProblemDetails), contentType);
}
///
@@ -111,7 +113,8 @@ public static RouteHandlerBuilder ProducesProblem(this RouteHandlerBuilder build
/// The response status code. Defaults to .
/// The response content type. Defaults to "application/problem+json".
/// A that can be used to further customize the endpoint.
- public static RouteHandlerBuilder ProducesValidationProblem(this RouteHandlerBuilder builder,
+ public static RouteHandlerBuilder ProducesValidationProblem(
+ this RouteHandlerBuilder builder,
int statusCode = StatusCodes.Status400BadRequest,
string? contentType = null)
{
@@ -120,9 +123,24 @@ public static RouteHandlerBuilder ProducesValidationProblem(this RouteHandlerBui
contentType = "application/problem+json";
}
- return Produces(builder, statusCode, contentType);
+ return Produces(builder, statusCode, typeof(HttpValidationProblemDetails), contentType);
}
+ ///
+ /// Adds the to for all endpoints
+ /// produced by .
+ ///
+ ///
+ /// The OpenAPI specification supports a tags classification to categorize operations
+ /// into related groups. These tags are typically included in the generated specification
+ /// and are typically used to group operations by tags in the UI.
+ ///
+ /// The .
+ /// A collection of tags to be associated with the endpoint.
+ /// A that can be used to further customize the endpoint.
+ public static TBuilder WithTags(this TBuilder builder, params string[] tags) where TBuilder : IEndpointConventionBuilder
+ => builder.WithMetadata(new TagsAttribute(tags));
+
///
/// Adds the to for all endpoints
/// produced by .
@@ -136,10 +154,7 @@ public static RouteHandlerBuilder ProducesValidationProblem(this RouteHandlerBui
/// A collection of tags to be associated with the endpoint.
/// A that can be used to further customize the endpoint.
public static RouteHandlerBuilder WithTags(this RouteHandlerBuilder builder, params string[] tags)
- {
- builder.WithMetadata(new TagsAttribute(tags));
- return builder;
- }
+ => WithTags(builder, tags);
///
/// Adds to for all endpoints
@@ -150,12 +165,12 @@ public static RouteHandlerBuilder WithTags(this RouteHandlerBuilder builder, par
/// The request content type that the endpoint accepts.
/// The list of additional request content types that the endpoint accepts.
/// A that can be used to further customize the endpoint.
- public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder,
- string contentType, params string[] additionalContentTypes) where TRequest : notnull
+ public static RouteHandlerBuilder Accepts(
+ this RouteHandlerBuilder builder,
+ string contentType,
+ params string[] additionalContentTypes) where TRequest : notnull
{
- Accepts(builder, typeof(TRequest), contentType, additionalContentTypes);
-
- return builder;
+ return Accepts(builder, typeof(TRequest), contentType, additionalContentTypes);
}
///
@@ -168,12 +183,13 @@ public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder bui
/// The request content type that the endpoint accepts.
/// The list of additional request content types that the endpoint accepts.
/// A that can be used to further customize the endpoint.
- public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder,
- bool isOptional, string contentType, params string[] additionalContentTypes) where TRequest : notnull
+ public static RouteHandlerBuilder Accepts(
+ this RouteHandlerBuilder builder,
+ bool isOptional,
+ string contentType,
+ params string[] additionalContentTypes) where TRequest : notnull
{
- Accepts(builder, typeof(TRequest), isOptional, contentType, additionalContentTypes);
-
- return builder;
+ return Accepts(builder, typeof(TRequest), isOptional, contentType, additionalContentTypes);
}
///
@@ -185,11 +201,13 @@ public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder bui
/// The request content type that the endpoint accepts.
/// The list of additional request content types that the endpoint accepts.
/// A that can be used to further customize the endpoint.
- public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder,
- Type requestType, string contentType, params string[] additionalContentTypes)
+ public static RouteHandlerBuilder Accepts(
+ this RouteHandlerBuilder builder,
+ Type requestType,
+ string contentType,
+ params string[] additionalContentTypes)
{
- builder.WithMetadata(new AcceptsMetadata(requestType, false, GetAllContentTypes(contentType, additionalContentTypes)));
- return builder;
+ return Accepts(builder, requestType, isOptional: false, contentType, additionalContentTypes);
}
///
@@ -202,38 +220,36 @@ public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder,
/// The request content type that the endpoint accepts.
/// The list of additional request content types that the endpoint accepts.
/// A that can be used to further customize the endpoint.
- public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder,
- Type requestType, bool isOptional, string contentType, params string[] additionalContentTypes)
+ public static RouteHandlerBuilder Accepts(
+ this RouteHandlerBuilder builder,
+ Type requestType,
+ bool isOptional,
+ string contentType,
+ params string[] additionalContentTypes)
{
- builder.WithMetadata(new AcceptsMetadata(requestType, isOptional, GetAllContentTypes(contentType, additionalContentTypes)));
- return builder;
+ var contentTypes = GetAllContentTypes(contentType, additionalContentTypes);
+ return builder.WithMetadata(new AcceptsMetadata(requestType, isOptional, contentTypes));
}
///
/// Adds to for all endpoints
/// produced by .
///
- /// The .
+ /// 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;
- }
+ public static TBuilder WithDescription(this TBuilder builder, string description) where TBuilder : IEndpointConventionBuilder
+ => builder.WithMetadata(new EndpointDescriptionAttribute(description));
///
/// Adds to for all endpoints
/// produced by .
///
- /// The .
+ /// 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;
- }
+ public static TBuilder WithSummary(this TBuilder builder, string summary) where TBuilder : IEndpointConventionBuilder
+ => builder.WithMetadata(new EndpointSummaryAttribute(summary));
private static string[] GetAllContentTypes(string contentType, string[] additionalContentTypes)
{
diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt
index 632d25fbd8af..6b1ab9652506 100644
--- a/src/Http/Routing/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt
@@ -11,12 +11,14 @@ 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.ExcludeFromDescription(this TBuilder builder) -> TBuilder
+static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.WithDescription(this TBuilder builder, string! description) -> TBuilder
+static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.WithSummary(this TBuilder builder, string! summary) -> TBuilder
+static Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions.WithTags(this TBuilder builder, params string![]! tags) -> TBuilder
static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, Microsoft.AspNetCore.Http.IRouteHandlerFilter! filter) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, System.Func! filterFactory) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder, System.Func>! routeHandlerFilter) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!
static Microsoft.AspNetCore.Http.RouteHandlerFilterExtensions.AddFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder! builder) -> 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!
static Microsoft.AspNetCore.Routing.LinkGeneratorEndpointNameAddressExtensions.GetPathByName(this Microsoft.AspNetCore.Routing.LinkGenerator! generator, Microsoft.AspNetCore.Http.HttpContext! httpContext, string! endpointName, Microsoft.AspNetCore.Routing.RouteValueDictionary? values = null, Microsoft.AspNetCore.Http.PathString? pathBase = null, Microsoft.AspNetCore.Http.FragmentString fragment = default(Microsoft.AspNetCore.Http.FragmentString), Microsoft.AspNetCore.Routing.LinkOptions? options = null) -> string?
static Microsoft.AspNetCore.Routing.LinkGeneratorEndpointNameAddressExtensions.GetPathByName(this Microsoft.AspNetCore.Routing.LinkGenerator! generator, string! endpointName, Microsoft.AspNetCore.Routing.RouteValueDictionary? values = null, Microsoft.AspNetCore.Http.PathString pathBase = default(Microsoft.AspNetCore.Http.PathString), Microsoft.AspNetCore.Http.FragmentString fragment = default(Microsoft.AspNetCore.Http.FragmentString), Microsoft.AspNetCore.Routing.LinkOptions? options = null) -> string?
static Microsoft.AspNetCore.Routing.LinkGeneratorEndpointNameAddressExtensions.GetUriByName(this Microsoft.AspNetCore.Routing.LinkGenerator! generator, Microsoft.AspNetCore.Http.HttpContext! httpContext, string! endpointName, Microsoft.AspNetCore.Routing.RouteValueDictionary? values = null, string? scheme = null, Microsoft.AspNetCore.Http.HostString? host = null, Microsoft.AspNetCore.Http.PathString? pathBase = null, Microsoft.AspNetCore.Http.FragmentString fragment = default(Microsoft.AspNetCore.Http.FragmentString), Microsoft.AspNetCore.Routing.LinkOptions? options = null) -> string?
diff --git a/src/Http/Routing/test/UnitTests/Builder/OpenApiRouteHandlerBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/OpenApiRouteHandlerBuilderExtensionsTest.cs
new file mode 100644
index 000000000000..54a87d2aa5aa
--- /dev/null
+++ b/src/Http/Routing/test/UnitTests/Builder/OpenApiRouteHandlerBuilderExtensionsTest.cs
@@ -0,0 +1,158 @@
+// 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.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Metadata;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Microsoft.AspNetCore.Routing.Builder;
+
+public class OpenApiRouteHandlerBuilderExtensionsTest
+{
+ [Fact]
+ public void ExcludeFromDescription_AddsExcludeFromDescriptionAttribute()
+ {
+ static void GenericExclude(IEndpointConventionBuilder builder) => builder.ExcludeFromDescription();
+ static void SpecificExclude(RouteHandlerBuilder builder) => builder.ExcludeFromDescription();
+
+ static void AssertMetadata(EndpointBuilder builder)
+ => Assert.IsType(Assert.Single(builder.Metadata));
+
+ RunWithBothBuilders(GenericExclude, SpecificExclude, AssertMetadata);
+ }
+
+ [Fact]
+ public void WithTags_AddsTagsAttribute()
+ {
+ static void GenericWithTags(IEndpointConventionBuilder builder) => builder.WithTags("a", "b", "c");
+ static void SpecificWithTags(RouteHandlerBuilder builder) => builder.WithTags("a", "b", "c");
+
+ static void AssertMetadata(EndpointBuilder builder)
+ {
+ var tags = Assert.IsType(Assert.Single(builder.Metadata));
+ Assert.Collection(tags.Tags,
+ tag => Assert.Equal("a", tag),
+ tag => Assert.Equal("b", tag),
+ tag => Assert.Equal("c", tag));
+ }
+
+ RunWithBothBuilders(GenericWithTags, SpecificWithTags, AssertMetadata);
+ }
+
+ [Fact]
+ public void Produces_AddsProducesResponseTypeMetadataWithJsonContentType()
+ {
+ var testBuilder = new TestEndointConventionBuilder();
+ var builder = new RouteHandlerBuilder(new[] { testBuilder });
+
+ builder.Produces();
+
+ var metadata = Assert.IsType(Assert.Single(testBuilder.Metadata));
+ Assert.Equal(typeof(TestEndointConventionBuilder), metadata.Type);
+ Assert.Equal(StatusCodes.Status200OK, metadata.StatusCode);
+ Assert.Equal("application/json", Assert.Single(metadata.ContentTypes));
+ }
+
+ [Fact]
+ public void Produces_AddsProducesResponseTypeMetadataWithVoidType()
+ {
+ var testBuilder = new TestEndointConventionBuilder();
+ var builder = new RouteHandlerBuilder(new[] { testBuilder });
+
+ builder.Produces(StatusCodes.Status404NotFound);
+
+ var metadata = Assert.IsType(Assert.Single(testBuilder.Metadata));
+ Assert.Equal(typeof(void), metadata.Type);
+ Assert.Equal(StatusCodes.Status404NotFound, metadata.StatusCode);
+ Assert.Empty(metadata.ContentTypes);
+ }
+
+ [Fact]
+ public void ProdcesProblem_AddsProducesResponseTypeMetadataWithProblemDetailsType()
+ {
+ var testBuilder = new TestEndointConventionBuilder();
+ var builder = new RouteHandlerBuilder(new[] { testBuilder });
+
+ builder.ProducesProblem(StatusCodes.Status400BadRequest);
+
+ var metadata = Assert.IsType(Assert.Single(testBuilder.Metadata));
+ Assert.Equal(typeof(ProblemDetails), metadata.Type);
+ Assert.Equal(StatusCodes.Status400BadRequest, metadata.StatusCode);
+ Assert.Equal("application/problem+json", Assert.Single(metadata.ContentTypes));
+ }
+
+ [Fact]
+ public void ProdcesValidiationProblem_AddsProducesResponseTypeMetadataWithHttpValidationProblemDetailsType()
+ {
+ var testBuilder = new TestEndointConventionBuilder();
+ var builder = new RouteHandlerBuilder(new[] { testBuilder });
+
+ builder.ProducesValidationProblem();
+
+ var metadata = Assert.IsType(Assert.Single(testBuilder.Metadata));
+ Assert.Equal(typeof(HttpValidationProblemDetails), metadata.Type);
+ Assert.Equal(StatusCodes.Status400BadRequest, metadata.StatusCode);
+ Assert.Equal("application/problem+json", Assert.Single(metadata.ContentTypes));
+ }
+
+ [Fact]
+ public void Accepts_AddsAcceptsMetadataWithSpecifiedType()
+ {
+ var testBuilder = new TestEndointConventionBuilder();
+ var builder = new RouteHandlerBuilder(new[] { testBuilder });
+
+ builder.Accepts("text/plain");
+
+ var metadata = Assert.IsType(Assert.Single(testBuilder.Metadata));
+
+ Assert.Equal(typeof(TestEndointConventionBuilder), metadata.RequestType);
+ Assert.Equal("text/plain", Assert.Single(metadata.ContentTypes));
+ Assert.False(metadata.IsOptional);
+ }
+
+ [Fact]
+ public void WithDescription_AddsEndpointDescriptionAttribute()
+ {
+ var builder = new TestEndointConventionBuilder();
+ builder.WithDescription("test description");
+
+ var metadata = Assert.IsType(Assert.Single(builder.Metadata));
+ Assert.Equal("test description", metadata.Description);
+ }
+
+ [Fact]
+ public void WithSummary_AddsEndpointSummaryAttribute()
+ {
+ var builder = new TestEndointConventionBuilder();
+ builder.WithSummary("test summary");
+
+ var metadata = Assert.IsType(Assert.Single(builder.Metadata));
+ Assert.Equal("test summary", metadata.Summary);
+ }
+
+ private void RunWithBothBuilders(
+ Action genericSetup,
+ Action specificSetup,
+ Action assert)
+ {
+ var testBuilder = new TestEndointConventionBuilder();
+ genericSetup(testBuilder);
+ assert(testBuilder);
+
+ var routeTestBuilder = new TestEndointConventionBuilder();
+ var routeHandlerBuilder = new RouteHandlerBuilder(new[] { routeTestBuilder });
+ specificSetup(routeHandlerBuilder);
+ assert(routeTestBuilder);
+ }
+
+ private sealed class TestEndointConventionBuilder : EndpointBuilder, IEndpointConventionBuilder
+ {
+ public void Add(Action convention)
+ {
+ convention(this);
+ }
+
+ public override Endpoint Build() => throw new NotImplementedException();
+ }
+}
diff --git a/src/Http/samples/MinimalSample/Program.cs b/src/Http/samples/MinimalSample/Program.cs
index 4eb5ecd34d41..4694e666df9a 100644
--- a/src/Http/samples/MinimalSample/Program.cs
+++ b/src/Http/samples/MinimalSample/Program.cs
@@ -18,7 +18,7 @@
var nestedGroup = app.MapGroup("/group/{groupName}")
.MapGroup("/nested/{nestedName}")
- .WithMetadata(new TagsAttribute("nested"));
+ .WithTags("nested");
nestedGroup
.MapGet("/", (string groupName, string nestedName) =>