From 7f635c3abd6529135e279e72665119dfc6b268b1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 9 Sep 2021 09:10:39 -0700 Subject: [PATCH] [release/6.0] Move OpenAPI extensions to routing assembly (#36310) * Move OpenAPI extensions to routing assembly * Address feedback from code and API review * Clean up ReadResponseMetadata implementation * Update IProducesResponseTypeMetadata to expose ContentTypes * Address more feedback from review * Update ContentTypes to IEnumerable * Make IEnumerable non-nullable * One more feedback * Address feedback from review Co-authored-by: Safia Abdalla <safia@microsoft.com> --- .../Metadata/IProducesResponseTypeMetadata.cs | 26 +++ .../src/PublicAPI.Unshipped.txt | 6 +- ...gateEndpointConventionBuilderExtensions.cs | 195 ++++++++++++++++- .../src/Microsoft.AspNetCore.Routing.csproj | 2 + src/Http/Routing/src/PublicAPI.Unshipped.txt | 11 +- .../Microsoft.AspNetCore.Routing.Tests.csproj | 4 - .../src/ApiResponseTypeProvider.cs | 3 +- .../EndpointMetadataApiDescriptionProvider.cs | 67 +++++- ...pointMetadataApiDescriptionProviderTest.cs | 32 +++ ...nApiEndpointConventionBuilderExtensions.cs | 207 ------------------ src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 16 +- .../ProducesResponseTypeMetadata.cs | 114 ++++++++++ 12 files changed, 450 insertions(+), 233 deletions(-) create mode 100644 src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs delete mode 100644 src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs create mode 100644 src/Shared/ApiExplorerTypes/ProducesResponseTypeMetadata.cs diff --git a/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs new file mode 100644 index 00000000000..7bbd8546d4a --- /dev/null +++ b/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Http.Metadata +{ + /// <summary> + /// Defines a contract for outline the response type returned from an endpoint. + /// </summary> + public interface IProducesResponseTypeMetadata + { + /// <summary> + /// Gets the optimistic return type of the action. + /// </summary> + Type? Type { get; } + + /// <summary> + /// Gets the HTTP status code of the response. + /// </summary> + int StatusCode { get; } + + /// <summary> + /// Gets the content types supported by the metadata. + /// </summary> + IEnumerable<string> ContentTypes { get; } + } +} \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index ee9f85e0ae8..90ae78a4458 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -37,4 +37,8 @@ static Microsoft.AspNetCore.Builder.UseExtensions.Use(this Microsoft.AspNetCore. static Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.UseMiddleware(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, System.Type! middleware, params object?[]! args) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! static Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.UseMiddleware<TMiddleware>(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, params object?[]! args) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! virtual Microsoft.AspNetCore.Http.ConnectionInfo.RequestClose() -> void -virtual Microsoft.AspNetCore.Http.WebSocketManager.AcceptWebSocketAsync(Microsoft.AspNetCore.Http.WebSocketAcceptContext! acceptContext) -> System.Threading.Tasks.Task<System.Net.WebSockets.WebSocket!>! \ No newline at end of file +virtual Microsoft.AspNetCore.Http.WebSocketManager.AcceptWebSocketAsync(Microsoft.AspNetCore.Http.WebSocketAcceptContext! acceptContext) -> System.Threading.Tasks.Task<System.Net.WebSockets.WebSocket!>! +Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata +Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata.StatusCode.get -> int +Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata.Type.get -> System.Type? +Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata.ContentTypes.get -> System.Collections.Generic.IEnumerable<string!>! \ No newline at end of file diff --git a/src/Http/Routing/src/Builder/OpenApiDelegateEndpointConventionBuilderExtensions.cs b/src/Http/Routing/src/Builder/OpenApiDelegateEndpointConventionBuilderExtensions.cs index ff97915d6eb..f6e65e08845 100644 --- a/src/Http/Routing/src/Builder/OpenApiDelegateEndpointConventionBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/OpenApiDelegateEndpointConventionBuilderExtensions.cs @@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Http { @@ -12,6 +14,115 @@ namespace Microsoft.AspNetCore.Http /// </summary> public static class OpenApiDelegateEndpointConventionBuilderExtensions { + private static readonly ExcludeFromDescriptionAttribute _excludeFromDescriptionMetadataAttribute = new(); + + /// <summary> + /// Adds the <see cref="IExcludeFromDescriptionMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all builders + /// produced by <paramref name="builder"/>. + /// </summary> + /// <param name="builder">The <see cref="DelegateEndpointConventionBuilder"/>.</param> + /// <returns>A <see cref="DelegateEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> + public static DelegateEndpointConventionBuilder ExcludeFromDescription(this DelegateEndpointConventionBuilder builder) + { + builder.WithMetadata(_excludeFromDescriptionMetadataAttribute); + + return builder; + } + + /// <summary> + /// Adds an <see cref="IProducesResponseTypeMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all builders + /// produced by <paramref name="builder"/>. + /// </summary> + /// <typeparam name="TResponse">The type of the response.</typeparam> + /// <param name="builder">The <see cref="DelegateEndpointConventionBuilder"/>.</param> + /// <param name="statusCode">The response status code. Defaults to StatusCodes.Status200OK.</param> + /// <param name="contentType">The response content type. Defaults to "application/json".</param> + /// <param name="additionalContentTypes">Additional response content types the endpoint produces for the supplied status code.</param> + /// <returns>A <see cref="DelegateEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> +#pragma warning disable RS0026 + public static DelegateEndpointConventionBuilder Produces<TResponse>(this DelegateEndpointConventionBuilder builder, +#pragma warning restore RS0026 + int statusCode = StatusCodes.Status200OK, + string? contentType = null, + params string[] additionalContentTypes) + { + return Produces(builder, statusCode, typeof(TResponse), contentType, additionalContentTypes); + } + + /// <summary> + /// Adds an <see cref="IProducesResponseTypeMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all builders + /// produced by <paramref name="builder"/>. + /// </summary> + /// <param name="builder">The <see cref="DelegateEndpointConventionBuilder"/>.</param> + /// <param name="statusCode">The response status code.</param> + /// <param name="responseType">The type of the response. Defaults to null.</param> + /// <param name="contentType">The response content type. Defaults to "application/json" if responseType is not null, otherwise defaults to null.</param> + /// <param name="additionalContentTypes">Additional response content types the endpoint produces for the supplied status code.</param> + /// <returns>A <see cref="DelegateEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> +#pragma warning disable RS0026 + public static DelegateEndpointConventionBuilder Produces(this DelegateEndpointConventionBuilder builder, +#pragma warning restore RS0026 + int statusCode, + Type? responseType = null, + string? contentType = null, + params string[] additionalContentTypes) + { + if (responseType is Type && string.IsNullOrEmpty(contentType)) + { + contentType = "application/json"; + } + + if (contentType is null) + { + builder.WithMetadata(new ProducesResponseTypeMetadata(responseType ?? typeof(void), statusCode)); + return builder; + } + + builder.WithMetadata(new ProducesResponseTypeMetadata(responseType ?? typeof(void), statusCode, contentType, additionalContentTypes)); + + return builder; + } + + /// <summary> + /// Adds an <see cref="IProducesResponseTypeMetadata"/> with a <see cref="ProblemDetails"/> type + /// to <see cref="EndpointBuilder.Metadata"/> for all builders produced by <paramref name="builder"/>. + /// </summary> + /// <param name="builder">The <see cref="DelegateEndpointConventionBuilder"/>.</param> + /// <param name="statusCode">The response status code.</param> + /// <param name="contentType">The response content type. Defaults to "application/problem+json".</param> + /// <returns>A <see cref="DelegateEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> + public static DelegateEndpointConventionBuilder ProducesProblem(this DelegateEndpointConventionBuilder builder, + int statusCode, + string? contentType = null) + { + if (string.IsNullOrEmpty(contentType)) + { + contentType = "application/problem+json"; + } + + return Produces<ProblemDetails>(builder, statusCode, contentType); + } + + /// <summary> + /// Adds an <see cref="IProducesResponseTypeMetadata"/> with a <see cref="HttpValidationProblemDetails"/> type + /// to <see cref="EndpointBuilder.Metadata"/> for all builders produced by <paramref name="builder"/>. + /// </summary> + /// <param name="builder">The <see cref="DelegateEndpointConventionBuilder"/>.</param> + /// <param name="statusCode">The response status code. Defaults to StatusCodes.Status400BadRequest.</param> + /// <param name="contentType">The response content type. Defaults to "application/validationproblem+json".</param> + /// <returns>A <see cref="DelegateEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> + public static DelegateEndpointConventionBuilder ProducesValidationProblem(this DelegateEndpointConventionBuilder builder, + int statusCode = StatusCodes.Status400BadRequest, + string? contentType = null) + { + if (string.IsNullOrEmpty(contentType)) + { + contentType = "application/validationproblem+json"; + } + + return Produces<HttpValidationProblemDetails>(builder, statusCode, contentType); + } + /// <summary> /// Adds the <see cref="ITagsMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all builders /// produced by <paramref name="builder"/>. @@ -29,5 +140,87 @@ namespace Microsoft.AspNetCore.Http builder.WithMetadata(new TagsAttribute(tags)); return builder; } + + /// <summary> + /// Adds <see cref="IAcceptsMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all builders + /// produced by <paramref name="builder"/>. + /// </summary> + /// <typeparam name="TRequest">The type of the request body.</typeparam> + /// <param name="builder">The <see cref="DelegateEndpointConventionBuilder"/>.</param> + /// <param name="contentType">The request content type that the endpoint accepts.</param> + /// <param name="additionalContentTypes">The list of additional request content types that the endpoint accepts.</param> + /// <returns>A <see cref="DelegateEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> + public static DelegateEndpointConventionBuilder Accepts<TRequest>(this DelegateEndpointConventionBuilder builder, + string contentType, params string[] additionalContentTypes) where TRequest : notnull + { + Accepts(builder, typeof(TRequest), contentType, additionalContentTypes); + + return builder; + } + + /// <summary> + /// Adds <see cref="IAcceptsMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all builders + /// produced by <paramref name="builder"/>. + /// </summary> + /// <typeparam name="TRequest">The type of the request body.</typeparam> + /// <param name="builder">The <see cref="DelegateEndpointConventionBuilder"/>.</param> + /// <param name="isOptional">Sets a value that determines if the request body is optional.</param> + /// <param name="contentType">The request content type that the endpoint accepts.</param> + /// <param name="additionalContentTypes">The list of additional request content types that the endpoint accepts.</param> + /// <returns>A <see cref="DelegateEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> + public static DelegateEndpointConventionBuilder Accepts<TRequest>(this DelegateEndpointConventionBuilder builder, + bool isOptional, string contentType, params string[] additionalContentTypes) where TRequest : notnull + { + Accepts(builder, typeof(TRequest), isOptional, contentType, additionalContentTypes); + + return builder; + } + + /// <summary> + /// Adds <see cref="IAcceptsMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all builders + /// produced by <paramref name="builder"/>. + /// </summary> + /// <param name="builder">The <see cref="DelegateEndpointConventionBuilder"/>.</param> + /// <param name="requestType">The type of the request body.</param> + /// <param name="contentType">The request content type that the endpoint accepts.</param> + /// <param name="additionalContentTypes">The list of additional request content types that the endpoint accepts.</param> + /// <returns>A <see cref="DelegateEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> + public static DelegateEndpointConventionBuilder Accepts(this DelegateEndpointConventionBuilder builder, + Type requestType, string contentType, params string[] additionalContentTypes) + { + builder.WithMetadata(new AcceptsMetadata(requestType, false, GetAllContentTypes(contentType, additionalContentTypes))); + return builder; + } + + + /// <summary> + /// Adds <see cref="IAcceptsMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all builders + /// produced by <paramref name="builder"/>. + /// </summary> + /// <param name="builder">The <see cref="DelegateEndpointConventionBuilder"/>.</param> + /// <param name="requestType">The type of the request body.</param> + /// <param name="isOptional">Sets a value that determines if the request body is optional.</param> + /// <param name="contentType">The request content type that the endpoint accepts.</param> + /// <param name="additionalContentTypes">The list of additional request content types that the endpoint accepts.</param> + /// <returns>A <see cref="DelegateEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> + public static DelegateEndpointConventionBuilder Accepts(this DelegateEndpointConventionBuilder builder, + Type requestType, bool isOptional, string contentType, params string[] additionalContentTypes) + { + builder.WithMetadata(new AcceptsMetadata(requestType, isOptional, GetAllContentTypes(contentType, additionalContentTypes))); + return builder; + } + + private static string[] GetAllContentTypes(string contentType, string[] additionalContentTypes) + { + var allContentTypes = new string[additionalContentTypes.Length + 1]; + allContentTypes[0] = contentType; + + for (var i = 0; i < additionalContentTypes.Length; i++) + { + allContentTypes[i + 1] = additionalContentTypes[i]; + } + + return allContentTypes; + } } -} \ No newline at end of file +} diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index 20e688ff0da..436280d15f3 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -30,6 +30,8 @@ <Compile Include="$(SharedSourceRoot)RoslynUtils\GeneratedNameParser.cs" /> <Compile Include="$(SharedSourceRoot)MediaType\ReadOnlyMediaTypeHeaderValue.cs" LinkBase="Shared" /> <Compile Include="$(SharedSourceRoot)MediaType\HttpTokenParsingRule.cs" LinkBase="Shared" /> + <Compile Include="$(SharedSourceRoot)ApiExplorerTypes\*.cs" LinkBase="Shared" /> + <Compile Include="$(SharedSourceRoot)RoutingMetadata\AcceptsMetadata.cs" LinkBase="Shared" /> </ItemGroup> <ItemGroup> diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt index 068b3c9e035..636eacda9f7 100644 --- a/src/Http/Routing/src/PublicAPI.Unshipped.txt +++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt @@ -44,5 +44,14 @@ Microsoft.AspNetCore.Routing.IExcludeFromDescriptionMetadata.ExcludeFromDescript Microsoft.AspNetCore.Routing.ExcludeFromDescriptionAttribute Microsoft.AspNetCore.Routing.ExcludeFromDescriptionAttribute.ExcludeFromDescriptionAttribute() -> void Microsoft.AspNetCore.Routing.ExcludeFromDescriptionAttribute.ExcludeFromDescription.get -> bool +static Microsoft.AspNetCore.Http.OpenApiDelegateEndpointConventionBuilderExtensions.WithTags(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, params string![]! tags) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiDelegateEndpointConventionBuilderExtensions.Accepts(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, System.Type! requestType, bool isOptional, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiDelegateEndpointConventionBuilderExtensions.Accepts(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, System.Type! requestType, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiDelegateEndpointConventionBuilderExtensions.Accepts<TRequest>(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, bool isOptional, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiDelegateEndpointConventionBuilderExtensions.Accepts<TRequest>(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiDelegateEndpointConventionBuilderExtensions.ExcludeFromDescription(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiDelegateEndpointConventionBuilderExtensions.Produces(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, int statusCode, System.Type? responseType = null, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiDelegateEndpointConventionBuilderExtensions.Produces<TResponse>(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, int statusCode = 200, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiDelegateEndpointConventionBuilderExtensions.ProducesProblem(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, int statusCode, string? contentType = null) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! +static Microsoft.AspNetCore.Http.OpenApiDelegateEndpointConventionBuilderExtensions.ProducesValidationProblem(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, int statusCode = 400, string? contentType = null) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! Microsoft.AspNetCore.Http.OpenApiDelegateEndpointConventionBuilderExtensions -static Microsoft.AspNetCore.Http.OpenApiDelegateEndpointConventionBuilderExtensions.WithTags(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, params string![]! tags) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! \ No newline at end of file diff --git a/src/Http/Routing/test/UnitTests/Microsoft.AspNetCore.Routing.Tests.csproj b/src/Http/Routing/test/UnitTests/Microsoft.AspNetCore.Routing.Tests.csproj index f49b90bbad7..b184a2f3a01 100644 --- a/src/Http/Routing/test/UnitTests/Microsoft.AspNetCore.Routing.Tests.csproj +++ b/src/Http/Routing/test/UnitTests/Microsoft.AspNetCore.Routing.Tests.csproj @@ -15,8 +15,4 @@ <Reference Include="Microsoft.Extensions.WebEncoders" /> </ItemGroup> - <ItemGroup> - <Compile Include="$(SharedSourceRoot)RoutingMetadata\AcceptsMetadata.cs" LinkBase="Shared" /> - </ItemGroup> - </Project> diff --git a/src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs index bac4c5b7fed..a8ecde793d1 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs @@ -201,7 +201,8 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer return results.Values.ToList(); } - private static void CalculateResponseFormatForType(ApiResponseType apiResponse, MediaTypeCollection declaredContentTypes, IEnumerable<IApiResponseTypeMetadataProvider>? responseTypeMetadataProviders, IModelMetadataProvider? modelMetadataProvider) + // Shared with EndpointMetadataApiDescriptionProvider + internal static void CalculateResponseFormatForType(ApiResponseType apiResponse, MediaTypeCollection declaredContentTypes, IEnumerable<IApiResponseTypeMetadataProvider>? responseTypeMetadataProviders, IModelMetadataProvider? modelMetadataProvider) { // If response formats have already been calculate for this type, // then exit early. This avoids populating the ApiResponseFormat for diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs index cdf597998dd..bcdb99fe719 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs @@ -243,15 +243,23 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer responseType = typeof(void); } - var responseMetadata = endpointMetadata.GetOrderedMetadata<IApiResponseMetadataProvider>(); + // We support attributes (which implement the IApiResponseMetadataProvider) interface + // and types added via the extension methods (which implement IProducesResponseTypeMetadata). + var responseProviderMetadata = endpointMetadata.GetOrderedMetadata<IApiResponseMetadataProvider>(); + var producesResponseMetadata = endpointMetadata.GetOrderedMetadata<IProducesResponseTypeMetadata>(); var errorMetadata = endpointMetadata.GetMetadata<ProducesErrorResponseTypeAttribute>(); var defaultErrorType = errorMetadata?.Type ?? typeof(void); var contentTypes = new MediaTypeCollection(); - var responseMetadataTypes = ApiResponseTypeProvider.ReadResponseMetadata( - responseMetadata, responseType, defaultErrorType, contentTypes); + var responseProviderMetadataTypes = ApiResponseTypeProvider.ReadResponseMetadata( + responseProviderMetadata, responseType, defaultErrorType, contentTypes); + var producesResponseMetadataTypes = ReadResponseMetadata(producesResponseMetadata, responseType); - if (responseMetadataTypes.Count > 0) + // We favor types added via the extension methods (which implements IProducesResponseTypeMetadata) + // over those that are added via attributes. + var responseMetadataTypes = producesResponseMetadataTypes.Values.Concat(responseProviderMetadataTypes); + + if (responseMetadataTypes.Any()) { foreach (var apiResponseType in responseMetadataTypes) { @@ -275,7 +283,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer apiResponseType.ApiResponseFormats.Add(defaultResponseFormat); } - supportedResponseTypes.Add(apiResponseType); + if (!supportedResponseTypes.Any(existingResponseType => existingResponseType.StatusCode == apiResponseType.StatusCode)) + { + supportedResponseTypes.Add(apiResponseType); + } + } } else @@ -294,6 +306,51 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer } } + private static Dictionary<int, ApiResponseType> ReadResponseMetadata( + IReadOnlyList<IProducesResponseTypeMetadata> responseMetadata, + Type? type) + { + var results = new Dictionary<int, ApiResponseType>(); + + foreach (var metadata in responseMetadata) + { + var statusCode = metadata.StatusCode; + + var apiResponseType = new ApiResponseType + { + Type = metadata.Type, + StatusCode = statusCode, + }; + + if (apiResponseType.Type == typeof(void)) + { + if (type != null && (statusCode == StatusCodes.Status200OK || statusCode == StatusCodes.Status201Created)) + { + // Allow setting the response type from the return type of the method if it has + // not been set explicitly by the method. + apiResponseType.Type = type; + } + } + + var attributeContentTypes = new MediaTypeCollection(); + if (metadata.ContentTypes != null) + { + foreach (var contentType in metadata.ContentTypes) + { + attributeContentTypes.Add(contentType); + } + } + ApiResponseTypeProvider.CalculateResponseFormatForType(apiResponseType, attributeContentTypes, responseTypeMetadataProviders: null, modelMetadataProvider: null); + + if (apiResponseType.Type != null) + { + results[apiResponseType.StatusCode] = apiResponseType; + } + } + + return results; + } + private static ApiResponseType CreateDefaultApiResponseType(Type responseType) { var apiResponseType = new ApiResponseType diff --git a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs index a004de2cab5..1866dd07578 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs @@ -635,6 +635,38 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer Assert.True(bodyParameterDescription.IsRequired); } + [Fact] + public void FavorsProducesMetadataOverAttribute() + { + // Arrange + var builder = CreateBuilder(); + builder.MapGet("/api/todos", [ProducesResponseType(typeof(List<string>), StatusCodes.Status200OK)]() => "") + .Produces<InferredJsonClass>(StatusCodes.Status200OK); + var context = new ApiDescriptionProviderContext(Array.Empty<ActionDescriptor>()); + + var endpointDataSource = builder.DataSources.OfType<EndpointDataSource>().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)); + + }); + } + #nullable enable [Fact] diff --git a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs deleted file mode 100644 index a233aa8c8a5..00000000000 --- a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs +++ /dev/null @@ -1,207 +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 Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http.Metadata; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; - -namespace Microsoft.AspNetCore.Http -{ - /// <summary> - /// Extension methods for adding response type metadata to endpoints. - /// </summary> - public static class OpenApiEndpointConventionBuilderExtensions - { - private static readonly ExcludeFromDescriptionAttribute _excludeFromDescriptionMetadataAttribute = new(); - - /// <summary> - /// Adds the <see cref="IExcludeFromDescriptionMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all builders - /// produced by <paramref name="builder"/>. - /// </summary> - /// <param name="builder">The <see cref="DelegateEndpointConventionBuilder"/>.</param> - /// <returns>A <see cref="DelegateEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> - public static DelegateEndpointConventionBuilder ExcludeFromDescription(this DelegateEndpointConventionBuilder builder) - { - builder.WithMetadata(_excludeFromDescriptionMetadataAttribute); - - return builder; - } - - /// <summary> - /// Adds the <see cref="ProducesResponseTypeAttribute"/> to <see cref="EndpointBuilder.Metadata"/> for all builders - /// produced by <paramref name="builder"/>. - /// </summary> - /// <typeparam name="TResponse">The type of the response.</typeparam> - /// <param name="builder">The <see cref="DelegateEndpointConventionBuilder"/>.</param> - /// <param name="statusCode">The response status code. Defaults to StatusCodes.Status200OK.</param> - /// <param name="contentType">The response content type. Defaults to "application/json".</param> - /// <param name="additionalContentTypes">Additional response content types the endpoint produces for the supplied status code.</param> - /// <returns>A <see cref="DelegateEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> -#pragma warning disable RS0026 - public static DelegateEndpointConventionBuilder Produces<TResponse>(this DelegateEndpointConventionBuilder builder, -#pragma warning restore RS0026 - int statusCode = StatusCodes.Status200OK, - string? contentType = null, - params string[] additionalContentTypes) - { - return Produces(builder, statusCode, typeof(TResponse), contentType, additionalContentTypes); - } - - /// <summary> - /// Adds the <see cref="ProducesResponseTypeAttribute"/> to <see cref="EndpointBuilder.Metadata"/> for all builders - /// produced by <paramref name="builder"/>. - /// </summary> - /// <param name="builder">The <see cref="DelegateEndpointConventionBuilder"/>.</param> - /// <param name="statusCode">The response status code.</param> - /// <param name="responseType">The type of the response. Defaults to null.</param> - /// <param name="contentType">The response content type. Defaults to "application/json" if responseType is not null, otherwise defaults to null.</param> - /// <param name="additionalContentTypes">Additional response content types the endpoint produces for the supplied status code.</param> - /// <returns>A <see cref="DelegateEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> -#pragma warning disable RS0026 - public static DelegateEndpointConventionBuilder Produces(this DelegateEndpointConventionBuilder builder, -#pragma warning restore RS0026 - int statusCode, - Type? responseType = null, - string? contentType = null, - params string[] additionalContentTypes) - { - if (responseType is Type && string.IsNullOrEmpty(contentType)) - { - 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; - } - - /// <summary> - /// Adds the <see cref="ProducesResponseTypeAttribute"/> with a <see cref="ProblemDetails"/> type - /// to <see cref="EndpointBuilder.Metadata"/> for all builders produced by <paramref name="builder"/>. - /// </summary> - /// <param name="builder">The <see cref="DelegateEndpointConventionBuilder"/>.</param> - /// <param name="statusCode">The response status code.</param> - /// <param name="contentType">The response content type. Defaults to "application/problem+json".</param> - /// <returns>A <see cref="DelegateEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> - public static DelegateEndpointConventionBuilder ProducesProblem(this DelegateEndpointConventionBuilder builder, - int statusCode, - string? contentType = null) - { - if (string.IsNullOrEmpty(contentType)) - { - contentType = "application/problem+json"; - } - - return Produces<ProblemDetails>(builder, statusCode, contentType); - } - - /// <summary> - /// Adds the <see cref="ProducesResponseTypeAttribute"/> with a <see cref="HttpValidationProblemDetails"/> type - /// to <see cref="EndpointBuilder.Metadata"/> for all builders produced by <paramref name="builder"/>. - /// </summary> - /// <param name="builder">The <see cref="DelegateEndpointConventionBuilder"/>.</param> - /// <param name="statusCode">The response status code. Defaults to StatusCodes.Status400BadRequest.</param> - /// <param name="contentType">The response content type. Defaults to "application/validationproblem+json".</param> - /// <returns>A <see cref="DelegateEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> - public static DelegateEndpointConventionBuilder ProducesValidationProblem(this DelegateEndpointConventionBuilder builder, - int statusCode = StatusCodes.Status400BadRequest, - string? contentType = null) - { - if (string.IsNullOrEmpty(contentType)) - { - contentType = "application/validationproblem+json"; - } - - return Produces<HttpValidationProblemDetails>(builder, statusCode, contentType); - } - - /// <summary> - /// Adds <see cref="IAcceptsMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all builders - /// produced by <paramref name="builder"/>. - /// </summary> - /// <typeparam name="TRequest">The type of the request body.</typeparam> - /// <param name="builder">The <see cref="DelegateEndpointConventionBuilder"/>.</param> - /// <param name="contentType">The request content type that the endpoint accepts.</param> - /// <param name="additionalContentTypes">The list of additional request content types that the endpoint accepts.</param> - /// <returns>A <see cref="DelegateEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> - public static DelegateEndpointConventionBuilder Accepts<TRequest>(this DelegateEndpointConventionBuilder builder, - string contentType, params string[] additionalContentTypes) where TRequest : notnull - { - Accepts(builder, typeof(TRequest), contentType, additionalContentTypes); - - return builder; - } - - /// <summary> - /// Adds <see cref="IAcceptsMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all builders - /// produced by <paramref name="builder"/>. - /// </summary> - /// <typeparam name="TRequest">The type of the request body.</typeparam> - /// <param name="builder">The <see cref="DelegateEndpointConventionBuilder"/>.</param> - /// <param name="isOptional">Sets a value that determines if the request body is optional.</param> - /// <param name="contentType">The request content type that the endpoint accepts.</param> - /// <param name="additionalContentTypes">The list of additional request content types that the endpoint accepts.</param> - /// <returns>A <see cref="DelegateEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> - public static DelegateEndpointConventionBuilder Accepts<TRequest>(this DelegateEndpointConventionBuilder builder, - bool isOptional, string contentType, params string[] additionalContentTypes) where TRequest : notnull - { - Accepts(builder, typeof(TRequest), isOptional, contentType, additionalContentTypes); - - return builder; - } - - /// <summary> - /// Adds <see cref="IAcceptsMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all builders - /// produced by <paramref name="builder"/>. - /// </summary> - /// <param name="builder">The <see cref="DelegateEndpointConventionBuilder"/>.</param> - /// <param name="requestType">The type of the request body.</param> - /// <param name="contentType">The request content type that the endpoint accepts.</param> - /// <param name="additionalContentTypes">The list of additional request content types that the endpoint accepts.</param> - /// <returns>A <see cref="DelegateEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> - public static DelegateEndpointConventionBuilder Accepts(this DelegateEndpointConventionBuilder builder, - Type requestType, string contentType, params string[] additionalContentTypes) - { - builder.WithMetadata(new AcceptsMetadata(requestType, false, GetAllContentTypes(contentType, additionalContentTypes))); - return builder; - } - - - /// <summary> - /// Adds <see cref="IAcceptsMetadata"/> to <see cref="EndpointBuilder.Metadata"/> for all builders - /// produced by <paramref name="builder"/>. - /// </summary> - /// <param name="builder">The <see cref="DelegateEndpointConventionBuilder"/>.</param> - /// <param name="requestType">The type of the request body.</param> - /// <param name="isOptional">Sets a value that determines if the request body is optional.</param> - /// <param name="contentType">The request content type that the endpoint accepts.</param> - /// <param name="additionalContentTypes">The list of additional request content types that the endpoint accepts.</param> - /// <returns>A <see cref="DelegateEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns> - public static DelegateEndpointConventionBuilder Accepts(this DelegateEndpointConventionBuilder builder, - Type requestType, bool isOptional, string contentType, params string[] additionalContentTypes) - { - builder.WithMetadata(new AcceptsMetadata(requestType, isOptional, GetAllContentTypes(contentType, additionalContentTypes))); - return builder; - } - - private static string[] GetAllContentTypes(string contentType, string[] additionalContentTypes) - { - var allContentTypes = new string[additionalContentTypes.Length + 1]; - allContentTypes[0] = contentType; - - for (var i = 0; i < additionalContentTypes.Length; i++) - { - allContentTypes[i + 1] = additionalContentTypes[i]; - } - - return allContentTypes; - } - } -} diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index bd730b12110..fc22667686c 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -547,15 +547,6 @@ Microsoft.AspNetCore.Mvc.Infrastructure.ActionDescriptorCollection.Items.get -> Microsoft.AspNetCore.Mvc.Infrastructure.AmbiguousActionException.AmbiguousActionException(System.Runtime.Serialization.SerializationInfo! info, System.Runtime.Serialization.StreamingContext context) -> void Microsoft.AspNetCore.Mvc.Infrastructure.AmbiguousActionException.AmbiguousActionException(string? message) -> void Microsoft.AspNetCore.Mvc.Infrastructure.ContentResultExecutor.ContentResultExecutor(Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Mvc.Infrastructure.ContentResultExecutor!>! logger, Microsoft.AspNetCore.Mvc.Infrastructure.IHttpResponseStreamWriterFactory! httpResponseStreamWriterFactory) -> void -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Accepts(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, System.Type! requestType, bool isOptional, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Accepts(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, System.Type! requestType, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Accepts<TRequest>(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, bool isOptional, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Accepts<TRequest>(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ExcludeFromDescription(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Produces(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, int statusCode, System.Type? responseType = null, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Produces<TResponse>(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, int statusCode = 200, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ProducesProblem(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, int statusCode, string? contentType = null) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! -static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ProducesValidationProblem(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, int statusCode = 400, string? contentType = null) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! ~Microsoft.AspNetCore.Mvc.Infrastructure.DefaultOutputFormatterSelector.DefaultOutputFormatterSelector(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Mvc.MvcOptions!>! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void Microsoft.AspNetCore.Mvc.Infrastructure.FileContentResultExecutor.FileContentResultExecutor(Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void Microsoft.AspNetCore.Mvc.Infrastructure.FileResultExecutorBase.FileResultExecutorBase(Microsoft.Extensions.Logging.ILogger! logger) -> void @@ -2040,9 +2031,6 @@ Microsoft.AspNetCore.Mvc.Formatters.MediaType.Encoding.get -> System.Text.Encodi Microsoft.AspNetCore.Mvc.Formatters.MediaType.GetParameter(string! parameterName) -> Microsoft.Extensions.Primitives.StringSegment Microsoft.AspNetCore.Mvc.Formatters.MediaType.MediaType(string! mediaType) -> void Microsoft.AspNetCore.Mvc.Formatters.MediaType.MediaType(string! mediaType, int offset, int? length) -> void -Microsoft.AspNetCore.Mvc.Formatters.MediaTypeCollection.Add(Microsoft.Net.Http.Headers.MediaTypeHeaderValue! item) -> void -Microsoft.AspNetCore.Mvc.Formatters.MediaTypeCollection.Insert(int index, Microsoft.Net.Http.Headers.MediaTypeHeaderValue! item) -> void -Microsoft.AspNetCore.Mvc.Formatters.MediaTypeCollection.Remove(Microsoft.Net.Http.Headers.MediaTypeHeaderValue! item) -> bool Microsoft.AspNetCore.Mvc.Formatters.OutputFormatter.SupportedMediaTypes.get -> Microsoft.AspNetCore.Mvc.Formatters.MediaTypeCollection! Microsoft.AspNetCore.Mvc.Formatters.StreamOutputFormatter.CanWriteResult(Microsoft.AspNetCore.Mvc.Formatters.OutputFormatterCanWriteContext! context) -> bool Microsoft.AspNetCore.Mvc.Formatters.StreamOutputFormatter.WriteAsync(Microsoft.AspNetCore.Mvc.Formatters.OutputFormatterWriteContext! context) -> System.Threading.Tasks.Task! @@ -2053,6 +2041,9 @@ Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonOutputFormatter.SystemTextJson Microsoft.AspNetCore.Mvc.Formatters.TextInputFormatter.SelectCharacterEncoding(Microsoft.AspNetCore.Mvc.Formatters.InputFormatterContext! context) -> System.Text.Encoding? Microsoft.AspNetCore.Mvc.Formatters.TextInputFormatter.SupportedEncodings.get -> System.Collections.Generic.IList<System.Text.Encoding!>! Microsoft.AspNetCore.Mvc.Formatters.TextOutputFormatter.SupportedEncodings.get -> System.Collections.Generic.IList<System.Text.Encoding!>! +Microsoft.AspNetCore.Mvc.Formatters.MediaTypeCollection.Add(Microsoft.Net.Http.Headers.MediaTypeHeaderValue! item) -> void +Microsoft.AspNetCore.Mvc.Formatters.MediaTypeCollection.Insert(int index, Microsoft.Net.Http.Headers.MediaTypeHeaderValue! item) -> void +Microsoft.AspNetCore.Mvc.Formatters.MediaTypeCollection.Remove(Microsoft.Net.Http.Headers.MediaTypeHeaderValue! item) -> bool Microsoft.AspNetCore.Mvc.FromBodyAttribute.BindingSource.get -> Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource! Microsoft.AspNetCore.Mvc.FromFormAttribute.BindingSource.get -> Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource! Microsoft.AspNetCore.Mvc.FromFormAttribute.Name.get -> string? @@ -3232,4 +3223,3 @@ virtual Microsoft.AspNetCore.Mvc.ProducesAttribute.OnResultExecuting(Microsoft.A 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.Http.OpenApiEndpointConventionBuilderExtensions diff --git a/src/Shared/ApiExplorerTypes/ProducesResponseTypeMetadata.cs b/src/Shared/ApiExplorerTypes/ProducesResponseTypeMetadata.cs new file mode 100644 index 00000000000..70ed78ac85c --- /dev/null +++ b/src/Shared/ApiExplorerTypes/ProducesResponseTypeMetadata.cs @@ -0,0 +1,114 @@ +// 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 Microsoft.Net.Http.Headers; +using Microsoft.AspNetCore.Http.Metadata; + +namespace Microsoft.AspNetCore.Http +{ + /// <summary> + /// Specifies the type of the value and status code returned by the action. + /// </summary> + internal sealed class ProducesResponseTypeMetadata : IProducesResponseTypeMetadata + { + private readonly IEnumerable<string> _contentTypes; + + /// <summary> + /// Initializes an instance of <see cref="ProducesResponseTypeMetadata"/>. + /// </summary> + /// <param name="statusCode">The HTTP response status code.</param> + public ProducesResponseTypeMetadata(int statusCode) + : this(typeof(void), statusCode) + { + IsResponseTypeSetByDefault = true; + } + + /// <summary> + /// Initializes an instance of <see cref="ProducesResponseTypeMetadata"/>. + /// </summary> + /// <param name="type">The <see cref="Type"/> of object that is going to be written in the response.</param> + /// <param name="statusCode">The HTTP response status code.</param> + public ProducesResponseTypeMetadata(Type type, int statusCode) + { + Type = type ?? throw new ArgumentNullException(nameof(type)); + StatusCode = statusCode; + IsResponseTypeSetByDefault = false; + _contentTypes = Enumerable.Empty<string>(); + } + + /// <summary> + /// Initializes an instance of <see cref="ProducesResponseTypeMetadata"/>. + /// </summary> + /// <param name="type">The <see cref="Type"/> of object that is going to be written in the response.</param> + /// <param name="statusCode">The HTTP response status code.</param> + /// <param name="contentType">The content type associated with the response.</param> + /// <param name="additionalContentTypes">Additional content types supported by the response.</param> + public ProducesResponseTypeMetadata(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; + + MediaTypeHeaderValue.Parse(contentType); + for (var i = 0; i < additionalContentTypes.Length; i++) + { + MediaTypeHeaderValue.Parse(additionalContentTypes[i]); + } + + _contentTypes = GetContentTypes(contentType, additionalContentTypes); + } + + /// <summary> + /// Gets or sets the type of the value returned by an action. + /// </summary> + public Type Type { get; set; } + + /// <summary> + /// Gets or sets the HTTP status code of the response. + /// </summary> + public int StatusCode { get; set; } + + /// <summary> + /// Used to distinguish a `Type` set by default in the constructor versus + /// one provided by the user. + /// + /// When <see langword="false"/>, then <see cref="Type"/> is set by user. + /// + /// When <see langword="true"/>, then <see cref="Type"/> is set by by + /// default in the constructor + /// </summary> + /// <value></value> + internal bool IsResponseTypeSetByDefault { get; } + + public IEnumerable<string> ContentTypes => _contentTypes; + + private static List<string> GetContentTypes(string contentType, string[] additionalContentTypes) + { + var contentTypes = new List<string>(additionalContentTypes.Length + 1); + ValidateContentType(contentType); + contentTypes.Add(contentType); + foreach (var type in additionalContentTypes) + { + ValidateContentType(type); + contentTypes.Add(type); + } + + return contentTypes; + + static void ValidateContentType(string type) + { + if (type.Contains('*', StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException($"Could not parse '{type}'. Content types with wildcards are not supported."); + } + } + } + } +} -- GitLab