From f0d255816897193959ea57cf68f52afe9de1b6d0 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 27 Jul 2021 10:01:06 -0500 Subject: [PATCH 01/20] Support setting content types in ProducesResponseTypeAttribute to close #34542 --- .../src/ProducesResponseTypeAttribute.cs | 65 ++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs b/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs index c954dfba5eb1..ef9af810858d 100644 --- a/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs +++ b/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs @@ -4,6 +4,7 @@ using System; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Mvc { @@ -33,6 +34,37 @@ public ProducesResponseTypeAttribute(Type type, int statusCode) Type = type ?? throw new ArgumentNullException(nameof(type)); StatusCode = statusCode; IsResponseTypeSetByDefault = false; + ContentTypes = new(); + } + + /// + /// Initializes an instance of . + /// + /// The of object that is going to be written in the response. + /// The HTTP response status code. + /// The content type associated with the response. + /// Additional content types supported by the response. + public ProducesResponseTypeAttribute(Type type, int statusCode, string? contentType, params string[] additionalContentTypes) + { + Type = type ?? throw new ArgumentNullException(nameof(type)); + StatusCode = statusCode; + IsResponseTypeSetByDefault = false; + + if (!string.IsNullOrEmpty(contentType)) + { + MediaTypeHeaderValue.Parse(contentType); + for (var i = 0; i < additionalContentTypes.Length; i++) + { + MediaTypeHeaderValue.Parse(additionalContentTypes[i]); + } + + ContentTypes = GetContentTypes(contentType, additionalContentTypes); + } + else + { + ContentTypes = new(); + } + } /// @@ -45,6 +77,11 @@ public ProducesResponseTypeAttribute(Type type, int statusCode) /// public int StatusCode { get; set; } + /// + /// Gets or sets the content types supported by the response. + /// + public MediaTypeCollection ContentTypes { get; set; } + /// /// Used to distinguish a `Type` set by default in the constructor versus /// one provided by the user. @@ -58,9 +95,33 @@ public ProducesResponseTypeAttribute(Type type, int statusCode) internal bool IsResponseTypeSetByDefault { get; } /// - void IApiResponseMetadataProvider.SetContentTypes(MediaTypeCollection contentTypes) + public void SetContentTypes(MediaTypeCollection contentTypes) { - // Users are supposed to use the 'Produces' attribute to set the content types that an action can support. + contentTypes.Clear(); + foreach (var contentType in ContentTypes) + { + contentTypes.Add(contentType); + } + } + + private MediaTypeCollection GetContentTypes(string contentType, string[] additionalContentTypes) + { + List completeContentTypes = new(additionalContentTypes.Length + 1); + completeContentTypes.Add(contentType); + completeContentTypes.AddRange(additionalContentTypes); + MediaTypeCollection contentTypes = new(); + foreach (var type in completeContentTypes) + { + var mediaType = new MediaType(type); + if (mediaType.HasWildcard) + { + throw new InvalidOperationException("Content types with wild cards are not supported."); + } + + contentTypes.Add(type); + } + + return contentTypes; } } } From e6c4335c8d3150df963cecfcedf75ff9468cce7e Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 27 Jul 2021 10:31:23 -0500 Subject: [PATCH 02/20] Add WithName extension method to resolve #34538 --- .../RoutingEndpointConventionBuilderExtensions.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs b/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs index 9b3bffac2b29..eec456a0081e 100644 --- a/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs @@ -118,5 +118,18 @@ public static TBuilder WithMetadata(this TBuilder builder, params obje return builder; } + + /// + /// Sets the for all endpoints produced + /// on the target . + /// + /// The . + /// The endpoint name. + /// The . + public static TBuilder WithName(this TBuilder builder, string endpointName) where TBuilder : IEndpointConventionBuilder + { + builder.WithMetadata(new EndpointNameMetadata(endpointName)); + return builder; + } } } From 74df1e334df1af8bf98617aa7fb9679443cda524 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 27 Jul 2021 10:52:55 -0500 Subject: [PATCH 03/20] Support setting endpoints on group names to resolve #34541 --- ...tingEndpointConventionBuilderExtensions.cs | 13 ++++++++ .../Routing/src/EndpointGroupNameMetadata.cs | 33 +++++++++++++++++++ .../Routing/src/IEndpointGroupNameMetadata.cs | 18 ++++++++++ 3 files changed, 64 insertions(+) create mode 100644 src/Http/Routing/src/EndpointGroupNameMetadata.cs create mode 100644 src/Http/Routing/src/IEndpointGroupNameMetadata.cs diff --git a/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs b/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs index eec456a0081e..5c00024da242 100644 --- a/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs @@ -131,5 +131,18 @@ public static TBuilder WithName(this TBuilder builder, string endpoint builder.WithMetadata(new EndpointNameMetadata(endpointName)); return builder; } + + /// + /// Sets the for all endpoints produced + /// on the target . + /// + /// The . + /// The endpoint group name. + /// The . + public static TBuilder WithGroupName(this TBuilder builder, string endpointGroupName) where TBuilder : IEndpointConventionBuilder + { + builder.WithMetadata(new EndpointGroupNameMetadata(endpointGroupName)); + return builder; + } } } diff --git a/src/Http/Routing/src/EndpointGroupNameMetadata.cs b/src/Http/Routing/src/EndpointGroupNameMetadata.cs new file mode 100644 index 000000000000..8073c1f1f73a --- /dev/null +++ b/src/Http/Routing/src/EndpointGroupNameMetadata.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// Specifies an endpoint group name in . + /// + public class EndpointGroupNameMetadata : IEndpointGroupNameMetadata + { + /// + /// Creates a new instance of with the provided endpoint name. + /// + /// The endpoint name. + public EndpointGroupNameMetadata(string endpointGroupName) + { + if (endpointGroupName == null) + { + throw new ArgumentNullException(nameof(endpointGroupName)); + } + + EndpointGroupName = endpointGroupName; + } + + /// + /// Gets the endpoint name. + /// + public string EndpointGroupName { get; } + } +} diff --git a/src/Http/Routing/src/IEndpointGroupNameMetadata.cs b/src/Http/Routing/src/IEndpointGroupNameMetadata.cs new file mode 100644 index 000000000000..cddf566111cf --- /dev/null +++ b/src/Http/Routing/src/IEndpointGroupNameMetadata.cs @@ -0,0 +1,18 @@ +// 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; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// Defines a contract use to specify an endpoint group name in . + /// + public interface IEndpointGroupNameMetadata + { + /// + /// Gets the endpoint group name. + /// + string EndpointGroupName { get; } + } +} From 1eefa01d7e39140d54863f69fd62f117b260fdab Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Wed, 28 Jul 2021 13:02:17 -0500 Subject: [PATCH 04/20] Add OpenAPI extension methods to resolve #33924 --- ...nApiEndpointConventionBuilderExtensions.cs | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs diff --git a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs new file mode 100644 index 000000000000..93a044ebafe3 --- /dev/null +++ b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs @@ -0,0 +1,81 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Builder; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Extension methods for adding response type metadata to endpoints. + /// + public static class OpenApiEndpointConventionBuilderExtensions + { + /// + /// Adds metadata indicating the type of response an endpoint produces. + /// + /// The type of the response. + /// The . + /// The response status code. Defatuls to StatusCodes.Status200OK. + /// The response content type. Defaults to "application/json" + /// Additional response content types the endpoint produces for the supplied status code. + /// A that can be used to further customize the endpoint. + public static IEndpointConventionBuilder Produces(this IEndpointConventionBuilder builder, + int statusCode = StatusCodes.Status200OK, + string? contentType = "application/json", + params string[] additionalContentTypes) + { + return Produces(builder, statusCode, typeof(TResponse), contentType, additionalContentTypes); + } + + /// + /// Adds metadata indicating the type of response an endpoint produces. + /// + /// The . + /// The response status code. Defatuls to StatusCodes.Status200OK. + /// 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. + /// A that can be used to further customize the endpoint. + public static IEndpointConventionBuilder Produces(this IEndpointConventionBuilder builder, + int statusCode = StatusCodes.Status200OK, + Type? responseType = null, + string? contentType = null, + params string[] additionalContentTypes) + { + if (responseType is Type && string.IsNullOrEmpty(contentType)) + { + contentType = "application/json"; + } + + builder.WithMetadata(new ProducesResponseTypeAttribute(responseType ?? typeof(void), statusCode, contentType, additionalContentTypes)); + + return builder; + } + + /// + /// Adds metadata indicating that the endpoint produces a Problem Details response. + /// + /// The . + /// The response status code. Defatuls to StatusCodes.Status500InternalServerError. + /// The response content type. Defaults to "application/problem+json". + /// A that can be used to further customize the endpoint. + public static IEndpointConventionBuilder ProducesProblem(this IEndpointConventionBuilder builder, + int statusCode = StatusCodes.Status500InternalServerError, + string contentType = "application/problem+json") + { + return Produces(builder, statusCode, contentType); + } + + /// + /// Adds metadata indicating that the endpoint produces a ProblemDetails response for validation errors. + /// + /// The . + /// The response status code. Defatuls to StatusCodes.Status400BadRequest. + /// The response content type. Defaults to "application/problem+json". + /// A that can be used to further customize the endpoint. + public static IEndpointConventionBuilder ProducesValidationProblem(this IEndpointConventionBuilder builder, + int statusCode = StatusCodes.Status400BadRequest, + string contentType = "application/problem+json") + { + return Produces(builder, statusCode, contentType); + } + } +} \ No newline at end of file From a85aab157516c35f797feb8050239f707205d9a9 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Thu, 29 Jul 2021 15:14:33 -0500 Subject: [PATCH 05/20] Add tests for new OpenAPI methods --- ...EndpointConventionBuilderExtensionsTest.cs | 32 +++++++ .../EndpointMetadataApiDescriptionProvider.cs | 1 + ...pointMetadataApiDescriptionProviderTest.cs | 68 +++++++++++++++ ...nApiEndpointConventionBuilderExtensions.cs | 5 +- .../ProducesResponseTypeAttributeTests.cs | 87 +++++++++++++++++++ 5 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs diff --git a/src/Http/Routing/test/UnitTests/Builder/RoutingEndpointConventionBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RoutingEndpointConventionBuilderExtensionsTest.cs index f66cec45c520..974ae412f727 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RoutingEndpointConventionBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RoutingEndpointConventionBuilderExtensionsTest.cs @@ -115,6 +115,38 @@ public void WithMetadata_ChainedCall_ReturnedBuilderIsDerivedType() Assert.True(chainedBuilder.TestProperty); } + [Fact] + public void WithName_SetsEndpointName() + { + // Arrange + var builder = CreateBuilder(); + + // Act + builder.WithName("SomeEndpointName"); + + // Assert + var endpoint = builder.Build(); + + var endpointName = endpoint.Metadata.GetMetadata(); + Assert.Equal("SomeEndpointName", endpointName.EndpointName); + } + + [Fact] + public void WithGroupName_SetsEndpointGroupName() + { + // Arrange + var builder = CreateBuilder(); + + // Act + builder.WithGroupName("SomeEndpointGroupName"); + + // Assert + var endpoint = builder.Build(); + + var endpointGroupName = endpoint.Metadata.GetMetadata(); + Assert.Equal("SomeEndpointGroupName", endpointGroupName.EndpointGroupName); + } + private TestEndpointConventionBuilder CreateBuilder() { var conventionBuilder = new DefaultEndpointConventionBuilder(new RouteEndpointBuilder( diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs index 0eccb64e7b35..92e140619a7e 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs @@ -89,6 +89,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string var apiDescription = new ApiDescription { HttpMethod = httpMethod, + GroupName = routeEndpoint.Metadata.GetMetadata()?.EndpointGroupName, RelativePath = routeEndpoint.RoutePattern.RawText?.TrimStart('/'), ActionDescriptor = new ActionDescriptor { diff --git a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs index afd698344d74..16c0d3dd1997 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs @@ -8,6 +8,7 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -358,6 +359,56 @@ public void AddsMetadataFromRouteEndpoint() Assert.True(apiExplorerSettings.IgnoreApi); } + [Fact] + public void RespectsProducesProblemExtensionMethod() + { + // Arrange + var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(null)); + builder.MapGet("/api/todos", () => "").ProducesProblem(); + var context = new ApiDescriptionProviderContext(Array.Empty()); + + var endpointDataSource = builder.DataSources.OfType().Single(); + var hostEnvironment = new HostEnvironment + { + ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) + }; + var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var apiDescription = Assert.Single(context.Results); + var responseTypes = Assert.Single(apiDescription.SupportedResponseTypes); + Assert.Equal(typeof(ProblemDetails), responseTypes.Type); + } + + [Fact] + public void RespectsProducesWithGroupNameExtensionMethod() + { + // Arrange + var endpointGroupName = "SomeEndpointGroupName"; + var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(null)); + builder.MapGet("/api/todos", () => "").Produces().WithGroupName(endpointGroupName); + var context = new ApiDescriptionProviderContext(Array.Empty()); + + var endpointDataSource = builder.DataSources.OfType().Single(); + var hostEnvironment = new HostEnvironment + { + ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) + }; + var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var apiDescription = Assert.Single(context.Results); + var responseTypes = Assert.Single(apiDescription.SupportedResponseTypes); + Assert.Equal(typeof(InferredJsonClass), responseTypes.Type); + Assert.Equal(endpointGroupName, apiDescription.GroupName); + } + private IList GetApiDescriptions( Delegate action, string pattern = null, @@ -423,5 +474,22 @@ private class HostEnvironment : IHostEnvironment public string ContentRootPath { get; set; } public IFileProvider ContentRootFileProvider { get; set; } } + + private class TestEndpointRouteBuilder : IEndpointRouteBuilder + { + public TestEndpointRouteBuilder(IApplicationBuilder applicationBuilder) + { + ApplicationBuilder = applicationBuilder ?? throw new ArgumentNullException(nameof(applicationBuilder)); + DataSources = new List(); + } + + public IApplicationBuilder ApplicationBuilder { get; } + + public IApplicationBuilder CreateApplicationBuilder() => ApplicationBuilder.New(); + + public ICollection DataSources { get; } + + public IServiceProvider ServiceProvider => ApplicationBuilder.ApplicationServices; + } } } diff --git a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs index 93a044ebafe3..3ef2ae85bcd7 100644 --- a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs +++ b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs @@ -1,5 +1,8 @@ -using Microsoft.AspNetCore.Mvc; +// 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.Mvc; namespace Microsoft.AspNetCore.Http { diff --git a/src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs b/src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs new file mode 100644 index 000000000000..7c5ddd312b8d --- /dev/null +++ b/src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Primitives; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc +{ + public class ProducesResponseTypeAttributeTests + { + [Fact] + public void ProducesResponseTypeAttribute_SetsContentType() + { + // Arrange + var mediaType1 = new StringSegment("application/json"); + var mediaType2 = new StringSegment("text/json;charset=utf-8"); + var producesContentAttribute = new ProducesResponseTypeAttribute(typeof(void), StatusCodes.Status200OK, "application/json", "text/json;charset=utf-8"); + + // Assert + Assert.Equal(2, producesContentAttribute.ContentTypes.Count); + MediaTypeAssert.Equal(mediaType1, producesContentAttribute.ContentTypes[0]); + MediaTypeAssert.Equal(mediaType2, producesContentAttribute.ContentTypes[1]); + } + + [Theory] + [InlineData("application/*", "application/*")] + [InlineData("application/xml, application/*, application/json", "application/*")] + [InlineData("application/*, application/json", "application/*")] + + [InlineData("*/*", "*/*")] + [InlineData("application/xml, */*, application/json", "*/*")] + [InlineData("*/*, application/json", "*/*")] + [InlineData("application/*+json", "application/*+json")] + [InlineData("application/json;v=1;*", "application/json;v=1;*")] + public void ProducesResponseTypeAttribute_InvalidContentType_Throws(string content, string invalidContentType) + { + // Act + var contentTypes = content.Split(',').Select(contentType => contentType.Trim()).ToArray(); + + // Assert + var ex = Assert.Throws( + () => new ProducesResponseTypeAttribute(typeof(void), StatusCodes.Status200OK, contentTypes[0], contentTypes.Skip(1).ToArray())); + + Assert.Equal( + $"Content types with wild cards are not supported.", + ex.Message); + } + + [Fact] + public void ProducesResponseTypeAttribute_WithTypeOnly_SetsTypeProperty() + { + // Arrange + var producesResponseTypeAttribute = new ProducesResponseTypeAttribute(typeof(Person), StatusCodes.Status200OK); + + // Act and Assert + Assert.NotNull(producesResponseTypeAttribute.Type); + Assert.Same(typeof(Person), producesResponseTypeAttribute.Type); + } + + [Fact] + public void ProducesResponseTypeAttribute_WithTypeOnly_DoesNotSetContentTypes() + { + // Arrange + var producesResponseTypeAttribute = new ProducesResponseTypeAttribute(typeof(Person), StatusCodes.Status200OK); + + // Act and Assert + Assert.NotNull(producesResponseTypeAttribute.ContentTypes); + Assert.Empty(producesResponseTypeAttribute.ContentTypes); + } + + private class Person + { + public int Id { get; set; } + + public string Name { get; set; } + } + } +} From d21964ef924de024278b28cb55630c4b76d27213 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Thu, 29 Jul 2021 15:27:32 -0500 Subject: [PATCH 06/20] Add endpoint metadata attributes --- .../Routing/src/EndpointGroupNameAttribute.cs | 34 +++++++++++++++++ src/Http/Routing/src/EndpointNameAttribute.cs | 38 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 src/Http/Routing/src/EndpointGroupNameAttribute.cs create mode 100644 src/Http/Routing/src/EndpointNameAttribute.cs diff --git a/src/Http/Routing/src/EndpointGroupNameAttribute.cs b/src/Http/Routing/src/EndpointGroupNameAttribute.cs new file mode 100644 index 000000000000..277ada39bb92 --- /dev/null +++ b/src/Http/Routing/src/EndpointGroupNameAttribute.cs @@ -0,0 +1,34 @@ + // Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.Http; + + namespace Microsoft.AspNetCore.Routing + { + /// + /// Specifies the endpoint group name in Microsoft.AspNetCore.Http.Endpoint.Metadata. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate, Inherited = false, AllowMultiple = false)] + public sealed class EndpointGroupNameAttribute : Attribute, IEndpointGroupNameMetadata + { + /// + /// Initializes an instance of the EndpointGroupNameAttribute. + /// + /// The endpoint name. + public EndpointGroupNameAttribute(string endpointGroupName) + { + if (endpointGroupName == null) + { + throw new ArgumentNullException(nameof(endpointGroupName)); + } + + EndpointGroupName = endpointGroupName; + } + + /// + /// The endpoint group name. + /// + public string EndpointGroupName { get; } + } + } \ No newline at end of file diff --git a/src/Http/Routing/src/EndpointNameAttribute.cs b/src/Http/Routing/src/EndpointNameAttribute.cs new file mode 100644 index 000000000000..9fd6fc72e295 --- /dev/null +++ b/src/Http/Routing/src/EndpointNameAttribute.cs @@ -0,0 +1,38 @@ + // Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.Http; + + namespace Microsoft.AspNetCore.Routing + { + /// + /// Specifies the endpoint name in Microsoft.AspNetCore.Http.Endpoint.Metadata. + /// + /// + /// Endpoint names must be unique within an application, and can be used to unambiguously + /// identify a desired endpoint for URI generation using Microsoft.AspNetCore.Routing.LinkGenerator. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate, Inherited = false, AllowMultiple = false)] + public sealed class EndpointNameAttribute : Attribute, IEndpointNameMetadata + { + /// + /// Initializes an instance of the EndpointNameAttribute. + /// + /// The endpoint name. + public EndpointNameAttribute(string endpointName) + { + if (endpointName == null) + { + throw new ArgumentNullException(nameof(endpointName)); + } + + EndpointName = endpointName; + } + + /// + /// The endpoint name. + /// + public string EndpointName { get; } + } + } \ No newline at end of file From 57f36ecd5114fcc59d751706c1bb8a1ea9940e15 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Thu, 29 Jul 2021 17:47:15 -0500 Subject: [PATCH 07/20] Update PublicAPI files with deltas --- src/Http/Routing/src/PublicAPI.Unshipped.txt | 13 +++++++++++++ .../src/Microsoft.AspNetCore.Mvc.Core.csproj | 1 + 2 files changed, 14 insertions(+) diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt index 99913da6a3da..7cb3206e0fab 100644 --- a/src/Http/Routing/src/PublicAPI.Unshipped.txt +++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt @@ -23,3 +23,16 @@ static Microsoft.AspNetCore.Builder.MinimalActionEndpointRouteBuilderExtensions. static Microsoft.AspNetCore.Builder.MinimalActionEndpointRouteBuilderExtensions.MapMethods(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Collections.Generic.IEnumerable! httpMethods, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! static Microsoft.AspNetCore.Builder.MinimalActionEndpointRouteBuilderExtensions.MapPost(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! static Microsoft.AspNetCore.Builder.MinimalActionEndpointRouteBuilderExtensions.MapPut(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! +Microsoft.AspNetCore.Routing.IEndpointGroupNameMetadata +Microsoft.AspNetCore.Routing.IEndpointGroupNameMetadata.EndpointGroupName.get -> string! +Microsoft.AspNetCore.Routing.EndpointGroupNameMetadata +Microsoft.AspNetCore.Routing.EndpointGroupNameMetadata.EndpointGroupNameMetadata(string! endpointGroupName) -> void +Microsoft.AspNetCore.Routing.EndpointGroupNameMetadata.EndpointGroupName.get -> string! +Microsoft.AspNetCore.Routing.EndpointGroupNameAttribute +Microsoft.AspNetCore.Routing.EndpointGroupNameAttribute.EndpointGroupNameAttribute(string! endpointGroupName) -> void +Microsoft.AspNetCore.Routing.EndpointGroupNameAttribute.EndpointGroupName.get -> string! +Microsoft.AspNetCore.Routing.EndpointNameAttribute +Microsoft.AspNetCore.Routing.EndpointNameAttribute.EndpointNameAttribute(string! endpointName) -> void +Microsoft.AspNetCore.Routing.EndpointNameAttribute.EndpointName.get -> string! +static Microsoft.AspNetCore.Builder.RoutingEndpointConventionBuilderExtensions.WithName(this TBuilder builder, string! endpointName) -> TBuilder +static Microsoft.AspNetCore.Builder.RoutingEndpointConventionBuilderExtensions.WithGroupName(this TBuilder builder, string! endpointGroupName) -> TBuilder \ No newline at end of file diff --git a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj index b2134e5d98d9..2fdedc645502 100644 --- a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj @@ -16,6 +16,7 @@ Microsoft.AspNetCore.Mvc.RouteAttribute aspnetcore;aspnetcoremvc false enable + RS0016;RS0026 From 5add62361037084f28d67dbb1f58945f378e80f9 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Thu, 29 Jul 2021 19:20:56 -0500 Subject: [PATCH 08/20] Add support for SuppressApi to close #34068 --- src/Http/Routing/src/ISuppressApiMetadata.cs | 23 +++++++++++++++++++ src/Http/Routing/src/PublicAPI.Unshipped.txt | 7 +++++- src/Http/Routing/src/SuppressApiMetadata.cs | 23 +++++++++++++++++++ .../EndpointMetadataApiDescriptionProvider.cs | 4 +++- ...pointMetadataApiDescriptionProviderTest.cs | 22 ++++++++++++++++++ ...nApiEndpointConventionBuilderExtensions.cs | 14 +++++++++++ 6 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 src/Http/Routing/src/ISuppressApiMetadata.cs create mode 100644 src/Http/Routing/src/SuppressApiMetadata.cs diff --git a/src/Http/Routing/src/ISuppressApiMetadata.cs b/src/Http/Routing/src/ISuppressApiMetadata.cs new file mode 100644 index 000000000000..ab8375800888 --- /dev/null +++ b/src/Http/Routing/src/ISuppressApiMetadata.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// Specifies an endpoint name in . + /// + /// + /// Endpoint names must be unique within an application, and can be used to unambiguously + /// identify a desired endpoint for URI generation using . + /// + public interface ISuppressApiMetadata + { + /// + /// Gets the endpoint name. + /// + bool SuppressApi { get; } + } +} diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt index 7cb3206e0fab..1e4abdff2bee 100644 --- a/src/Http/Routing/src/PublicAPI.Unshipped.txt +++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt @@ -35,4 +35,9 @@ Microsoft.AspNetCore.Routing.EndpointNameAttribute Microsoft.AspNetCore.Routing.EndpointNameAttribute.EndpointNameAttribute(string! endpointName) -> void Microsoft.AspNetCore.Routing.EndpointNameAttribute.EndpointName.get -> string! static Microsoft.AspNetCore.Builder.RoutingEndpointConventionBuilderExtensions.WithName(this TBuilder builder, string! endpointName) -> TBuilder -static Microsoft.AspNetCore.Builder.RoutingEndpointConventionBuilderExtensions.WithGroupName(this TBuilder builder, string! endpointGroupName) -> TBuilder \ No newline at end of file +static Microsoft.AspNetCore.Builder.RoutingEndpointConventionBuilderExtensions.WithGroupName(this TBuilder builder, string! endpointGroupName) -> TBuilder +Microsoft.AspNetCore.Routing.ISuppressApiMetadata +Microsoft.AspNetCore.Routing.ISuppressApiMetadata.SuppressApi.get -> bool +Microsoft.AspNetCore.Routing.SuppressApiMetadata +Microsoft.AspNetCore.Routing.SuppressApiMetadata.SuppressApiMetadata() -> void +Microsoft.AspNetCore.Routing.SuppressApiMetadata.SuppressApi.get -> bool diff --git a/src/Http/Routing/src/SuppressApiMetadata.cs b/src/Http/Routing/src/SuppressApiMetadata.cs new file mode 100644 index 000000000000..8ecf51ab0bbe --- /dev/null +++ b/src/Http/Routing/src/SuppressApiMetadata.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// Specifies an endpoint name in . + /// + /// + /// Endpoint names must be unique within an application, and can be used to unambiguously + /// identify a desired endpoint for URI generation using . + /// + public sealed class SuppressApiMetadata : ISuppressApiMetadata + { + /// + /// Gets the endpoint name. + /// + public bool SuppressApi => true; + } +} diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs index 92e140619a7e..444396f1dbdc 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs @@ -52,7 +52,9 @@ public void OnProvidersExecuting(ApiDescriptionProviderContext context) { if (endpoint is RouteEndpoint routeEndpoint && routeEndpoint.Metadata.GetMetadata() is { } methodInfo && - routeEndpoint.Metadata.GetMetadata() is { } httpMethodMetadata) + routeEndpoint.Metadata.GetMetadata() is { } httpMethodMetadata && + (routeEndpoint.Metadata.GetMetadata() == null || + routeEndpoint.Metadata.GetMetadata() is { SuppressApi: false} )) { // REVIEW: Should we add an ApiDescription for endpoints without IHttpMethodMetadata? Swagger doesn't handle // a null HttpMethod even though it's nullable on ApiDescription, so we'd need to define "default" HTTP methods. diff --git a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs index 16c0d3dd1997..735eb5b5e5fe 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs @@ -409,6 +409,28 @@ public void RespectsProducesWithGroupNameExtensionMethod() Assert.Equal(endpointGroupName, apiDescription.GroupName); } + [Fact] + public void RespectsSuppressApiMethod() + { + // Arrange + var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(null)); + builder.MapGet("/api/todos", () => "").Produces().SuppressApi(); + var context = new ApiDescriptionProviderContext(Array.Empty()); + + var endpointDataSource = builder.DataSources.OfType().Single(); + var hostEnvironment = new HostEnvironment + { + ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) + }; + var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Empty(context.Results); + } + private IList GetApiDescriptions( Delegate action, string pattern = null, diff --git a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs index 3ef2ae85bcd7..a53a795dc98f 100644 --- a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs +++ b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Http { @@ -11,6 +12,19 @@ namespace Microsoft.AspNetCore.Http /// public static class OpenApiEndpointConventionBuilderExtensions { + /// + /// Adds metadata to support suppressing OpenAPI documentation from + /// being generated for this endpoint. + /// + /// The . + /// A that can be used to further customize the endpoint. + public static IEndpointConventionBuilder SuppressApi(this IEndpointConventionBuilder builder) + { + builder.WithMetadata(new SuppressApiMetadata()); + + return builder; + } + /// /// Adds metadata indicating the type of response an endpoint produces. /// From 3940f1f925fc0700607f9aae5d2f0574ecd9531b Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 30 Jul 2021 18:56:53 -0500 Subject: [PATCH 09/20] Update tests to account for supporting setting content types --- .../test/DefaultApiDescriptionProviderTest.cs | 10 +++++----- .../test/Mvc.FunctionalTests/ApiExplorerTest.cs | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs index c3492411df05..5de26a83b22a 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs @@ -567,7 +567,7 @@ public void GetApiDescription_ReturnsActionResultWithProduces_And_ProducesConten // Arrange var action = CreateActionDescriptor(methodName, controllerType); action.FilterDescriptors = filterDescriptors; - var expectedMediaTypes = new[] { "application/json", "text/json" }; + var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; // Act var descriptions = GetApiDescriptions(action); @@ -677,7 +677,7 @@ public void GetApiDescription_ReturnsVoidWithProducesContentType( // Arrange var action = CreateActionDescriptor(methodName, controllerType); action.FilterDescriptors = filterDescriptors; - var expectedMediaTypes = new[] { "application/json", "text/json" }; + var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; // Act var descriptions = GetApiDescriptions(action); @@ -740,7 +740,7 @@ public void GetApiDescription_ReturnsActionResultOfTWithProducesContentType( new ProducesResponseTypeAttribute(typeof(ErrorDetails), 500), FilterScope.Action) }; - var expectedMediaTypes = new[] { "application/json", "text/json" }; + var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; // Act var descriptions = GetApiDescriptions(action); @@ -810,7 +810,7 @@ public void GetApiDescription_ReturnsActionResultOfTWithProducesContentType_ForS new ProducesResponseTypeAttribute(typeof(ErrorDetails), 500), FilterScope.Action) }; - var expectedMediaTypes = new[] { "application/json", "text/json" }; + var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; // Act var descriptions = GetApiDescriptions(action); @@ -880,7 +880,7 @@ public void GetApiDescription_ReturnsActionResultOfSequenceOfTWithProducesConten new ProducesResponseTypeAttribute(typeof(ErrorDetails), 500), FilterScope.Action) }; - var expectedMediaTypes = new[] { "application/json", "text/json" }; + var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; // Act var descriptions = GetApiDescriptions(action); diff --git a/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs b/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs index f9432bcd55b4..b0ba16f331ed 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs @@ -607,7 +607,7 @@ public async Task ExplicitResponseTypeDecoration_SuppressesDefaultStatus_AlsoHon // Arrange var type1 = typeof(ApiExplorerWebSite.Product).FullName; var type2 = typeof(SerializableError).FullName; - var expectedMediaTypes = new[] { "text/xml" }; + var expectedMediaTypes = new[] { "application/xml", "text/xml", "application/json", "text/json" }; // Act var response = await Client.GetAsync( @@ -671,7 +671,7 @@ public async Task ExplicitResponseTypeDecoration_WithExplicitDefaultStatus_Speci // Arrange var type1 = typeof(ApiExplorerWebSite.Product).FullName; var type2 = typeof(SerializableError).FullName; - var expectedMediaTypes = new[] { "text/xml" }; + var expectedMediaTypes = new[] { "application/xml", "text/xml", "application/json", "text/json" }; // Act var response = await Client.GetAsync( @@ -696,12 +696,14 @@ public async Task ExplicitResponseTypeDecoration_WithExplicitDefaultStatus_Speci expectedMediaTypes, responseType.ResponseFormats.Select(responseFormat => responseFormat.MediaType).ToArray()); } + [Fact] public async Task ApiExplorer_ResponseType_InheritingFromController() { // Arrange var type = "ApiExplorerWebSite.Product"; var errorType = "ApiExplorerWebSite.ErrorInfo"; + var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; // Act var response = await Client.GetAsync( @@ -719,15 +721,13 @@ public async Task ApiExplorer_ResponseType_InheritingFromController() { Assert.Equal(type, responseType.ResponseType); Assert.Equal(200, responseType.StatusCode); - var responseFormat = Assert.Single(responseType.ResponseFormats); - Assert.Equal("application/json", responseFormat.MediaType); + Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType)); }, responseType => { Assert.Equal(errorType, responseType.ResponseType); Assert.Equal(500, responseType.StatusCode); - var responseFormat = Assert.Single(responseType.ResponseFormats); - Assert.Equal("application/json", responseFormat.MediaType); + Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType)); }); } @@ -1252,7 +1252,7 @@ public async Task ApiConvention_ForGetMethodThatDoesNotMatchConvention() public async Task ApiConvention_ForMethodWithResponseTypeAttributes() { // Arrange - var expectedMediaTypes = new[] { "application/json" }; + var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; // Act var response = await Client.PostAsync( @@ -1318,7 +1318,7 @@ public async Task ApiConvention_ForPostMethodThatMatchesConvention() public async Task ApiConvention_ForPostActionWithProducesAttribute() { // Arrange - var expectedMediaTypes = new[] { "application/json", "text/json", }; + var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; // Act var response = await Client.PostAsync( From 96cec514be751defdb0bd3e29d567e78ae0e169e Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Sat, 31 Jul 2021 10:17:09 -0500 Subject: [PATCH 10/20] Fix up PublicAPI analyzer warnings --- .../Routing/src/EndpointGroupNameAttribute.cs | 4 ++-- .../Routing/src/EndpointGroupNameMetadata.cs | 8 ++++---- src/Http/Routing/src/EndpointNameAttribute.cs | 6 +++--- .../Routing/src/IEndpointGroupNameMetadata.cs | 2 +- ...nApiEndpointConventionBuilderExtensions.cs | 19 ++++++++++++------- .../src/Microsoft.AspNetCore.Mvc.Core.csproj | 1 - src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 11 ++++++++++- 7 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/Http/Routing/src/EndpointGroupNameAttribute.cs b/src/Http/Routing/src/EndpointGroupNameAttribute.cs index 277ada39bb92..e9e5124ca37e 100644 --- a/src/Http/Routing/src/EndpointGroupNameAttribute.cs +++ b/src/Http/Routing/src/EndpointGroupNameAttribute.cs @@ -1,4 +1,4 @@ - // Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.Routing { /// - /// Specifies the endpoint group name in Microsoft.AspNetCore.Http.Endpoint.Metadata. + /// Specifies the endpoint group name in . /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate, Inherited = false, AllowMultiple = false)] public sealed class EndpointGroupNameAttribute : Attribute, IEndpointGroupNameMetadata diff --git a/src/Http/Routing/src/EndpointGroupNameMetadata.cs b/src/Http/Routing/src/EndpointGroupNameMetadata.cs index 8073c1f1f73a..bca2895c1f62 100644 --- a/src/Http/Routing/src/EndpointGroupNameMetadata.cs +++ b/src/Http/Routing/src/EndpointGroupNameMetadata.cs @@ -7,14 +7,14 @@ namespace Microsoft.AspNetCore.Routing { /// - /// Specifies an endpoint group name in . + /// Specifies an endpoint group name in . /// public class EndpointGroupNameMetadata : IEndpointGroupNameMetadata { /// - /// Creates a new instance of with the provided endpoint name. + /// Creates a new instance of with the provided endpoint group name. /// - /// The endpoint name. + /// The endpoint group name. public EndpointGroupNameMetadata(string endpointGroupName) { if (endpointGroupName == null) @@ -26,7 +26,7 @@ public EndpointGroupNameMetadata(string endpointGroupName) } /// - /// Gets the endpoint name. + /// Gets the endpoint group name. /// public string EndpointGroupName { get; } } diff --git a/src/Http/Routing/src/EndpointNameAttribute.cs b/src/Http/Routing/src/EndpointNameAttribute.cs index 9fd6fc72e295..af4b47f4aff7 100644 --- a/src/Http/Routing/src/EndpointNameAttribute.cs +++ b/src/Http/Routing/src/EndpointNameAttribute.cs @@ -1,4 +1,4 @@ - // Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -7,11 +7,11 @@ namespace Microsoft.AspNetCore.Routing { /// - /// Specifies the endpoint name in Microsoft.AspNetCore.Http.Endpoint.Metadata. + /// Specifies the endpoint name in . /// /// /// Endpoint names must be unique within an application, and can be used to unambiguously - /// identify a desired endpoint for URI generation using Microsoft.AspNetCore.Routing.LinkGenerator. + /// identify a desired endpoint for URI generation using /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate, Inherited = false, AllowMultiple = false)] public sealed class EndpointNameAttribute : Attribute, IEndpointNameMetadata diff --git a/src/Http/Routing/src/IEndpointGroupNameMetadata.cs b/src/Http/Routing/src/IEndpointGroupNameMetadata.cs index cddf566111cf..08d7fefc63d3 100644 --- a/src/Http/Routing/src/IEndpointGroupNameMetadata.cs +++ b/src/Http/Routing/src/IEndpointGroupNameMetadata.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.Routing { /// - /// Defines a contract use to specify an endpoint group name in . + /// Defines a contract used to specify an endpoint group name in . /// public interface IEndpointGroupNameMetadata { diff --git a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs index a53a795dc98f..ffaa95cbc62b 100644 --- a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs +++ b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs @@ -12,6 +12,7 @@ namespace Microsoft.AspNetCore.Http /// public static class OpenApiEndpointConventionBuilderExtensions { + private static readonly SuppressApiMetadata _suppressApiMetadata = new(); /// /// Adds metadata to support suppressing OpenAPI documentation from /// being generated for this endpoint. @@ -20,7 +21,7 @@ public static class OpenApiEndpointConventionBuilderExtensions /// A that can be used to further customize the endpoint. public static IEndpointConventionBuilder SuppressApi(this IEndpointConventionBuilder builder) { - builder.WithMetadata(new SuppressApiMetadata()); + builder.WithMetadata(_suppressApiMetadata); return builder; } @@ -30,11 +31,13 @@ public static IEndpointConventionBuilder SuppressApi(this IEndpointConventionBui /// /// The type of the response. /// The . - /// The response status code. Defatuls to StatusCodes.Status200OK. - /// The response content type. Defaults to "application/json" + /// The response status code. Defaults to StatusCodes.Status200OK. + /// The response content type. Defaults to "application/json". /// 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 IEndpointConventionBuilder Produces(this IEndpointConventionBuilder builder, +#pragma warning restore RS0026 int statusCode = StatusCodes.Status200OK, string? contentType = "application/json", params string[] additionalContentTypes) @@ -46,12 +49,14 @@ public static IEndpointConventionBuilder Produces(this IEndpointConve /// Adds metadata indicating the type of response an endpoint produces. /// /// The . - /// The response status code. Defatuls to StatusCodes.Status200OK. + /// The response status code. Defaults to StatusCodes.Status200OK. /// 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. /// A that can be used to further customize the endpoint. +#pragma warning disable RS0026 public static IEndpointConventionBuilder Produces(this IEndpointConventionBuilder builder, +#pragma warning restore RS0026 int statusCode = StatusCodes.Status200OK, Type? responseType = null, string? contentType = null, @@ -71,7 +76,7 @@ public static IEndpointConventionBuilder Produces(this IEndpointConventionBuilde /// Adds metadata indicating that the endpoint produces a Problem Details response. /// /// The . - /// The response status code. Defatuls to StatusCodes.Status500InternalServerError. + /// The response status code. Defaults to StatusCodes.Status500InternalServerError. /// The response content type. Defaults to "application/problem+json". /// A that can be used to further customize the endpoint. public static IEndpointConventionBuilder ProducesProblem(this IEndpointConventionBuilder builder, @@ -85,7 +90,7 @@ public static IEndpointConventionBuilder ProducesProblem(this IEndpointConventio /// Adds metadata indicating that the endpoint produces a ProblemDetails response for validation errors. /// /// The . - /// The response status code. Defatuls to StatusCodes.Status400BadRequest. + /// The response status code. Defaults to StatusCodes.Status400BadRequest. /// The response content type. Defaults to "application/problem+json". /// A that can be used to further customize the endpoint. public static IEndpointConventionBuilder ProducesValidationProblem(this IEndpointConventionBuilder builder, @@ -95,4 +100,4 @@ public static IEndpointConventionBuilder ProducesValidationProblem(this IEndpoin return Produces(builder, statusCode, contentType); } } -} \ No newline at end of file +} diff --git a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj index 2fdedc645502..b2134e5d98d9 100644 --- a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj @@ -16,7 +16,6 @@ Microsoft.AspNetCore.Mvc.RouteAttribute aspnetcore;aspnetcoremvc false enable - RS0016;RS0026 diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index 7b9282c00c38..ecd2af933a80 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -3219,4 +3219,13 @@ virtual Microsoft.AspNetCore.Mvc.ProducesAttribute.OnResultExecuted(Microsoft.As virtual Microsoft.AspNetCore.Mvc.ProducesAttribute.OnResultExecuting(Microsoft.AspNetCore.Mvc.Filters.ResultExecutingContext! context) -> void virtual Microsoft.AspNetCore.Mvc.RequireHttpsAttribute.HandleNonHttpsRequest(Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext! filterContext) -> void virtual Microsoft.AspNetCore.Mvc.RequireHttpsAttribute.OnAuthorization(Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext! filterContext) -> void - +Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute.ProducesResponseTypeAttribute(System.Type! type, int statusCode, string? contentType, params string![]! additionalContentTypes) -> void +Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute.ContentTypes.get -> Microsoft.AspNetCore.Mvc.Formatters.MediaTypeCollection! +Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute.ContentTypes.set -> void +Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute.SetContentTypes(Microsoft.AspNetCore.Mvc.Formatters.MediaTypeCollection! contentTypes) -> void +Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.SuppressApi(this Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! builder) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Produces(this Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! builder, int statusCode = 200, string? contentType = "application/json", params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Produces(this Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! builder, int statusCode = 200, System.Type? responseType = null, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ProducesProblem(this Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! builder, int statusCode = 500, string! contentType = "application/problem+json") -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ProducesValidationProblem(this Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! builder, int statusCode = 400, string! contentType = "application/problem+json") -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! From d5e6e73c6cb09ca9e8df72b92a6c55cc0de59de9 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Sat, 31 Jul 2021 13:14:08 -0500 Subject: [PATCH 11/20] Clean up source files --- src/Http/Routing/src/ISuppressApiMetadata.cs | 9 +++------ src/Http/Routing/src/SuppressApiMetadata.cs | 9 +++------ src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs | 2 +- .../Mvc.Core/test/ProducesResponseTypeAttributeTests.cs | 2 +- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/Http/Routing/src/ISuppressApiMetadata.cs b/src/Http/Routing/src/ISuppressApiMetadata.cs index ab8375800888..08fc97664e9d 100644 --- a/src/Http/Routing/src/ISuppressApiMetadata.cs +++ b/src/Http/Routing/src/ISuppressApiMetadata.cs @@ -7,16 +7,13 @@ namespace Microsoft.AspNetCore.Routing { /// - /// Specifies an endpoint name in . + /// Indicates that API explorer data should not be emitted for this endpoint. /// - /// - /// Endpoint names must be unique within an application, and can be used to unambiguously - /// identify a desired endpoint for URI generation using . - /// public interface ISuppressApiMetadata { /// - /// Gets the endpoint name. + /// Gets a value indicating whether API explorer + /// data should be emitted for this endpoint. /// bool SuppressApi { get; } } diff --git a/src/Http/Routing/src/SuppressApiMetadata.cs b/src/Http/Routing/src/SuppressApiMetadata.cs index 8ecf51ab0bbe..d40cf7a9983a 100644 --- a/src/Http/Routing/src/SuppressApiMetadata.cs +++ b/src/Http/Routing/src/SuppressApiMetadata.cs @@ -7,16 +7,13 @@ namespace Microsoft.AspNetCore.Routing { /// - /// Specifies an endpoint name in . + /// Indicates that API explorer data should not be emitted for this endpoint. /// - /// - /// Endpoint names must be unique within an application, and can be used to unambiguously - /// identify a desired endpoint for URI generation using . - /// public sealed class SuppressApiMetadata : ISuppressApiMetadata { /// - /// Gets the endpoint name. + /// Gets a value indicating whether API explorer + /// data should be emitted for this endpoint. /// public bool SuppressApi => true; } diff --git a/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs b/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs index ef9af810858d..85ddbb2bf291 100644 --- a/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs +++ b/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs @@ -115,7 +115,7 @@ private MediaTypeCollection GetContentTypes(string contentType, string[] additio var mediaType = new MediaType(type); if (mediaType.HasWildcard) { - throw new InvalidOperationException("Content types with wild cards are not supported."); + throw new InvalidOperationException("Content types with wildcards are not supported."); } contentTypes.Add(type); diff --git a/src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs b/src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs index 7c5ddd312b8d..bbca19807a3d 100644 --- a/src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs +++ b/src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs @@ -51,7 +51,7 @@ public void ProducesResponseTypeAttribute_InvalidContentType_Throws(string conte () => new ProducesResponseTypeAttribute(typeof(void), StatusCodes.Status200OK, contentTypes[0], contentTypes.Skip(1).ToArray())); Assert.Equal( - $"Content types with wild cards are not supported.", + $"Content types with wildcards are not supported.", ex.Message); } From 14dbfab88c186bb9d7bdeba3f5823d87d5454bbf Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 2 Aug 2021 14:53:11 -0500 Subject: [PATCH 12/20] Address feedback from API review --- ...tingEndpointConventionBuilderExtensions.cs | 8 ++-- .../Routing/src/EndpointGroupNameAttribute.cs | 2 +- .../Routing/src/EndpointGroupNameMetadata.cs | 33 -------------- .../src/ExclueFromApiExplorerAttribute.cs | 22 ++++++++++ ...a.cs => IExclueFromApiExplorerMetadata.cs} | 6 +-- src/Http/Routing/src/PublicAPI.Unshipped.txt | 13 +++--- src/Http/Routing/src/SuppressApiMetadata.cs | 20 --------- ...EndpointConventionBuilderExtensionsTest.cs | 4 +- .../EndpointMetadataApiDescriptionProvider.cs | 6 +-- ...pointMetadataApiDescriptionProviderTest.cs | 2 +- ...nApiEndpointConventionBuilderExtensions.cs | 43 +++++++++++-------- .../src/ProducesResponseTypeAttribute.cs | 38 ++++++++-------- src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 15 +++---- 13 files changed, 89 insertions(+), 123 deletions(-) delete mode 100644 src/Http/Routing/src/EndpointGroupNameMetadata.cs create mode 100644 src/Http/Routing/src/ExclueFromApiExplorerAttribute.cs rename src/Http/Routing/src/{ISuppressApiMetadata.cs => IExclueFromApiExplorerMetadata.cs} (70%) delete mode 100644 src/Http/Routing/src/SuppressApiMetadata.cs diff --git a/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs b/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs index 5c00024da242..b556a557c432 100644 --- a/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs @@ -120,7 +120,7 @@ public static TBuilder WithMetadata(this TBuilder builder, params obje } /// - /// Sets the for all endpoints produced + /// Sets the for all endpoints produced /// on the target . /// /// The . @@ -128,12 +128,12 @@ public static TBuilder WithMetadata(this TBuilder builder, params obje /// The . public static TBuilder WithName(this TBuilder builder, string endpointName) where TBuilder : IEndpointConventionBuilder { - builder.WithMetadata(new EndpointNameMetadata(endpointName)); + builder.WithMetadata(new EndpointNameAttribute(endpointName)); return builder; } /// - /// Sets the for all endpoints produced + /// Sets the for all endpoints produced /// on the target . /// /// The . @@ -141,7 +141,7 @@ public static TBuilder WithName(this TBuilder builder, string endpoint /// The . public static TBuilder WithGroupName(this TBuilder builder, string endpointGroupName) where TBuilder : IEndpointConventionBuilder { - builder.WithMetadata(new EndpointGroupNameMetadata(endpointGroupName)); + builder.WithMetadata(new EndpointGroupNameAttribute(endpointGroupName)); return builder; } } diff --git a/src/Http/Routing/src/EndpointGroupNameAttribute.cs b/src/Http/Routing/src/EndpointGroupNameAttribute.cs index e9e5124ca37e..0b2472fe3308 100644 --- a/src/Http/Routing/src/EndpointGroupNameAttribute.cs +++ b/src/Http/Routing/src/EndpointGroupNameAttribute.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Routing /// /// Specifies the endpoint group name in . /// - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate, Inherited = false, AllowMultiple = false)] + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate | AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class EndpointGroupNameAttribute : Attribute, IEndpointGroupNameMetadata { /// diff --git a/src/Http/Routing/src/EndpointGroupNameMetadata.cs b/src/Http/Routing/src/EndpointGroupNameMetadata.cs deleted file mode 100644 index bca2895c1f62..000000000000 --- a/src/Http/Routing/src/EndpointGroupNameMetadata.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Routing -{ - /// - /// Specifies an endpoint group name in . - /// - public class EndpointGroupNameMetadata : IEndpointGroupNameMetadata - { - /// - /// Creates a new instance of with the provided endpoint group name. - /// - /// The endpoint group name. - public EndpointGroupNameMetadata(string endpointGroupName) - { - if (endpointGroupName == null) - { - throw new ArgumentNullException(nameof(endpointGroupName)); - } - - EndpointGroupName = endpointGroupName; - } - - /// - /// Gets the endpoint group name. - /// - public string EndpointGroupName { get; } - } -} diff --git a/src/Http/Routing/src/ExclueFromApiExplorerAttribute.cs b/src/Http/Routing/src/ExclueFromApiExplorerAttribute.cs new file mode 100644 index 000000000000..601158c1bef9 --- /dev/null +++ b/src/Http/Routing/src/ExclueFromApiExplorerAttribute.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// Indicates that this should not be included in the generated API metadata. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate, AllowMultiple = false, Inherited = true)] + public sealed class ExclueFromApiExplorerAttribute : Attribute, IExclueFromApiExplorerMetadata + { + /// + /// Gets a value indicating whether API explorer + /// data should be excluded for this endpoint. If , + /// API metadata is not emitted. + /// + public bool ExclueFromApiExplorer => true; + } +} diff --git a/src/Http/Routing/src/ISuppressApiMetadata.cs b/src/Http/Routing/src/IExclueFromApiExplorerMetadata.cs similarity index 70% rename from src/Http/Routing/src/ISuppressApiMetadata.cs rename to src/Http/Routing/src/IExclueFromApiExplorerMetadata.cs index 08fc97664e9d..1b48448852e7 100644 --- a/src/Http/Routing/src/ISuppressApiMetadata.cs +++ b/src/Http/Routing/src/IExclueFromApiExplorerMetadata.cs @@ -7,14 +7,14 @@ namespace Microsoft.AspNetCore.Routing { /// - /// Indicates that API explorer data should not be emitted for this endpoint. + /// Indicates wheter or not that API explorer data should be emitted for this endpoint. /// - public interface ISuppressApiMetadata + public interface IExclueFromApiExplorerMetadata { /// /// Gets a value indicating whether API explorer /// data should be emitted for this endpoint. /// - bool SuppressApi { get; } + bool ExclueFromApiExplorer { get; } } } diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt index 1e4abdff2bee..57edf56f4e54 100644 --- a/src/Http/Routing/src/PublicAPI.Unshipped.txt +++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt @@ -25,9 +25,6 @@ static Microsoft.AspNetCore.Builder.MinimalActionEndpointRouteBuilderExtensions. static Microsoft.AspNetCore.Builder.MinimalActionEndpointRouteBuilderExtensions.MapPut(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! Microsoft.AspNetCore.Routing.IEndpointGroupNameMetadata Microsoft.AspNetCore.Routing.IEndpointGroupNameMetadata.EndpointGroupName.get -> string! -Microsoft.AspNetCore.Routing.EndpointGroupNameMetadata -Microsoft.AspNetCore.Routing.EndpointGroupNameMetadata.EndpointGroupNameMetadata(string! endpointGroupName) -> void -Microsoft.AspNetCore.Routing.EndpointGroupNameMetadata.EndpointGroupName.get -> string! Microsoft.AspNetCore.Routing.EndpointGroupNameAttribute Microsoft.AspNetCore.Routing.EndpointGroupNameAttribute.EndpointGroupNameAttribute(string! endpointGroupName) -> void Microsoft.AspNetCore.Routing.EndpointGroupNameAttribute.EndpointGroupName.get -> string! @@ -36,8 +33,8 @@ Microsoft.AspNetCore.Routing.EndpointNameAttribute.EndpointNameAttribute(string! Microsoft.AspNetCore.Routing.EndpointNameAttribute.EndpointName.get -> string! static Microsoft.AspNetCore.Builder.RoutingEndpointConventionBuilderExtensions.WithName(this TBuilder builder, string! endpointName) -> TBuilder static Microsoft.AspNetCore.Builder.RoutingEndpointConventionBuilderExtensions.WithGroupName(this TBuilder builder, string! endpointGroupName) -> TBuilder -Microsoft.AspNetCore.Routing.ISuppressApiMetadata -Microsoft.AspNetCore.Routing.ISuppressApiMetadata.SuppressApi.get -> bool -Microsoft.AspNetCore.Routing.SuppressApiMetadata -Microsoft.AspNetCore.Routing.SuppressApiMetadata.SuppressApiMetadata() -> void -Microsoft.AspNetCore.Routing.SuppressApiMetadata.SuppressApi.get -> bool +Microsoft.AspNetCore.Routing.IExclueFromApiExplorerMetadata +Microsoft.AspNetCore.Routing.IExclueFromApiExplorerMetadata.ExclueFromApiExplorer.get -> bool +Microsoft.AspNetCore.Routing.ExclueFromApiExplorerAttribute +Microsoft.AspNetCore.Routing.ExclueFromApiExplorerAttribute.ExclueFromApiExplorerAttribute() -> void +Microsoft.AspNetCore.Routing.ExclueFromApiExplorerAttribute.ExclueFromApiExplorer.get -> bool diff --git a/src/Http/Routing/src/SuppressApiMetadata.cs b/src/Http/Routing/src/SuppressApiMetadata.cs deleted file mode 100644 index d40cf7a9983a..000000000000 --- a/src/Http/Routing/src/SuppressApiMetadata.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Routing -{ - /// - /// Indicates that API explorer data should not be emitted for this endpoint. - /// - public sealed class SuppressApiMetadata : ISuppressApiMetadata - { - /// - /// Gets a value indicating whether API explorer - /// data should be emitted for this endpoint. - /// - public bool SuppressApi => true; - } -} diff --git a/src/Http/Routing/test/UnitTests/Builder/RoutingEndpointConventionBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RoutingEndpointConventionBuilderExtensionsTest.cs index 974ae412f727..9141829aeae4 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RoutingEndpointConventionBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RoutingEndpointConventionBuilderExtensionsTest.cs @@ -127,7 +127,7 @@ public void WithName_SetsEndpointName() // Assert var endpoint = builder.Build(); - var endpointName = endpoint.Metadata.GetMetadata(); + var endpointName = endpoint.Metadata.GetMetadata(); Assert.Equal("SomeEndpointName", endpointName.EndpointName); } @@ -143,7 +143,7 @@ public void WithGroupName_SetsEndpointGroupName() // Assert var endpoint = builder.Build(); - var endpointGroupName = endpoint.Metadata.GetMetadata(); + var endpointGroupName = endpoint.Metadata.GetMetadata(); Assert.Equal("SomeEndpointGroupName", endpointGroupName.EndpointGroupName); } diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs index 444396f1dbdc..4f5ae91c7dd9 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs @@ -53,8 +53,8 @@ public void OnProvidersExecuting(ApiDescriptionProviderContext context) if (endpoint is RouteEndpoint routeEndpoint && routeEndpoint.Metadata.GetMetadata() is { } methodInfo && routeEndpoint.Metadata.GetMetadata() is { } httpMethodMetadata && - (routeEndpoint.Metadata.GetMetadata() == null || - routeEndpoint.Metadata.GetMetadata() is { SuppressApi: false} )) + (routeEndpoint.Metadata.GetMetadata() == null || + routeEndpoint.Metadata.GetMetadata() is { ExclueFromApiExplorer: false} )) { // REVIEW: Should we add an ApiDescription for endpoints without IHttpMethodMetadata? Swagger doesn't handle // a null HttpMethod even though it's nullable on ApiDescription, so we'd need to define "default" HTTP methods. @@ -91,7 +91,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string var apiDescription = new ApiDescription { HttpMethod = httpMethod, - GroupName = routeEndpoint.Metadata.GetMetadata()?.EndpointGroupName, + GroupName = routeEndpoint.Metadata.GetMetadata()?.EndpointGroupName, RelativePath = routeEndpoint.RoutePattern.RawText?.TrimStart('/'), ActionDescriptor = new ActionDescriptor { diff --git a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs index 735eb5b5e5fe..ce9d2e3bc298 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs @@ -414,7 +414,7 @@ public void RespectsSuppressApiMethod() { // Arrange var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(null)); - builder.MapGet("/api/todos", () => "").Produces().SuppressApi(); + builder.MapGet("/api/todos", () => "").Produces().ExcludeFromApiExplorer(); var context = new ApiDescriptionProviderContext(Array.Empty()); var endpointDataSource = builder.DataSources.OfType().Single(); diff --git a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs index ffaa95cbc62b..25f770c946d0 100644 --- a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs +++ b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs @@ -12,16 +12,17 @@ namespace Microsoft.AspNetCore.Http /// public static class OpenApiEndpointConventionBuilderExtensions { - private static readonly SuppressApiMetadata _suppressApiMetadata = new(); + private static readonly ExclueFromApiExplorerAttribute _excludeFromApiMetadataAttribute = new(); + /// /// Adds metadata to support suppressing OpenAPI documentation from /// being generated for this endpoint. /// - /// The . - /// A that can be used to further customize the endpoint. - public static IEndpointConventionBuilder SuppressApi(this IEndpointConventionBuilder builder) + /// The . + /// A that can be used to further customize the endpoint. + public static MinimalActionEndpointConventionBuilder ExcludeFromApiExplorer(this MinimalActionEndpointConventionBuilder builder) { - builder.WithMetadata(_suppressApiMetadata); + builder.WithMetadata(_excludeFromApiMetadataAttribute); return builder; } @@ -30,16 +31,16 @@ public static IEndpointConventionBuilder SuppressApi(this IEndpointConventionBui /// Adds metadata indicating the type of response an endpoint produces. /// /// The type of the response. - /// The . + /// The . /// The response status code. Defaults to StatusCodes.Status200OK. /// The response content type. Defaults to "application/json". /// Additional response content types the endpoint produces for the supplied status code. - /// A that can be used to further customize the endpoint. + /// A that can be used to further customize the endpoint. #pragma warning disable RS0026 - public static IEndpointConventionBuilder Produces(this IEndpointConventionBuilder builder, + public static MinimalActionEndpointConventionBuilder Produces(this MinimalActionEndpointConventionBuilder builder, #pragma warning restore RS0026 int statusCode = StatusCodes.Status200OK, - string? contentType = "application/json", + string? contentType = null, params string[] additionalContentTypes) { return Produces(builder, statusCode, typeof(TResponse), contentType, additionalContentTypes); @@ -48,14 +49,14 @@ public static IEndpointConventionBuilder Produces(this IEndpointConve /// /// Adds metadata indicating the type of response an endpoint produces. /// - /// The . + /// The . /// The response status code. Defaults to StatusCodes.Status200OK. /// 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. - /// A that can be used to further customize the endpoint. + /// A that can be used to further customize the endpoint. #pragma warning disable RS0026 - public static IEndpointConventionBuilder Produces(this IEndpointConventionBuilder builder, + public static MinimalActionEndpointConventionBuilder Produces(this MinimalActionEndpointConventionBuilder builder, #pragma warning restore RS0026 int statusCode = StatusCodes.Status200OK, Type? responseType = null, @@ -67,6 +68,12 @@ public static IEndpointConventionBuilder Produces(this IEndpointConventionBuilde contentType = "application/json"; } + if (contentType is null) + { + builder.WithMetadata(new ProducesResponseTypeAttribute(responseType ?? typeof(void), statusCode)); + return builder; + } + builder.WithMetadata(new ProducesResponseTypeAttribute(responseType ?? typeof(void), statusCode, contentType, additionalContentTypes)); return builder; @@ -75,11 +82,11 @@ public static IEndpointConventionBuilder Produces(this IEndpointConventionBuilde /// /// Adds metadata indicating that the endpoint produces a Problem Details response. /// - /// The . + /// The . /// The response status code. Defaults to StatusCodes.Status500InternalServerError. /// The response content type. Defaults to "application/problem+json". - /// A that can be used to further customize the endpoint. - public static IEndpointConventionBuilder ProducesProblem(this IEndpointConventionBuilder builder, + /// A that can be used to further customize the endpoint. + public static MinimalActionEndpointConventionBuilder ProducesProblem(this MinimalActionEndpointConventionBuilder builder, int statusCode = StatusCodes.Status500InternalServerError, string contentType = "application/problem+json") { @@ -89,11 +96,11 @@ public static IEndpointConventionBuilder ProducesProblem(this IEndpointConventio /// /// Adds metadata indicating that the endpoint produces a ProblemDetails response for validation errors. /// - /// The . + /// The . /// The response status code. Defaults to StatusCodes.Status400BadRequest. /// The response content type. Defaults to "application/problem+json". - /// A that can be used to further customize the endpoint. - public static IEndpointConventionBuilder ProducesValidationProblem(this IEndpointConventionBuilder builder, + /// A that can be used to further customize the endpoint. + public static MinimalActionEndpointConventionBuilder ProducesValidationProblem(this MinimalActionEndpointConventionBuilder builder, int statusCode = StatusCodes.Status400BadRequest, string contentType = "application/problem+json") { diff --git a/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs b/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs index 85ddbb2bf291..01e333ee5d01 100644 --- a/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs +++ b/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs @@ -14,6 +14,8 @@ namespace Microsoft.AspNetCore.Mvc [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class ProducesResponseTypeAttribute : Attribute, IApiResponseMetadataProvider { + private readonly MediaTypeCollection _contentTypes = new(); + /// /// Initializes an instance of . /// @@ -34,7 +36,6 @@ public ProducesResponseTypeAttribute(Type type, int statusCode) Type = type ?? throw new ArgumentNullException(nameof(type)); StatusCode = statusCode; IsResponseTypeSetByDefault = false; - ContentTypes = new(); } /// @@ -44,27 +45,24 @@ public ProducesResponseTypeAttribute(Type type, int statusCode) /// The HTTP response status code. /// The content type associated with the response. /// Additional content types supported by the response. - public ProducesResponseTypeAttribute(Type type, int statusCode, string? contentType, params string[] additionalContentTypes) + public ProducesResponseTypeAttribute(Type type, int statusCode, string contentType, params string[] additionalContentTypes) { + if (contentType == null) + { + throw new ArgumentNullException(nameof(contentType)); + } + Type = type ?? throw new ArgumentNullException(nameof(type)); StatusCode = statusCode; IsResponseTypeSetByDefault = false; - if (!string.IsNullOrEmpty(contentType)) - { - MediaTypeHeaderValue.Parse(contentType); - for (var i = 0; i < additionalContentTypes.Length; i++) - { - MediaTypeHeaderValue.Parse(additionalContentTypes[i]); - } - - ContentTypes = GetContentTypes(contentType, additionalContentTypes); - } - else + MediaTypeHeaderValue.Parse(contentType); + for (var i = 0; i < additionalContentTypes.Length; i++) { - ContentTypes = new(); + MediaTypeHeaderValue.Parse(additionalContentTypes[i]); } + _contentTypes = GetContentTypes(contentType, additionalContentTypes); } /// @@ -77,11 +75,6 @@ public ProducesResponseTypeAttribute(Type type, int statusCode, string? contentT /// public int StatusCode { get; set; } - /// - /// Gets or sets the content types supported by the response. - /// - public MediaTypeCollection ContentTypes { get; set; } - /// /// Used to distinguish a `Type` set by default in the constructor versus /// one provided by the user. @@ -94,11 +87,14 @@ public ProducesResponseTypeAttribute(Type type, int statusCode, string? contentT /// internal bool IsResponseTypeSetByDefault { get; } + // Internal for testing + internal MediaTypeCollection ContentTypes => _contentTypes; + /// - public void SetContentTypes(MediaTypeCollection contentTypes) + void IApiResponseMetadataProvider.SetContentTypes(MediaTypeCollection contentTypes) { contentTypes.Clear(); - foreach (var contentType in ContentTypes) + foreach (var contentType in _contentTypes) { contentTypes.Add(contentType); } diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index ecd2af933a80..a05275ae47ac 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -3219,13 +3219,10 @@ virtual Microsoft.AspNetCore.Mvc.ProducesAttribute.OnResultExecuted(Microsoft.As virtual Microsoft.AspNetCore.Mvc.ProducesAttribute.OnResultExecuting(Microsoft.AspNetCore.Mvc.Filters.ResultExecutingContext! context) -> void virtual Microsoft.AspNetCore.Mvc.RequireHttpsAttribute.HandleNonHttpsRequest(Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext! filterContext) -> void virtual Microsoft.AspNetCore.Mvc.RequireHttpsAttribute.OnAuthorization(Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext! filterContext) -> void -Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute.ProducesResponseTypeAttribute(System.Type! type, int statusCode, string? contentType, params string![]! additionalContentTypes) -> void -Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute.ContentTypes.get -> Microsoft.AspNetCore.Mvc.Formatters.MediaTypeCollection! -Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute.ContentTypes.set -> void -Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute.SetContentTypes(Microsoft.AspNetCore.Mvc.Formatters.MediaTypeCollection! contentTypes) -> void +Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute.ProducesResponseTypeAttribute(System.Type! type, int statusCode, string! contentType, params string![]! additionalContentTypes) -> void Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.SuppressApi(this Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! builder) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Produces(this Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! builder, int statusCode = 200, string? contentType = "application/json", params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Produces(this Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! builder, int statusCode = 200, System.Type? responseType = null, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ProducesProblem(this Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! builder, int statusCode = 500, string! contentType = "application/problem+json") -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ProducesValidationProblem(this Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! builder, int statusCode = 400, string! contentType = "application/problem+json") -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ExcludeFromApiExplorer(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Produces(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, int statusCode = 200, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Produces(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, int statusCode = 200, System.Type? responseType = null, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ProducesProblem(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, int statusCode = 500, string! contentType = "application/problem+json") -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ProducesValidationProblem(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, int statusCode = 400, string! contentType = "application/problem+json") -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! From 5c21deb7cc4d3837e9235d1ba37cf6a3dfd37af6 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 2 Aug 2021 23:04:32 +0000 Subject: [PATCH 13/20] Fix typo and update type signature --- ...Attribute.cs => ExcludeFromApiExplorerAttribute.cs} | 8 ++++---- ...rMetadata.cs => IExcludeFromApiExplorerMetadata.cs} | 8 ++++---- src/Http/Routing/src/PublicAPI.Unshipped.txt | 10 +++++----- .../src/EndpointMetadataApiDescriptionProvider.cs | 4 ++-- .../OpenApiEndpointConventionBuilderExtensions.cs | 2 +- src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) rename src/Http/Routing/src/{ExclueFromApiExplorerAttribute.cs => ExcludeFromApiExplorerAttribute.cs} (67%) rename src/Http/Routing/src/{IExclueFromApiExplorerMetadata.cs => IExcludeFromApiExplorerMetadata.cs} (66%) diff --git a/src/Http/Routing/src/ExclueFromApiExplorerAttribute.cs b/src/Http/Routing/src/ExcludeFromApiExplorerAttribute.cs similarity index 67% rename from src/Http/Routing/src/ExclueFromApiExplorerAttribute.cs rename to src/Http/Routing/src/ExcludeFromApiExplorerAttribute.cs index 601158c1bef9..c38f968e675e 100644 --- a/src/Http/Routing/src/ExclueFromApiExplorerAttribute.cs +++ b/src/Http/Routing/src/ExcludeFromApiExplorerAttribute.cs @@ -10,13 +10,13 @@ namespace Microsoft.AspNetCore.Routing /// Indicates that this should not be included in the generated API metadata. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate, AllowMultiple = false, Inherited = true)] - public sealed class ExclueFromApiExplorerAttribute : Attribute, IExclueFromApiExplorerMetadata + public sealed class ExcludeFromApiExplorerAttribute : Attribute, IExcludeFromApiExplorerMetadata { /// - /// Gets a value indicating whether API explorer - /// data should be excluded for this endpoint. If , + /// Gets a value indicating whether API explorer + /// data should be excluded for this endpoint. If , /// API metadata is not emitted. /// - public bool ExclueFromApiExplorer => true; + public bool ExcludeFromApiExplorer => true; } } diff --git a/src/Http/Routing/src/IExclueFromApiExplorerMetadata.cs b/src/Http/Routing/src/IExcludeFromApiExplorerMetadata.cs similarity index 66% rename from src/Http/Routing/src/IExclueFromApiExplorerMetadata.cs rename to src/Http/Routing/src/IExcludeFromApiExplorerMetadata.cs index 1b48448852e7..bd5b857e1fce 100644 --- a/src/Http/Routing/src/IExclueFromApiExplorerMetadata.cs +++ b/src/Http/Routing/src/IExcludeFromApiExplorerMetadata.cs @@ -9,12 +9,12 @@ namespace Microsoft.AspNetCore.Routing /// /// Indicates wheter or not that API explorer data should be emitted for this endpoint. /// - public interface IExclueFromApiExplorerMetadata + public interface IExcludeFromApiExplorerMetadata { /// - /// Gets a value indicating whether API explorer - /// data should be emitted for this endpoint. + /// Gets a value indicating whether API explorer + /// data should be emitted for this endpoint. /// - bool ExclueFromApiExplorer { get; } + bool ExcludeFromApiExplorer { get; } } } diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt index 57edf56f4e54..ed74f27f477a 100644 --- a/src/Http/Routing/src/PublicAPI.Unshipped.txt +++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt @@ -33,8 +33,8 @@ Microsoft.AspNetCore.Routing.EndpointNameAttribute.EndpointNameAttribute(string! Microsoft.AspNetCore.Routing.EndpointNameAttribute.EndpointName.get -> string! static Microsoft.AspNetCore.Builder.RoutingEndpointConventionBuilderExtensions.WithName(this TBuilder builder, string! endpointName) -> TBuilder static Microsoft.AspNetCore.Builder.RoutingEndpointConventionBuilderExtensions.WithGroupName(this TBuilder builder, string! endpointGroupName) -> TBuilder -Microsoft.AspNetCore.Routing.IExclueFromApiExplorerMetadata -Microsoft.AspNetCore.Routing.IExclueFromApiExplorerMetadata.ExclueFromApiExplorer.get -> bool -Microsoft.AspNetCore.Routing.ExclueFromApiExplorerAttribute -Microsoft.AspNetCore.Routing.ExclueFromApiExplorerAttribute.ExclueFromApiExplorerAttribute() -> void -Microsoft.AspNetCore.Routing.ExclueFromApiExplorerAttribute.ExclueFromApiExplorer.get -> bool +Microsoft.AspNetCore.Routing.IExcludeFromApiExplorerMetadata +Microsoft.AspNetCore.Routing.IExcludeFromApiExplorerMetadata.ExcludeFromApiExplorer.get -> bool +Microsoft.AspNetCore.Routing.ExcludeFromApiExplorerAttribute +Microsoft.AspNetCore.Routing.ExcludeFromApiExplorerAttribute.ExcludeFromApiExplorerAttribute() -> void +Microsoft.AspNetCore.Routing.ExcludeFromApiExplorerAttribute.ExcludeFromApiExplorer.get -> bool diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs index 4f5ae91c7dd9..eb05da757aae 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs @@ -53,8 +53,8 @@ public void OnProvidersExecuting(ApiDescriptionProviderContext context) if (endpoint is RouteEndpoint routeEndpoint && routeEndpoint.Metadata.GetMetadata() is { } methodInfo && routeEndpoint.Metadata.GetMetadata() is { } httpMethodMetadata && - (routeEndpoint.Metadata.GetMetadata() == null || - routeEndpoint.Metadata.GetMetadata() is { ExclueFromApiExplorer: false} )) + (routeEndpoint.Metadata.GetMetadata() == null || + routeEndpoint.Metadata.GetMetadata() is { ExcludeFromApiExplorer: false} )) { // REVIEW: Should we add an ApiDescription for endpoints without IHttpMethodMetadata? Swagger doesn't handle // a null HttpMethod even though it's nullable on ApiDescription, so we'd need to define "default" HTTP methods. diff --git a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs index 25f770c946d0..0b47d5c6db26 100644 --- a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs +++ b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Http /// public static class OpenApiEndpointConventionBuilderExtensions { - private static readonly ExclueFromApiExplorerAttribute _excludeFromApiMetadataAttribute = new(); + private static readonly ExcludeFromApiExplorerAttribute _excludeFromApiMetadataAttribute = new(); /// /// Adds metadata to support suppressing OpenAPI documentation from diff --git a/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs b/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs index 01e333ee5d01..7e6ddbe97c8f 100644 --- a/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs +++ b/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs @@ -51,7 +51,7 @@ public ProducesResponseTypeAttribute(Type type, int statusCode, string contentTy { throw new ArgumentNullException(nameof(contentType)); } - + Type = type ?? throw new ArgumentNullException(nameof(type)); StatusCode = statusCode; IsResponseTypeSetByDefault = false; @@ -100,7 +100,7 @@ void IApiResponseMetadataProvider.SetContentTypes(MediaTypeCollection contentTyp } } - private MediaTypeCollection GetContentTypes(string contentType, string[] additionalContentTypes) + private static MediaTypeCollection GetContentTypes(string contentType, string[] additionalContentTypes) { List completeContentTypes = new(additionalContentTypes.Length + 1); completeContentTypes.Add(contentType); From a914a2dd9f2ff1e8b8afa4c0bb191a4e19cb5e2e Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 3 Aug 2021 17:27:23 -0500 Subject: [PATCH 14/20] Apply feedback from second API review --- ...Attribute.cs => ExcludeFromDescriptionAttribute.cs} | 4 ++-- ...rMetadata.cs => IExcludeFromDescriptionMetadata.cs} | 4 ++-- src/Http/Routing/src/PublicAPI.Unshipped.txt | 10 +++++----- .../src/EndpointMetadataApiDescriptionProvider.cs | 4 ++-- .../test/EndpointMetadataApiDescriptionProviderTest.cs | 4 ++-- .../OpenApiEndpointConventionBuilderExtensions.cs | 8 ++++---- src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 6 +++--- 7 files changed, 20 insertions(+), 20 deletions(-) rename src/Http/Routing/src/{ExcludeFromApiExplorerAttribute.cs => ExcludeFromDescriptionAttribute.cs} (83%) rename src/Http/Routing/src/{IExcludeFromApiExplorerMetadata.cs => IExcludeFromDescriptionMetadata.cs} (84%) diff --git a/src/Http/Routing/src/ExcludeFromApiExplorerAttribute.cs b/src/Http/Routing/src/ExcludeFromDescriptionAttribute.cs similarity index 83% rename from src/Http/Routing/src/ExcludeFromApiExplorerAttribute.cs rename to src/Http/Routing/src/ExcludeFromDescriptionAttribute.cs index c38f968e675e..c60ed1695e1b 100644 --- a/src/Http/Routing/src/ExcludeFromApiExplorerAttribute.cs +++ b/src/Http/Routing/src/ExcludeFromDescriptionAttribute.cs @@ -10,13 +10,13 @@ namespace Microsoft.AspNetCore.Routing /// Indicates that this should not be included in the generated API metadata. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate, AllowMultiple = false, Inherited = true)] - public sealed class ExcludeFromApiExplorerAttribute : Attribute, IExcludeFromApiExplorerMetadata + public sealed class ExcludeFromDescriptionAttribute : Attribute, IExcludeFromDescriptionMetadata { /// /// Gets a value indicating whether API explorer /// data should be excluded for this endpoint. If , /// API metadata is not emitted. /// - public bool ExcludeFromApiExplorer => true; + public bool ExcludeFromDescription => true; } } diff --git a/src/Http/Routing/src/IExcludeFromApiExplorerMetadata.cs b/src/Http/Routing/src/IExcludeFromDescriptionMetadata.cs similarity index 84% rename from src/Http/Routing/src/IExcludeFromApiExplorerMetadata.cs rename to src/Http/Routing/src/IExcludeFromDescriptionMetadata.cs index bd5b857e1fce..76c358d94fe4 100644 --- a/src/Http/Routing/src/IExcludeFromApiExplorerMetadata.cs +++ b/src/Http/Routing/src/IExcludeFromDescriptionMetadata.cs @@ -9,12 +9,12 @@ namespace Microsoft.AspNetCore.Routing /// /// Indicates wheter or not that API explorer data should be emitted for this endpoint. /// - public interface IExcludeFromApiExplorerMetadata + public interface IExcludeFromDescriptionMetadata { /// /// Gets a value indicating whether API explorer /// data should be emitted for this endpoint. /// - bool ExcludeFromApiExplorer { get; } + bool ExcludeFromDescription { get; } } } diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt index ed74f27f477a..9d83b7228220 100644 --- a/src/Http/Routing/src/PublicAPI.Unshipped.txt +++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt @@ -33,8 +33,8 @@ Microsoft.AspNetCore.Routing.EndpointNameAttribute.EndpointNameAttribute(string! Microsoft.AspNetCore.Routing.EndpointNameAttribute.EndpointName.get -> string! static Microsoft.AspNetCore.Builder.RoutingEndpointConventionBuilderExtensions.WithName(this TBuilder builder, string! endpointName) -> TBuilder static Microsoft.AspNetCore.Builder.RoutingEndpointConventionBuilderExtensions.WithGroupName(this TBuilder builder, string! endpointGroupName) -> TBuilder -Microsoft.AspNetCore.Routing.IExcludeFromApiExplorerMetadata -Microsoft.AspNetCore.Routing.IExcludeFromApiExplorerMetadata.ExcludeFromApiExplorer.get -> bool -Microsoft.AspNetCore.Routing.ExcludeFromApiExplorerAttribute -Microsoft.AspNetCore.Routing.ExcludeFromApiExplorerAttribute.ExcludeFromApiExplorerAttribute() -> void -Microsoft.AspNetCore.Routing.ExcludeFromApiExplorerAttribute.ExcludeFromApiExplorer.get -> bool +Microsoft.AspNetCore.Routing.IExcludeFromDescriptionMetadata +Microsoft.AspNetCore.Routing.IExcludeFromDescriptionMetadata.ExcludeFromDescription.get -> bool +Microsoft.AspNetCore.Routing.ExcludeFromDescriptionAttribute +Microsoft.AspNetCore.Routing.ExcludeFromDescriptionAttribute.ExcludeFromDescriptionAttribute() -> void +Microsoft.AspNetCore.Routing.ExcludeFromDescriptionAttribute.ExcludeFromDescription.get -> bool diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs index eb05da757aae..29106b47311a 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs @@ -53,8 +53,8 @@ public void OnProvidersExecuting(ApiDescriptionProviderContext context) if (endpoint is RouteEndpoint routeEndpoint && routeEndpoint.Metadata.GetMetadata() is { } methodInfo && routeEndpoint.Metadata.GetMetadata() is { } httpMethodMetadata && - (routeEndpoint.Metadata.GetMetadata() == null || - routeEndpoint.Metadata.GetMetadata() is { ExcludeFromApiExplorer: false} )) + (routeEndpoint.Metadata.GetMetadata() == null || + routeEndpoint.Metadata.GetMetadata() is { ExcludeFromDescription: false} )) { // REVIEW: Should we add an ApiDescription for endpoints without IHttpMethodMetadata? Swagger doesn't handle // a null HttpMethod even though it's nullable on ApiDescription, so we'd need to define "default" HTTP methods. diff --git a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs index ce9d2e3bc298..477fcb551635 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs @@ -364,7 +364,7 @@ public void RespectsProducesProblemExtensionMethod() { // Arrange var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(null)); - builder.MapGet("/api/todos", () => "").ProducesProblem(); + builder.MapGet("/api/todos", () => "").ProducesProblem(StatusCodes.Status400BadRequest); var context = new ApiDescriptionProviderContext(Array.Empty()); var endpointDataSource = builder.DataSources.OfType().Single(); @@ -414,7 +414,7 @@ public void RespectsSuppressApiMethod() { // Arrange var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(null)); - builder.MapGet("/api/todos", () => "").Produces().ExcludeFromApiExplorer(); + builder.MapGet("/api/todos", () => "").Produces().ExcludeFromDescription(); var context = new ApiDescriptionProviderContext(Array.Empty()); var endpointDataSource = builder.DataSources.OfType().Single(); diff --git a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs index 0b47d5c6db26..086576cc0acc 100644 --- a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs +++ b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Http /// public static class OpenApiEndpointConventionBuilderExtensions { - private static readonly ExcludeFromApiExplorerAttribute _excludeFromApiMetadataAttribute = new(); + private static readonly ExcludeFromDescriptionAttribute _excludeFromApiMetadataAttribute = new(); /// /// Adds metadata to support suppressing OpenAPI documentation from @@ -20,7 +20,7 @@ public static class OpenApiEndpointConventionBuilderExtensions /// /// The . /// A that can be used to further customize the endpoint. - public static MinimalActionEndpointConventionBuilder ExcludeFromApiExplorer(this MinimalActionEndpointConventionBuilder builder) + public static MinimalActionEndpointConventionBuilder ExcludeFromDescription(this MinimalActionEndpointConventionBuilder builder) { builder.WithMetadata(_excludeFromApiMetadataAttribute); @@ -58,7 +58,7 @@ public static MinimalActionEndpointConventionBuilder Produces(this Mi #pragma warning disable RS0026 public static MinimalActionEndpointConventionBuilder Produces(this MinimalActionEndpointConventionBuilder builder, #pragma warning restore RS0026 - int statusCode = StatusCodes.Status200OK, + int statusCode, Type? responseType = null, string? contentType = null, params string[] additionalContentTypes) @@ -87,7 +87,7 @@ public static MinimalActionEndpointConventionBuilder Produces(this MinimalAction /// The response content type. Defaults to "application/problem+json". /// A that can be used to further customize the endpoint. public static MinimalActionEndpointConventionBuilder ProducesProblem(this MinimalActionEndpointConventionBuilder builder, - int statusCode = StatusCodes.Status500InternalServerError, + int statusCode, string contentType = "application/problem+json") { return Produces(builder, statusCode, contentType); diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index a05275ae47ac..6a3975b5e6b4 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -3221,8 +3221,8 @@ virtual Microsoft.AspNetCore.Mvc.RequireHttpsAttribute.HandleNonHttpsRequest(Mic virtual Microsoft.AspNetCore.Mvc.RequireHttpsAttribute.OnAuthorization(Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext! filterContext) -> void Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute.ProducesResponseTypeAttribute(System.Type! type, int statusCode, string! contentType, params string![]! additionalContentTypes) -> void Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ExcludeFromApiExplorer(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ExcludeFromDescription(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Produces(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, int statusCode = 200, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Produces(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, int statusCode = 200, System.Type? responseType = null, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ProducesProblem(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, int statusCode = 500, string! contentType = "application/problem+json") -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Produces(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, int statusCode, System.Type? responseType = null, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ProducesProblem(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, int statusCode, string! contentType = "application/problem+json") -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ProducesValidationProblem(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, int statusCode = 400, string! contentType = "application/problem+json") -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! From 8885db75974551d620f9b5be888ba15434757393 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 3 Aug 2021 17:41:17 -0500 Subject: [PATCH 15/20] Update docstrings --- .../Routing/src/EndpointGroupNameAttribute.cs | 6 ++---- src/Http/Routing/src/EndpointNameAttribute.cs | 4 +--- .../src/ExcludeFromDescriptionAttribute.cs | 6 +----- .../src/IExcludeFromDescriptionMetadata.cs | 5 +++-- ...pointMetadataApiDescriptionProviderTest.cs | 2 +- ...nApiEndpointConventionBuilderExtensions.cs | 20 +++++++++++-------- 6 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/Http/Routing/src/EndpointGroupNameAttribute.cs b/src/Http/Routing/src/EndpointGroupNameAttribute.cs index 0b2472fe3308..66dc6fe659a2 100644 --- a/src/Http/Routing/src/EndpointGroupNameAttribute.cs +++ b/src/Http/Routing/src/EndpointGroupNameAttribute.cs @@ -15,7 +15,7 @@ public sealed class EndpointGroupNameAttribute : Attribute, IEndpointGroupNameMe /// /// Initializes an instance of the EndpointGroupNameAttribute. /// - /// The endpoint name. + /// The endpoint group name. public EndpointGroupNameAttribute(string endpointGroupName) { if (endpointGroupName == null) @@ -26,9 +26,7 @@ public EndpointGroupNameAttribute(string endpointGroupName) EndpointGroupName = endpointGroupName; } - /// - /// The endpoint group name. - /// + /// public string EndpointGroupName { get; } } } \ No newline at end of file diff --git a/src/Http/Routing/src/EndpointNameAttribute.cs b/src/Http/Routing/src/EndpointNameAttribute.cs index af4b47f4aff7..9692dc8321b9 100644 --- a/src/Http/Routing/src/EndpointNameAttribute.cs +++ b/src/Http/Routing/src/EndpointNameAttribute.cs @@ -30,9 +30,7 @@ public EndpointNameAttribute(string endpointName) EndpointName = endpointName; } - /// - /// The endpoint name. - /// + /// public string EndpointName { get; } } } \ No newline at end of file diff --git a/src/Http/Routing/src/ExcludeFromDescriptionAttribute.cs b/src/Http/Routing/src/ExcludeFromDescriptionAttribute.cs index c60ed1695e1b..6aeb8426e5bf 100644 --- a/src/Http/Routing/src/ExcludeFromDescriptionAttribute.cs +++ b/src/Http/Routing/src/ExcludeFromDescriptionAttribute.cs @@ -12,11 +12,7 @@ namespace Microsoft.AspNetCore.Routing [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate, AllowMultiple = false, Inherited = true)] public sealed class ExcludeFromDescriptionAttribute : Attribute, IExcludeFromDescriptionMetadata { - /// - /// Gets a value indicating whether API explorer - /// data should be excluded for this endpoint. If , - /// API metadata is not emitted. - /// + /// public bool ExcludeFromDescription => true; } } diff --git a/src/Http/Routing/src/IExcludeFromDescriptionMetadata.cs b/src/Http/Routing/src/IExcludeFromDescriptionMetadata.cs index 76c358d94fe4..5fa70e3ac8ed 100644 --- a/src/Http/Routing/src/IExcludeFromDescriptionMetadata.cs +++ b/src/Http/Routing/src/IExcludeFromDescriptionMetadata.cs @@ -12,8 +12,9 @@ namespace Microsoft.AspNetCore.Routing public interface IExcludeFromDescriptionMetadata { /// - /// Gets a value indicating whether API explorer - /// data should be emitted for this endpoint. + /// Gets a value indicating whether OpenAPI + /// data should be excluded for this endpoint. If , + /// API metadata is not emitted. /// bool ExcludeFromDescription { get; } } diff --git a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs index 477fcb551635..f803687d1de1 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs @@ -410,7 +410,7 @@ public void RespectsProducesWithGroupNameExtensionMethod() } [Fact] - public void RespectsSuppressApiMethod() + public void RespectsExcludeFromDescription() { // Arrange var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(null)); diff --git a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs index 086576cc0acc..f75daa6ebe80 100644 --- a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs +++ b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs @@ -15,8 +15,8 @@ public static class OpenApiEndpointConventionBuilderExtensions private static readonly ExcludeFromDescriptionAttribute _excludeFromApiMetadataAttribute = new(); /// - /// Adds metadata to support suppressing OpenAPI documentation from - /// being generated for this endpoint. + /// Adds the to for all builders + /// produced by . /// /// The . /// A that can be used to further customize the endpoint. @@ -28,7 +28,8 @@ public static MinimalActionEndpointConventionBuilder ExcludeFromDescription(this } /// - /// Adds metadata indicating the type of response an endpoint produces. + /// Adds the to for all builders + /// produced by . /// /// The type of the response. /// The . @@ -47,10 +48,11 @@ public static MinimalActionEndpointConventionBuilder Produces(this Mi } /// - /// Adds metadata indicating the type of response an endpoint produces. + /// Adds the to for all builders + /// produced by . /// /// The . - /// The response status code. Defaults to StatusCodes.Status200OK. + /// 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. @@ -80,10 +82,11 @@ public static MinimalActionEndpointConventionBuilder Produces(this MinimalAction } /// - /// Adds metadata indicating that the endpoint produces a Problem Details response. + /// Adds the with a type + /// to for all builders produced by . /// /// The . - /// The response status code. Defaults to StatusCodes.Status500InternalServerError. + /// 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 MinimalActionEndpointConventionBuilder ProducesProblem(this MinimalActionEndpointConventionBuilder builder, @@ -94,7 +97,8 @@ public static MinimalActionEndpointConventionBuilder ProducesProblem(this Minima } /// - /// Adds metadata indicating that the endpoint produces a ProblemDetails response for validation errors. + /// Adds the with a type + /// to for all builders produced by . /// /// The . /// The response status code. Defaults to StatusCodes.Status400BadRequest. From 504b9f1beb132e6f277d8bb7d1e19a540c63baa2 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Wed, 4 Aug 2021 05:44:27 -0700 Subject: [PATCH 16/20] Apply suggestions from code review Co-authored-by: Martin Costello --- src/Http/Routing/src/IExcludeFromDescriptionMetadata.cs | 2 +- .../src/Builder/OpenApiEndpointConventionBuilderExtensions.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Http/Routing/src/IExcludeFromDescriptionMetadata.cs b/src/Http/Routing/src/IExcludeFromDescriptionMetadata.cs index 5fa70e3ac8ed..4e3c1eb997da 100644 --- a/src/Http/Routing/src/IExcludeFromDescriptionMetadata.cs +++ b/src/Http/Routing/src/IExcludeFromDescriptionMetadata.cs @@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.Routing { /// - /// Indicates wheter or not that API explorer data should be emitted for this endpoint. + /// Indicates whether or not that API explorer data should be emitted for this endpoint. /// public interface IExcludeFromDescriptionMetadata { diff --git a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs index f75daa6ebe80..be17bccc0ee3 100644 --- a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs +++ b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Http /// public static class OpenApiEndpointConventionBuilderExtensions { - private static readonly ExcludeFromDescriptionAttribute _excludeFromApiMetadataAttribute = new(); + private static readonly ExcludeFromDescriptionAttribute _excludeFromDescriptionMetadataAttribute = new(); /// /// Adds the to for all builders @@ -22,7 +22,7 @@ public static class OpenApiEndpointConventionBuilderExtensions /// A that can be used to further customize the endpoint. public static MinimalActionEndpointConventionBuilder ExcludeFromDescription(this MinimalActionEndpointConventionBuilder builder) { - builder.WithMetadata(_excludeFromApiMetadataAttribute); + builder.WithMetadata(_excludeFromDescriptionMetadataAttribute); return builder; } From a3e4171237be077c81eb2f878244557f5d9bc6ca Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Thu, 5 Aug 2021 03:05:54 +0000 Subject: [PATCH 17/20] Address non-test related feedback --- ...tingEndpointConventionBuilderExtensions.cs | 8 ++- .../Routing/src/EndpointGroupNameAttribute.cs | 6 +- .../EndpointMetadataApiDescriptionProvider.cs | 3 +- .../src/ProducesResponseTypeAttribute.cs | 5 +- src/Mvc/Mvc.Core/src/Resources.resx | 57 ++++++++++--------- 5 files changed, 43 insertions(+), 36 deletions(-) diff --git a/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs b/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs index b556a557c432..fe674265788d 100644 --- a/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs @@ -121,7 +121,9 @@ public static TBuilder WithMetadata(this TBuilder builder, params obje /// /// Sets the for all endpoints produced - /// on the target . + /// on the target given the . + /// The on the endpoint is used for link generation and + /// is treated as the operation ID in the given endpoint's OpenAPI specification. /// /// The . /// The endpoint name. @@ -134,7 +136,9 @@ public static TBuilder WithName(this TBuilder builder, string endpoint /// /// Sets the for all endpoints produced - /// on the target . + /// on the target given the . + /// The on the endpoint is used to set the endpoint's + /// GroupName in the OpenAPI specification. /// /// The . /// The endpoint group name. diff --git a/src/Http/Routing/src/EndpointGroupNameAttribute.cs b/src/Http/Routing/src/EndpointGroupNameAttribute.cs index 66dc6fe659a2..68511b6ca930 100644 --- a/src/Http/Routing/src/EndpointGroupNameAttribute.cs +++ b/src/Http/Routing/src/EndpointGroupNameAttribute.cs @@ -3,7 +3,7 @@ using System; using Microsoft.AspNetCore.Http; - + namespace Microsoft.AspNetCore.Routing { /// @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Routing public sealed class EndpointGroupNameAttribute : Attribute, IEndpointGroupNameMetadata { /// - /// Initializes an instance of the EndpointGroupNameAttribute. + /// Initializes an instance of the . /// /// The endpoint group name. public EndpointGroupNameAttribute(string endpointGroupName) @@ -22,7 +22,7 @@ public EndpointGroupNameAttribute(string endpointGroupName) { throw new ArgumentNullException(nameof(endpointGroupName)); } - + EndpointGroupName = endpointGroupName; } diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs index 29106b47311a..3ad253dcdb07 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs @@ -53,8 +53,7 @@ public void OnProvidersExecuting(ApiDescriptionProviderContext context) if (endpoint is RouteEndpoint routeEndpoint && routeEndpoint.Metadata.GetMetadata() is { } methodInfo && routeEndpoint.Metadata.GetMetadata() is { } httpMethodMetadata && - (routeEndpoint.Metadata.GetMetadata() == null || - routeEndpoint.Metadata.GetMetadata() is { ExcludeFromDescription: false} )) + routeEndpoint.Metadata.GetMetadata() is null or { ExcludeFromDescription: false}) { // REVIEW: Should we add an ApiDescription for endpoints without IHttpMethodMetadata? Swagger doesn't handle // a null HttpMethod even though it's nullable on ApiDescription, so we'd need to define "default" HTTP methods. diff --git a/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs b/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs index 7e6ddbe97c8f..ab4f3f2680ad 100644 --- a/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs +++ b/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; +using Resources = Microsoft.AspNetCore.Mvc.Core.Resources; namespace Microsoft.AspNetCore.Mvc { @@ -102,7 +103,7 @@ void IApiResponseMetadataProvider.SetContentTypes(MediaTypeCollection contentTyp private static MediaTypeCollection GetContentTypes(string contentType, string[] additionalContentTypes) { - List completeContentTypes = new(additionalContentTypes.Length + 1); + var completeContentTypes = new List(additionalContentTypes.Length + 1); completeContentTypes.Add(contentType); completeContentTypes.AddRange(additionalContentTypes); MediaTypeCollection contentTypes = new(); @@ -111,7 +112,7 @@ private static MediaTypeCollection GetContentTypes(string contentType, string[] var mediaType = new MediaType(type); if (mediaType.HasWildcard) { - throw new InvalidOperationException("Content types with wildcards are not supported."); + throw new InvalidOperationException(Resources.FormatGetContentTypes_WildcardsNotSupported(type)); } contentTypes.Add(type); diff --git a/src/Mvc/Mvc.Core/src/Resources.resx b/src/Mvc/Mvc.Core/src/Resources.resx index f27f845f7162..ab50909c99cf 100644 --- a/src/Mvc/Mvc.Core/src/Resources.resx +++ b/src/Mvc/Mvc.Core/src/Resources.resx @@ -1,17 +1,17 @@  - @@ -510,4 +510,7 @@ {0} cannot update a record type model. If a '{1}' must be updated, include it in an object type. + + Could not parse '{0}'. Content types with wildcards are not supported. + \ No newline at end of file From c8a1cd95202cb2c834ed51dddeb0a9c2e8d2736b Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Thu, 5 Aug 2021 17:50:12 +0000 Subject: [PATCH 18/20] Handle setting content types for ProducesResponseType attribute --- .../src/ApiResponseTypeProvider.cs | 89 +++++++++++---- .../EndpointMetadataApiDescriptionProvider.cs | 4 +- .../test/ApiResponseTypeProviderTest.cs | 52 +++++++++ .../test/DefaultApiDescriptionProviderTest.cs | 10 +- ...pointMetadataApiDescriptionProviderTest.cs | 105 +++++++++++++++++- .../ProducesResponseTypeAttributeTests.cs | 10 +- .../Mvc.FunctionalTests/ApiExplorerTest.cs | 16 +-- 7 files changed, 236 insertions(+), 50 deletions(-) diff --git a/src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs index b7cad53ad3e7..7e58ec99990c 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs @@ -79,8 +79,14 @@ private ICollection GetApiResponseTypes( Type defaultErrorType) { var contentTypes = new MediaTypeCollection(); + var responseTypeMetadataProviders = _mvcOptions.OutputFormatters.OfType(); - var responseTypes = ReadResponseMetadata(responseMetadataAttributes, type, defaultErrorType, contentTypes); + var responseTypes = ReadResponseMetadata( + responseMetadataAttributes, + type, + defaultErrorType, + contentTypes, + responseTypeMetadataProviders); // Set the default status only when no status has already been set explicitly if (responseTypes.Count == 0 && type != null) @@ -102,7 +108,10 @@ private ICollection GetApiResponseTypes( contentTypes.Add((string)null!); } - CalculateResponseFormats(responseTypes, contentTypes); + foreach(var apiResponse in responseTypes) + { + CalculateResponseFormatForType(apiResponse, contentTypes, responseTypeMetadataProviders, _modelMetadataProvider); + } return responseTypes; } @@ -112,7 +121,9 @@ internal static List ReadResponseMetadata( IReadOnlyList responseMetadataAttributes, Type? type, Type defaultErrorType, - MediaTypeCollection contentTypes) + MediaTypeCollection contentTypes, + IEnumerable? responseTypeMetadataProviders = null, + IModelMetadataProvider? _modelMetadataProvider = null) { var results = new Dictionary(); @@ -123,7 +134,18 @@ internal static List ReadResponseMetadata( { foreach (var metadataAttribute in responseMetadataAttributes) { - metadataAttribute.SetContentTypes(contentTypes); + // All ProducesXAttributes, except for ProducesResponseTypeAttribute do + // not allow multiple instances on the same method/class/etc. For those + // scenarios, the `SetContentTypes` method on the attribute continuously + // clears out more general content types in favor of more specific ones + // since we iterate through the attributes in order. For example, if a + // Produces exists on both a controller and an action within the controller, + // we favor the definition in the action. This is a semantic that does not + // apply to ProducesResponseType, which allows multiple instances on an target. + if (metadataAttribute is not ProducesResponseTypeAttribute) + { + metadataAttribute.SetContentTypes(contentTypes); + } var statusCode = metadataAttribute.StatusCode; @@ -157,6 +179,18 @@ internal static List ReadResponseMetadata( } } + // We special case the handling of ProcuesResponseTypeAttributes since + // multiple ProducesResponseTypeAttributes are permitted on a single + // action/controller/etc. In that scenario, instead of picking the most-specific + // set of content types (like we do with the Produces attribute above) we process + // the content types for each attribute independently. + if (metadataAttribute is ProducesResponseTypeAttribute) + { + var attributeContentTypes = new MediaTypeCollection(); + metadataAttribute.SetContentTypes(attributeContentTypes); + CalculateResponseFormatForType(apiResponseType, attributeContentTypes, responseTypeMetadataProviders, _modelMetadataProvider); + } + if (apiResponseType.Type != null) { results[apiResponseType.StatusCode] = apiResponseType; @@ -167,9 +201,15 @@ internal static List ReadResponseMetadata( return results.Values.ToList(); } - private void CalculateResponseFormats(ICollection responseTypes, MediaTypeCollection declaredContentTypes) + private static void CalculateResponseFormatForType(ApiResponseType apiResponse, MediaTypeCollection declaredContentTypes, IEnumerable? responseTypeMetadataProviders, IModelMetadataProvider? _modelMetadataProvider) { - var responseTypeMetadataProviders = _mvcOptions.OutputFormatters.OfType(); + // If response formats have already been calculate for this type, + // then exit early. This avoids populating the ApiResponseFormat for + // types that have already been handled, specifically ProducesResponseTypes. + if (apiResponse.ApiResponseFormats.Count > 0) + { + return; + } // Given the content-types that were declared for this action, determine the formatters that support the content-type for the given // response type. @@ -179,21 +219,20 @@ private void CalculateResponseFormats(ICollection responseTypes // 3. When no formatter supports the specified content-type, use the user specified value as is. This is useful in actions where the user // dictates the content-type. // e.g. [Produces("application/pdf")] Action() => FileStream("somefile.pdf", "application/pdf"); - - foreach (var apiResponse in responseTypes) + var responseType = apiResponse.Type; + if (responseType == null || responseType == typeof(void)) { - var responseType = apiResponse.Type; - if (responseType == null || responseType == typeof(void)) - { - continue; - } + return; + } - apiResponse.ModelMetadata = _modelMetadataProvider.GetMetadataForType(responseType); + apiResponse.ModelMetadata = _modelMetadataProvider?.GetMetadataForType(responseType); - foreach (var contentType in declaredContentTypes) - { - var isSupportedContentType = false; + foreach (var contentType in declaredContentTypes) + { + var isSupportedContentType = false; + if (responseTypeMetadataProviders != null) + { foreach (var responseTypeMetadataProvider in responseTypeMetadataProviders) { var formatterSupportedContentTypes = responseTypeMetadataProvider.GetSupportedContentTypes( @@ -216,15 +255,17 @@ private void CalculateResponseFormats(ICollection responseTypes }); } } + } + + - if (!isSupportedContentType && contentType != null) + if (!isSupportedContentType && contentType != null) + { + // No output formatter was found that supports this content type. Add the user specified content type as-is to the result. + apiResponse.ApiResponseFormats.Add(new ApiResponseFormat { - // No output formatter was found that supports this content type. Add the user specified content type as-is to the result. - apiResponse.ApiResponseFormats.Add(new ApiResponseFormat - { - MediaType = contentType, - }); - } + MediaType = contentType, + }); } } } diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs index 3ad253dcdb07..a376b6e28b9f 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs @@ -269,7 +269,9 @@ private static void AddSupportedResponseTypes( { AddResponseContentTypes(apiResponseType.ApiResponseFormats, contentTypes); } - else if (CreateDefaultApiResponseFormat(apiResponseType.Type) is { } defaultResponseFormat) + // Only set the default response type if it hasn't already been set via a + // ProducesResponseTypeAttribute. + else if (apiResponseType.ApiResponseFormats.Count == 0 && CreateDefaultApiResponseFormat(apiResponseType.Type) is { } defaultResponseFormat) { apiResponseType.ApiResponseFormats.Add(defaultResponseFormat); } diff --git a/src/Mvc/Mvc.ApiExplorer/test/ApiResponseTypeProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/ApiResponseTypeProviderTest.cs index 8a8f0e2091a4..d8091f39feb6 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/ApiResponseTypeProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/ApiResponseTypeProviderTest.cs @@ -709,6 +709,51 @@ public void GetApiResponseTypes_UsesContentTypeWithoutWildCard_WhenNoFormatterSu }); } + [Fact] + public void ApiAction_HandlesContentTypesAndStatusCodes() + { + // Arrange + var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.GetUser)); + actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesAttribute("text/xml") { Type = typeof(BaseModel) }, FilterScope.Action)); + actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesResponseTypeAttribute(typeof(ValidationProblemDetails), 400, "application/validationproblem+json"), FilterScope.Action)); + actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesResponseTypeAttribute(typeof(ProblemDetails), 404, "application/problem+json"), FilterScope.Action)); + actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesResponseTypeAttribute(409), FilterScope.Action)); + + var provider = new ApiResponseTypeProvider(new EmptyModelMetadataProvider(), new ActionResultTypeMapper(), new MvcOptions()); + + // Act + var result = provider.GetApiResponseTypes(actionDescriptor); + + // Assert + Assert.Collection( + result.OrderBy(r => r.StatusCode), + responseType => + { + Assert.Equal(typeof(BaseModel), responseType.Type); + Assert.Equal(200, responseType.StatusCode); + Assert.Equal(new[] { "text/xml" }, GetSortedMediaTypes(responseType)); + + }, + responseType => + { + Assert.Equal(typeof(ValidationProblemDetails), responseType.Type); + Assert.Equal(400, responseType.StatusCode); + Assert.Equal(new[] { "application/validationproblem+json" }, GetSortedMediaTypes(responseType)); + }, + responseType => + { + Assert.Equal(typeof(ProblemDetails), responseType.Type); + Assert.Equal(404, responseType.StatusCode); + Assert.Equal(new[] { "application/problem+json" }, GetSortedMediaTypes(responseType)); + }, + responseType => + { + Assert.Equal(typeof(void), responseType.Type); + Assert.Equal(409, responseType.StatusCode); + Assert.Empty(GetSortedMediaTypes(responseType)); + }); + } + private static ApiResponseTypeProvider GetProvider() { var mvcOptions = new MvcOptions @@ -719,6 +764,13 @@ private static ApiResponseTypeProvider GetProvider() return provider; } + private static IEnumerable GetSortedMediaTypes(ApiResponseType apiResponseType) + { + return apiResponseType.ApiResponseFormats + .OrderBy(format => format.MediaType) + .Select(format => format.MediaType); + } + private static ControllerActionDescriptor GetControllerActionDescriptor(Type type, string name) { var method = type.GetMethod(name); diff --git a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs index 5de26a83b22a..c3492411df05 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs @@ -567,7 +567,7 @@ public void GetApiDescription_ReturnsActionResultWithProduces_And_ProducesConten // Arrange var action = CreateActionDescriptor(methodName, controllerType); action.FilterDescriptors = filterDescriptors; - var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; + var expectedMediaTypes = new[] { "application/json", "text/json" }; // Act var descriptions = GetApiDescriptions(action); @@ -677,7 +677,7 @@ public void GetApiDescription_ReturnsVoidWithProducesContentType( // Arrange var action = CreateActionDescriptor(methodName, controllerType); action.FilterDescriptors = filterDescriptors; - var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; + var expectedMediaTypes = new[] { "application/json", "text/json" }; // Act var descriptions = GetApiDescriptions(action); @@ -740,7 +740,7 @@ public void GetApiDescription_ReturnsActionResultOfTWithProducesContentType( new ProducesResponseTypeAttribute(typeof(ErrorDetails), 500), FilterScope.Action) }; - var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; + var expectedMediaTypes = new[] { "application/json", "text/json" }; // Act var descriptions = GetApiDescriptions(action); @@ -810,7 +810,7 @@ public void GetApiDescription_ReturnsActionResultOfTWithProducesContentType_ForS new ProducesResponseTypeAttribute(typeof(ErrorDetails), 500), FilterScope.Action) }; - var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; + var expectedMediaTypes = new[] { "application/json", "text/json" }; // Act var descriptions = GetApiDescriptions(action); @@ -880,7 +880,7 @@ public void GetApiDescription_ReturnsActionResultOfSequenceOfTWithProducesConten new ProducesResponseTypeAttribute(typeof(ErrorDetails), 500), FilterScope.Action) }; - var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; + var expectedMediaTypes = new[] { "application/json", "text/json" }; // Act var descriptions = GetApiDescriptions(action); diff --git a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs index f803687d1de1..007dab7a6c93 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs @@ -391,7 +391,7 @@ public void RespectsProducesWithGroupNameExtensionMethod() var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(null)); builder.MapGet("/api/todos", () => "").Produces().WithGroupName(endpointGroupName); var context = new ApiDescriptionProviderContext(Array.Empty()); - + var endpointDataSource = builder.DataSources.OfType().Single(); var hostEnvironment = new HostEnvironment { @@ -406,7 +406,7 @@ public void RespectsProducesWithGroupNameExtensionMethod() var apiDescription = Assert.Single(context.Results); var responseTypes = Assert.Single(apiDescription.SupportedResponseTypes); Assert.Equal(typeof(InferredJsonClass), responseTypes.Type); - Assert.Equal(endpointGroupName, apiDescription.GroupName); + Assert.Equal(endpointGroupName, apiDescription.GroupName); } [Fact] @@ -416,7 +416,7 @@ public void RespectsExcludeFromDescription() var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(null)); builder.MapGet("/api/todos", () => "").Produces().ExcludeFromDescription(); var context = new ApiDescriptionProviderContext(Array.Empty()); - + var endpointDataSource = builder.DataSources.OfType().Single(); var hostEnvironment = new HostEnvironment { @@ -431,6 +431,105 @@ public void RespectsExcludeFromDescription() Assert.Empty(context.Results); } + [Fact] + public void HandlesProducesWithProducesProblem() + { + // Arrange + var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(null)); + builder.MapGet("/api/todos", () => "") + .Produces(StatusCodes.Status200OK) + .ProducesValidationProblem() + .ProducesProblem(StatusCodes.Status404NotFound) + .ProducesProblem(StatusCodes.Status409Conflict); + var context = new ApiDescriptionProviderContext(Array.Empty()); + + var endpointDataSource = builder.DataSources.OfType().Single(); + var hostEnvironment = new HostEnvironment + { + ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) + }; + var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); + + // Act + provider.OnProvidersExecuting(context); + provider.OnProvidersExecuted(context); + + // Assert + Assert.Collection( + context.Results.SelectMany(r => r.SupportedResponseTypes).OrderBy(r => r.StatusCode), + responseType => + { + Assert.Equal(typeof(InferredJsonClass), responseType.Type); + Assert.Equal(200, responseType.StatusCode); + Assert.Equal(new[] { "application/json" }, GetSortedMediaTypes(responseType)); + + }, + responseType => + { + Assert.Equal(typeof(HttpValidationProblemDetails), responseType.Type); + Assert.Equal(400, responseType.StatusCode); + Assert.Equal(new[] { "application/problem+json" }, GetSortedMediaTypes(responseType)); + }, + responseType => + { + Assert.Equal(typeof(ProblemDetails), responseType.Type); + Assert.Equal(404, responseType.StatusCode); + Assert.Equal(new[] { "application/problem+json" }, GetSortedMediaTypes(responseType)); + }, + responseType => + { + Assert.Equal(typeof(ProblemDetails), responseType.Type); + Assert.Equal(409, responseType.StatusCode); + Assert.Equal(new[] { "application/problem+json" }, GetSortedMediaTypes(responseType)); + }); + } + + [Fact] + public void HandleMultipleProduces() + { + // Arrange + var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(null)); + builder.MapGet("/api/todos", () => "") + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status201Created); + var context = new ApiDescriptionProviderContext(Array.Empty()); + + var endpointDataSource = builder.DataSources.OfType().Single(); + var hostEnvironment = new HostEnvironment + { + ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) + }; + var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService()); + + // Act + provider.OnProvidersExecuting(context); + provider.OnProvidersExecuted(context); + + // Assert + Assert.Collection( + context.Results.SelectMany(r => r.SupportedResponseTypes).OrderBy(r => r.StatusCode), + responseType => + { + Assert.Equal(typeof(InferredJsonClass), responseType.Type); + Assert.Equal(200, responseType.StatusCode); + Assert.Equal(new[] { "application/json" }, GetSortedMediaTypes(responseType)); + + }, + responseType => + { + Assert.Equal(typeof(InferredJsonClass), responseType.Type); + Assert.Equal(201, responseType.StatusCode); + Assert.Equal(new[] { "application/json" }, GetSortedMediaTypes(responseType)); + }); + } + + private static IEnumerable GetSortedMediaTypes(ApiResponseType apiResponseType) + { + return apiResponseType.ApiResponseFormats + .OrderBy(format => format.MediaType) + .Select(format => format.MediaType); + } + private IList GetApiDescriptions( Delegate action, string pattern = null, diff --git a/src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs b/src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs index bbca19807a3d..13ef9298bab6 100644 --- a/src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs +++ b/src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs @@ -1,16 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Formatters; -using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Primitives; -using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc @@ -51,7 +43,7 @@ public void ProducesResponseTypeAttribute_InvalidContentType_Throws(string conte () => new ProducesResponseTypeAttribute(typeof(void), StatusCodes.Status200OK, contentTypes[0], contentTypes.Skip(1).ToArray())); Assert.Equal( - $"Content types with wildcards are not supported.", + $"Could not parse '{invalidContentType}'. Content types with wildcards are not supported.", ex.Message); } diff --git a/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs b/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs index b0ba16f331ed..f9432bcd55b4 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs @@ -607,7 +607,7 @@ public async Task ExplicitResponseTypeDecoration_SuppressesDefaultStatus_AlsoHon // Arrange var type1 = typeof(ApiExplorerWebSite.Product).FullName; var type2 = typeof(SerializableError).FullName; - var expectedMediaTypes = new[] { "application/xml", "text/xml", "application/json", "text/json" }; + var expectedMediaTypes = new[] { "text/xml" }; // Act var response = await Client.GetAsync( @@ -671,7 +671,7 @@ public async Task ExplicitResponseTypeDecoration_WithExplicitDefaultStatus_Speci // Arrange var type1 = typeof(ApiExplorerWebSite.Product).FullName; var type2 = typeof(SerializableError).FullName; - var expectedMediaTypes = new[] { "application/xml", "text/xml", "application/json", "text/json" }; + var expectedMediaTypes = new[] { "text/xml" }; // Act var response = await Client.GetAsync( @@ -696,14 +696,12 @@ public async Task ExplicitResponseTypeDecoration_WithExplicitDefaultStatus_Speci expectedMediaTypes, responseType.ResponseFormats.Select(responseFormat => responseFormat.MediaType).ToArray()); } - [Fact] public async Task ApiExplorer_ResponseType_InheritingFromController() { // Arrange var type = "ApiExplorerWebSite.Product"; var errorType = "ApiExplorerWebSite.ErrorInfo"; - var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; // Act var response = await Client.GetAsync( @@ -721,13 +719,15 @@ public async Task ApiExplorer_ResponseType_InheritingFromController() { Assert.Equal(type, responseType.ResponseType); Assert.Equal(200, responseType.StatusCode); - Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType)); + var responseFormat = Assert.Single(responseType.ResponseFormats); + Assert.Equal("application/json", responseFormat.MediaType); }, responseType => { Assert.Equal(errorType, responseType.ResponseType); Assert.Equal(500, responseType.StatusCode); - Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType)); + var responseFormat = Assert.Single(responseType.ResponseFormats); + Assert.Equal("application/json", responseFormat.MediaType); }); } @@ -1252,7 +1252,7 @@ public async Task ApiConvention_ForGetMethodThatDoesNotMatchConvention() public async Task ApiConvention_ForMethodWithResponseTypeAttributes() { // Arrange - var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; + var expectedMediaTypes = new[] { "application/json" }; // Act var response = await Client.PostAsync( @@ -1318,7 +1318,7 @@ public async Task ApiConvention_ForPostMethodThatMatchesConvention() public async Task ApiConvention_ForPostActionWithProducesAttribute() { // Arrange - var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; + var expectedMediaTypes = new[] { "application/json", "text/json", }; // Act var response = await Client.PostAsync( From 63d63290d1ce557a8e52ccf2e8d4b97bf5c732a5 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 6 Aug 2021 17:16:25 +0000 Subject: [PATCH 19/20] Address feedback from peer review --- .../src/ApiResponseTypeProvider.cs | 10 +++++----- .../EndpointMetadataApiDescriptionProvider.cs | 2 +- .../test/ApiResponseTypeProviderTest.cs | 2 +- ...pointMetadataApiDescriptionProviderTest.cs | 2 +- ...nApiEndpointConventionBuilderExtensions.cs | 20 ++++++++++++++----- .../src/ProducesResponseTypeAttribute.cs | 13 +++++++----- src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 4 ++-- .../ProducesResponseTypeAttributeTests.cs | 3 +-- 8 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs index 7e58ec99990c..bac4c5b7fed9 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs @@ -108,7 +108,7 @@ private ICollection GetApiResponseTypes( contentTypes.Add((string)null!); } - foreach(var apiResponse in responseTypes) + foreach (var apiResponse in responseTypes) { CalculateResponseFormatForType(apiResponse, contentTypes, responseTypeMetadataProviders, _modelMetadataProvider); } @@ -123,7 +123,7 @@ internal static List ReadResponseMetadata( Type defaultErrorType, MediaTypeCollection contentTypes, IEnumerable? responseTypeMetadataProviders = null, - IModelMetadataProvider? _modelMetadataProvider = null) + IModelMetadataProvider? modelMetadataProvider = null) { var results = new Dictionary(); @@ -188,7 +188,7 @@ internal static List ReadResponseMetadata( { var attributeContentTypes = new MediaTypeCollection(); metadataAttribute.SetContentTypes(attributeContentTypes); - CalculateResponseFormatForType(apiResponseType, attributeContentTypes, responseTypeMetadataProviders, _modelMetadataProvider); + CalculateResponseFormatForType(apiResponseType, attributeContentTypes, responseTypeMetadataProviders, modelMetadataProvider); } if (apiResponseType.Type != null) @@ -201,7 +201,7 @@ internal static List ReadResponseMetadata( return results.Values.ToList(); } - private static void CalculateResponseFormatForType(ApiResponseType apiResponse, MediaTypeCollection declaredContentTypes, IEnumerable? responseTypeMetadataProviders, IModelMetadataProvider? _modelMetadataProvider) + private static void CalculateResponseFormatForType(ApiResponseType apiResponse, MediaTypeCollection declaredContentTypes, IEnumerable? responseTypeMetadataProviders, IModelMetadataProvider? modelMetadataProvider) { // If response formats have already been calculate for this type, // then exit early. This avoids populating the ApiResponseFormat for @@ -225,7 +225,7 @@ private static void CalculateResponseFormatForType(ApiResponseType apiResponse, return; } - apiResponse.ModelMetadata = _modelMetadataProvider?.GetMetadataForType(responseType); + apiResponse.ModelMetadata = modelMetadataProvider?.GetMetadataForType(responseType); foreach (var contentType in declaredContentTypes) { diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs index a376b6e28b9f..f61212044b0c 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs @@ -53,7 +53,7 @@ public void OnProvidersExecuting(ApiDescriptionProviderContext context) if (endpoint is RouteEndpoint routeEndpoint && routeEndpoint.Metadata.GetMetadata() is { } methodInfo && routeEndpoint.Metadata.GetMetadata() is { } httpMethodMetadata && - routeEndpoint.Metadata.GetMetadata() is null or { ExcludeFromDescription: false}) + routeEndpoint.Metadata.GetMetadata() is null or { ExcludeFromDescription: false }) { // REVIEW: Should we add an ApiDescription for endpoints without IHttpMethodMetadata? Swagger doesn't handle // a null HttpMethod even though it's nullable on ApiDescription, so we'd need to define "default" HTTP methods. diff --git a/src/Mvc/Mvc.ApiExplorer/test/ApiResponseTypeProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/ApiResponseTypeProviderTest.cs index d8091f39feb6..fe0225df8e97 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/ApiResponseTypeProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/ApiResponseTypeProviderTest.cs @@ -710,7 +710,7 @@ public void GetApiResponseTypes_UsesContentTypeWithoutWildCard_WhenNoFormatterSu } [Fact] - public void ApiAction_HandlesContentTypesAndStatusCodes() + public void GetApiResponseTypes_HandlesActionWithMultipleContentTypesAndProduces() { // Arrange var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.GetUser)); diff --git a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs index 007dab7a6c93..e390d4c85ef4 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs @@ -468,7 +468,7 @@ public void HandlesProducesWithProducesProblem() { Assert.Equal(typeof(HttpValidationProblemDetails), responseType.Type); Assert.Equal(400, responseType.StatusCode); - Assert.Equal(new[] { "application/problem+json" }, GetSortedMediaTypes(responseType)); + Assert.Equal(new[] { "application/validationproblem+json" }, GetSortedMediaTypes(responseType)); }, responseType => { diff --git a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs index be17bccc0ee3..78c9b6a826db 100644 --- a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs +++ b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs @@ -83,7 +83,7 @@ public static MinimalActionEndpointConventionBuilder Produces(this MinimalAction /// /// Adds the with a type - /// to for all builders produced by . + /// to for all builders produced by . /// /// The . /// The response status code. @@ -91,23 +91,33 @@ public static MinimalActionEndpointConventionBuilder Produces(this MinimalAction /// A that can be used to further customize the endpoint. public static MinimalActionEndpointConventionBuilder ProducesProblem(this MinimalActionEndpointConventionBuilder builder, int statusCode, - string contentType = "application/problem+json") + string? contentType = null) { + if (string.IsNullOrEmpty(contentType)) + { + contentType = "application/problem+json"; + } + return Produces(builder, statusCode, contentType); } /// /// Adds the with a type - /// to for all builders produced by . + /// to for all builders produced by . /// /// The . /// The response status code. Defaults to StatusCodes.Status400BadRequest. - /// The response content type. Defaults to "application/problem+json". + /// The response content type. Defaults to "application/validationproblem+json". /// A that can be used to further customize the endpoint. public static MinimalActionEndpointConventionBuilder ProducesValidationProblem(this MinimalActionEndpointConventionBuilder builder, int statusCode = StatusCodes.Status400BadRequest, - string contentType = "application/problem+json") + string? contentType = null) { + if (string.IsNullOrEmpty(contentType)) + { + contentType = "application/validationproblem+json"; + } + return Produces(builder, statusCode, contentType); } } diff --git a/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs b/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs index ab4f3f2680ad..ed19ccd5ebe0 100644 --- a/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs +++ b/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Mvc [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class ProducesResponseTypeAttribute : Attribute, IApiResponseMetadataProvider { - private readonly MediaTypeCollection _contentTypes = new(); + private readonly MediaTypeCollection? _contentTypes; /// /// Initializes an instance of . @@ -89,15 +89,18 @@ public ProducesResponseTypeAttribute(Type type, int statusCode, string contentTy internal bool IsResponseTypeSetByDefault { get; } // Internal for testing - internal MediaTypeCollection ContentTypes => _contentTypes; + internal MediaTypeCollection? ContentTypes => _contentTypes; /// void IApiResponseMetadataProvider.SetContentTypes(MediaTypeCollection contentTypes) { - contentTypes.Clear(); - foreach (var contentType in _contentTypes) + if (_contentTypes is not null) { - contentTypes.Add(contentType); + contentTypes.Clear(); + foreach (var contentType in _contentTypes) + { + contentTypes.Add(contentType); + } } } diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index 6a3975b5e6b4..02d3d3791fdd 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -3224,5 +3224,5 @@ Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ExcludeFromDescription(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Produces(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, int statusCode = 200, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Produces(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, int statusCode, System.Type? responseType = null, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ProducesProblem(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, int statusCode, string! contentType = "application/problem+json") -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ProducesValidationProblem(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, int statusCode = 400, string! contentType = "application/problem+json") -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ProducesProblem(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, int statusCode, string? contentType = null) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ProducesValidationProblem(this Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! builder, int statusCode = 400, string? contentType = null) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder! diff --git a/src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs b/src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs index 13ef9298bab6..bb2baa722733 100644 --- a/src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs +++ b/src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs @@ -65,8 +65,7 @@ public void ProducesResponseTypeAttribute_WithTypeOnly_DoesNotSetContentTypes() var producesResponseTypeAttribute = new ProducesResponseTypeAttribute(typeof(Person), StatusCodes.Status200OK); // Act and Assert - Assert.NotNull(producesResponseTypeAttribute.ContentTypes); - Assert.Empty(producesResponseTypeAttribute.ContentTypes); + Assert.Null(producesResponseTypeAttribute.ContentTypes); } private class Person From 94260c4883f63717d22f769f01a3d1f4881e043d Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 6 Aug 2021 17:48:13 +0000 Subject: [PATCH 20/20] Add test for ProducesResponseType override scenario --- .../Mvc.FunctionalTests/ApiExplorerTest.cs | 35 +++++++++++++++++++ ...rResponseTypeOverrideOnActionController.cs | 7 ++++ 2 files changed, 42 insertions(+) diff --git a/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs b/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs index f9432bcd55b4..d6305c36acfe 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs @@ -766,6 +766,41 @@ public async Task ApiExplorer_ResponseType_OverrideOnAction() }); } + [Fact] + public async Task ApiExplorer_ResponseTypeWithContentType_OverrideOnAction() + { + // This test scenario validates that a ProducesResponseType attribute will overide + // content-type given by a Produces attribute with a lower-specificity. + // Arrange + var type = "ApiExplorerWebSite.Customer"; + var errorType = "ApiExplorerWebSite.ErrorInfo"; + + // Act + var response = await Client.GetAsync( + "http://localhost/ApiExplorerResponseTypeOverrideOnAction/Action2"); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(body); + + // Assert + var description = Assert.Single(result); + + Assert.Collection( + description.SupportedResponseTypes.OrderBy(responseType => responseType.StatusCode), + responseType => + { + Assert.Equal(type, responseType.ResponseType); + Assert.Equal(200, responseType.StatusCode); + Assert.Equal(new[] { "text/plain" }, GetSortedMediaTypes(responseType)); + }, + responseType => + { + Assert.Equal(errorType, responseType.ResponseType); + Assert.Equal(500, responseType.StatusCode); + Assert.Equal(new[] { "application/json" }, GetSortedMediaTypes(responseType)); + }); + } + [ConditionalFact] // Mono issue - https://github.com/aspnet/External/issues/18 [FrameworkSkipCondition(RuntimeFrameworks.Mono)] diff --git a/src/Mvc/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerResponseTypeOverrideOnActionController.cs b/src/Mvc/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerResponseTypeOverrideOnActionController.cs index c9a09accea2f..7afcdd4d9487 100644 --- a/src/Mvc/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerResponseTypeOverrideOnActionController.cs +++ b/src/Mvc/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerResponseTypeOverrideOnActionController.cs @@ -22,6 +22,13 @@ public object GetAction() { return null; } + + [HttpGet("Action2")] + [ProducesResponseType(typeof(Customer), 200, "text/plain")] + public object GetActionWithContentTypeOverride() + { + return null; + } } public class ErrorInfo