From 1f433b61d7ab594cd0b07a52494db63baf26c857 Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Sun, 17 Mar 2024 18:32:25 +0100 Subject: [PATCH 01/18] Started adding Description to several attributes --- .../src/ApiExplorer/IApiResponseMetadataProvider.cs | 5 +++++ src/Mvc/Mvc.Core/src/ProducesAttribute.cs | 3 +++ .../Mvc.Core/src/ProducesDefaultResponseTypeAttribute.cs | 7 ++++++- src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs | 7 ++++++- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Mvc/Mvc.Core/src/ApiExplorer/IApiResponseMetadataProvider.cs b/src/Mvc/Mvc.Core/src/ApiExplorer/IApiResponseMetadataProvider.cs index 37e1d7daffab..5b65a587f830 100644 --- a/src/Mvc/Mvc.Core/src/ApiExplorer/IApiResponseMetadataProvider.cs +++ b/src/Mvc/Mvc.Core/src/ApiExplorer/IApiResponseMetadataProvider.cs @@ -17,6 +17,11 @@ public interface IApiResponseMetadataProvider : IFilterMetadata /// Type? Type { get; } + /// + /// Gets the description of the response. + /// + string? Description { get; } + /// /// Gets the HTTP status code of the response. /// diff --git a/src/Mvc/Mvc.Core/src/ProducesAttribute.cs b/src/Mvc/Mvc.Core/src/ProducesAttribute.cs index f7bbbc380dfc..bcef087a2e6a 100644 --- a/src/Mvc/Mvc.Core/src/ProducesAttribute.cs +++ b/src/Mvc/Mvc.Core/src/ProducesAttribute.cs @@ -52,6 +52,9 @@ public ProducesAttribute(string contentType, params string[] additionalContentTy /// public Type? Type { get; set; } + /// + public string? Description { get; set; } + /// /// Gets or sets the supported response content types. Used to set . /// diff --git a/src/Mvc/Mvc.Core/src/ProducesDefaultResponseTypeAttribute.cs b/src/Mvc/Mvc.Core/src/ProducesDefaultResponseTypeAttribute.cs index 95468183d7c1..2606a17f5ff8 100644 --- a/src/Mvc/Mvc.Core/src/ProducesDefaultResponseTypeAttribute.cs +++ b/src/Mvc/Mvc.Core/src/ProducesDefaultResponseTypeAttribute.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 Microsoft.AspNetCore.Mvc.ApiExplorer; @@ -39,6 +39,11 @@ public ProducesDefaultResponseTypeAttribute(Type type) /// public int StatusCode { get; } + /// + /// Gets the description of the response. + /// + public string? Description { get; set; } + /// void IApiResponseMetadataProvider.SetContentTypes(MediaTypeCollection contentTypes) { diff --git a/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs b/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs index 6399a66d464d..c2a39d9a78af 100644 --- a/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs +++ b/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.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 Microsoft.AspNetCore.Mvc.ApiExplorer; @@ -87,6 +87,11 @@ public ProducesResponseTypeAttribute(Type type, int statusCode, string contentTy // Internal for testing internal MediaTypeCollection? ContentTypes => _contentTypes; + /// + /// Gets the description of the response. + /// + public string? Description { get; set; } + /// void IApiResponseMetadataProvider.SetContentTypes(MediaTypeCollection contentTypes) { From d71759ca3c45a9fbe56cb66915913df1c677b68f Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Tue, 30 Apr 2024 22:24:23 +0200 Subject: [PATCH 02/18] Add new properties to unshipped apis --- src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index 82795268b6c5..7739a5d0b4df 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -1,6 +1,13 @@ #nullable enable *REMOVED*virtual Microsoft.AspNetCore.Mvc.ControllerBase.Problem(string? detail = null, string? instance = null, int? statusCode = null, string? title = null, string? type = null) -> Microsoft.AspNetCore.Mvc.ObjectResult! *REMOVED*virtual Microsoft.AspNetCore.Mvc.ControllerBase.ValidationProblem(string? detail = null, string? instance = null, int? statusCode = null, string? title = null, string? type = null, Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary? modelStateDictionary = null) -> Microsoft.AspNetCore.Mvc.ActionResult! +Microsoft.AspNetCore.Mvc.ApiExplorer.IApiResponseMetadataProvider.Description.get -> string? +Microsoft.AspNetCore.Mvc.ProducesAttribute.Description.get -> string? +Microsoft.AspNetCore.Mvc.ProducesAttribute.Description.set -> void +Microsoft.AspNetCore.Mvc.ProducesDefaultResponseTypeAttribute.Description.get -> string? +Microsoft.AspNetCore.Mvc.ProducesDefaultResponseTypeAttribute.Description.set -> void +Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute.Description.get -> string? +Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute.Description.set -> void virtual Microsoft.AspNetCore.Mvc.ControllerBase.Problem(string? detail, string? instance, int? statusCode, string? title, string? type) -> Microsoft.AspNetCore.Mvc.ObjectResult! virtual Microsoft.AspNetCore.Mvc.ControllerBase.Problem(string? detail = null, string? instance = null, int? statusCode = null, string? title = null, string? type = null, System.Collections.Generic.IDictionary? extensions = null) -> Microsoft.AspNetCore.Mvc.ObjectResult! virtual Microsoft.AspNetCore.Mvc.ControllerBase.ValidationProblem(string? detail, string? instance, int? statusCode, string? title, string? type, Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary? modelStateDictionary) -> Microsoft.AspNetCore.Mvc.ActionResult! From 019bd172ee6d4883c6bb00541ea6a83235b5448b Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Tue, 30 Apr 2024 22:37:59 +0200 Subject: [PATCH 03/18] Some more progress of adding Description in some places --- src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiResponseType.cs | 5 +++++ src/Mvc/Mvc.Abstractions/src/PublicAPI.Unshipped.txt | 2 ++ .../GetResponseMetadataTests.cs | 4 +++- src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs | 3 +++ .../test/DefaultApiDescriptionProviderTest.cs | 2 ++ 5 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiResponseType.cs b/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiResponseType.cs index 71fc8ecf46d9..549f0b659b86 100644 --- a/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiResponseType.cs +++ b/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiResponseType.cs @@ -33,6 +33,11 @@ public class ApiResponseType /// public Type? Type { get; set; } + /// + /// Gets the description of the response. + /// + public string? Description { get; set; } + /// /// Gets or sets the HTTP response status code. /// diff --git a/src/Mvc/Mvc.Abstractions/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Abstractions/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..ada28c50d99a 100644 --- a/src/Mvc/Mvc.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Abstractions/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Mvc.ApiExplorer.ApiResponseType.Description.get -> string? +Microsoft.AspNetCore.Mvc.ApiExplorer.ApiResponseType.Description.set -> void diff --git a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/SymbolApiResponseMetadataProviderTest/GetResponseMetadataTests.cs b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/SymbolApiResponseMetadataProviderTest/GetResponseMetadataTests.cs index 6bf00e9c555d..d355f834ceeb 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/SymbolApiResponseMetadataProviderTest/GetResponseMetadataTests.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/SymbolApiResponseMetadataProviderTest/GetResponseMetadataTests.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; @@ -60,6 +60,8 @@ public class CustomResponseTypeAttribute : Attribute, IApiResponseMetadataProvid public int StatusCode { get; set; } + public string Description { get; set; } + public void SetContentTypes(MediaTypeCollection contentTypes) { } diff --git a/src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs index d1f151812ae6..645421a7fce7 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs @@ -163,11 +163,14 @@ internal static Dictionary ReadResponseMetadata( var statusCode = metadataAttribute.StatusCode; + var description = metadataAttribute.Description; + var apiResponseType = new ApiResponseType { Type = metadataAttribute.Type, StatusCode = statusCode, IsDefaultResponse = metadataAttribute is IApiDefaultResponseMetadataProvider, + Description = description }; if (apiResponseType.Type == typeof(void)) diff --git a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs index ab0b5ebe592f..7c23b545b114 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs @@ -2874,6 +2874,8 @@ public ContentTypeAttribute(string mediaType) public Type Type { get; set; } + public string Description { get; set; } + public void SetContentTypes(MediaTypeCollection contentTypes) { contentTypes.Clear(); From 6da811acf5794a6273879461582373492223a602 Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Fri, 16 Aug 2024 14:28:53 +0200 Subject: [PATCH 04/18] Small improvements based on API review comments --- .../src/Metadata/IProducesResponseTypeMetadata.cs | 5 +++++ .../src/Metadata/ProducesResponseTypeMetadata.cs | 5 +++++ src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiResponseType.cs | 2 +- src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs | 2 +- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs index 92bc264271ba..4f832cfea935 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs @@ -18,6 +18,11 @@ public interface IProducesResponseTypeMetadata /// int StatusCode { get; } + /// + /// Gets the description of the response. + /// + string? Description { get; } + /// /// Gets the content types supported by the metadata. /// diff --git a/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs index edb1ab091dc0..70f96351257d 100644 --- a/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs @@ -68,6 +68,11 @@ private ProducesResponseTypeMetadata(int statusCode, Type? type, IEnumerable public int StatusCode { get; private set; } + /// + /// Gets or sets the description of the response. + /// + string? Description { get; private set; } + /// /// Gets or sets the content types associated with the response. /// diff --git a/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiResponseType.cs b/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiResponseType.cs index 549f0b659b86..2bfe14ff7b45 100644 --- a/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiResponseType.cs +++ b/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiResponseType.cs @@ -34,7 +34,7 @@ public class ApiResponseType public Type? Type { get; set; } /// - /// Gets the description of the response. + /// Gets or sets the description of the response. /// public string? Description { get; set; } diff --git a/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs b/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs index c2a39d9a78af..f9c55192040e 100644 --- a/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs +++ b/src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs @@ -88,7 +88,7 @@ public ProducesResponseTypeAttribute(Type type, int statusCode, string contentTy internal MediaTypeCollection? ContentTypes => _contentTypes; /// - /// Gets the description of the response. + /// Gets or sets the description of the response. /// public string? Description { get; set; } From 951676d37326ce817b9ad3cfe585d2f79fc51555 Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Fri, 16 Aug 2024 14:58:07 +0200 Subject: [PATCH 05/18] Added missing modifier to property --- .../src/Metadata/ProducesResponseTypeMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs index 70f96351257d..540f0a1bc6fd 100644 --- a/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs @@ -71,7 +71,7 @@ private ProducesResponseTypeMetadata(int statusCode, Type? type, IEnumerable /// Gets or sets the description of the response. /// - string? Description { get; private set; } + public string? Description { get; private set; } /// /// Gets or sets the content types associated with the response. From 3bdbe26d0bbe77b187620ef81e4f066459070862 Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Fri, 16 Aug 2024 15:14:14 +0200 Subject: [PATCH 06/18] Make changes in unshipped.txt so the http project builds --- src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 0d2aab25f97d..6940023e6978 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -11,3 +11,5 @@ Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata.HasTryParse.get -> Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata.IsOptional.get -> bool Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata.Name.get -> string! Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata.ParameterInfo.get -> System.Reflection.ParameterInfo! +Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.Description.get -> string? +Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata.Description.get -> string? From 442d101b3f26b2292630b7b11e714c0e8960c7cb Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Fri, 16 Aug 2024 15:45:57 +0200 Subject: [PATCH 07/18] Add Description to OpenApiRouteHandlerBuilderExtensions and extra constructor and static methods to ProducesResponseTypeMetadata for correct description overloads --- .../Metadata/ProducesResponseTypeMetadata.cs | 49 +++++- .../OpenApiRouteHandlerBuilderExtensions.cs | 151 ++++++++++++++++++ 2 files changed, 199 insertions(+), 1 deletion(-) diff --git a/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs index 540f0a1bc6fd..b0cfb7cd2534 100644 --- a/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs @@ -50,6 +50,43 @@ static void ValidateContentType(string type) } } + /// + /// Initializes an instance of . + /// + /// The HTTP response status code. + /// The of object that is going to be written in the response. + /// The descrption of the response. + /// Content types supported by the response. + public ProducesResponseTypeMetadata(int statusCode, Type? type = null, string? description = null, string[]? contentTypes = null) + { + StatusCode = statusCode; + Type = type; + Description = description; + + if (contentTypes is null || contentTypes.Length == 0) + { + ContentTypes = Enumerable.Empty(); + } + else + { + for (var i = 0; i < contentTypes.Length; i++) + { + MediaTypeHeaderValue.Parse(contentTypes[i]); + ValidateContentType(contentTypes[i]); + } + + ContentTypes = 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."); + } + } + } + // Only for internal use where validation is unnecessary. private ProducesResponseTypeMetadata(int statusCode, Type? type, IEnumerable contentTypes) { @@ -58,6 +95,15 @@ private ProducesResponseTypeMetadata(int statusCode, Type? type, IEnumerable contentTypes) + { + Type = type; + StatusCode = statusCode; + Description = description; + ContentTypes = contentTypes; + } + /// /// Gets or sets the type of the value returned by an action. /// @@ -81,8 +127,9 @@ private ProducesResponseTypeMetadata(int statusCode, Type? type, IEnumerable public override string ToString() { - return DebuggerHelpers.GetDebugText(nameof(StatusCode), StatusCode, nameof(ContentTypes), ContentTypes, nameof(Type), Type, includeNullValues: false, prefix: "Produces"); + return DebuggerHelpers.GetDebugText(nameof(StatusCode), StatusCode, nameof(ContentTypes), ContentTypes, nameof(Type), Type, nameof(Description), Description, includeNullValues: false, prefix: "Produces"); } internal static ProducesResponseTypeMetadata CreateUnvalidated(Type? type, int statusCode, IEnumerable contentTypes) => new(statusCode, type, contentTypes); + internal static ProducesResponseTypeMetadata CreateUnvalidated(Type? type, int statusCode, string? description, IEnumerable contentTypes) => new(statusCode, type, description, contentTypes); } diff --git a/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs b/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs index cf6faa2267f9..49cdbf94dd92 100644 --- a/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs @@ -55,6 +55,29 @@ public static RouteHandlerBuilder Produces( return Produces(builder, statusCode, typeof(TResponse), contentType, additionalContentTypes); } + /// + /// Adds an to for all endpoints + /// produced by . + /// + /// The type of the response. + /// The . + /// The response status code. Defaults to . + /// The response content type. Defaults to "application/json". + /// The description of the response. 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 RouteHandlerBuilder Produces( +#pragma warning restore RS0026 + this RouteHandlerBuilder builder, + int statusCode = StatusCodes.Status200OK, + string? contentType = null, + string? description = null, + params string[] additionalContentTypes) + { + return Produces(builder, statusCode, typeof(TResponse), contentType, description, additionalContentTypes); + } + /// /// Adds an to for all endpoints /// produced by . @@ -91,6 +114,44 @@ public static RouteHandlerBuilder Produces( return builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode, responseType ?? typeof(void), contentTypes)); } + /// + /// Adds an to for all endpoints + /// produced by . + /// + /// The . + /// The response status code. + /// The type of the response. Defaults to null. + /// The description 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 RouteHandlerBuilder Produces( +#pragma warning restore RS0026 + this RouteHandlerBuilder builder, + int statusCode, + Type? responseType = null, + string? description = null, + string? contentType = null, + params string[] additionalContentTypes) + { + if (responseType is Type && string.IsNullOrEmpty(contentType)) + { + contentType = ContentTypeConstants.JsonContentType; + } + + if (contentType is null) + { + return builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode, responseType ?? typeof(void), description)); + } + + var contentTypes = new string[additionalContentTypes.Length + 1]; + contentTypes[0] = contentType; + additionalContentTypes.CopyTo(contentTypes, 1); + + return builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode, responseType ?? typeof(void), description, contentTypes)); + } + /// /// Adds an with a type /// to for all endpoints produced by . @@ -109,6 +170,25 @@ public static RouteHandlerBuilder ProducesProblem(this RouteHandlerBuilder build return Produces(builder, statusCode, typeof(ProblemDetails), contentType); } + /// + /// Adds an with a type + /// to for all endpoints produced by . + /// + /// The . + /// The response status code. + /// The description of the response. Defaults to null. + /// The response content type. Defaults to "application/problem+json". + /// A that can be used to further customize the endpoint. + public static RouteHandlerBuilder ProducesProblem(this RouteHandlerBuilder builder, int statusCode, string? description = null, string? contentType = null) + { + if (string.IsNullOrEmpty(contentType)) + { + contentType = ContentTypeConstants.ProblemDetailsContentType; + } + + return Produces(builder, statusCode, typeof(ProblemDetails), description, contentType); + } + /// /// Adds an with a type /// to for all endpoints produced by . @@ -130,6 +210,28 @@ public static TBuilder ProducesProblem(this TBuilder builder, int stat return builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode, typeof(ProblemDetails), [contentType])); } + /// + /// Adds an with a type + /// to for all endpoints produced by . + /// + /// The . + /// The response status code. + /// The description of the response. Defaults to null. + /// The response content type. Defaults to "application/problem+json". + /// A that can be used to further customize the endpoint. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static TBuilder ProducesProblem(this TBuilder builder, int statusCode, string? description = null, string? contentType = null) +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + where TBuilder : IEndpointConventionBuilder + { + if (string.IsNullOrEmpty(contentType)) + { + contentType = ContentTypeConstants.ProblemDetailsContentType; + } + + return builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode, typeof(ProblemDetails), description, [contentType])); + } + /// /// Adds an with a type /// to for all endpoints produced by . @@ -151,6 +253,29 @@ public static RouteHandlerBuilder ProducesValidationProblem( return Produces(builder, statusCode, typeof(HttpValidationProblemDetails), contentType); } + /// + /// Adds an with a type + /// to for all endpoints produced by . + /// + /// The . + /// The response status code. Defaults to . + /// The description of the response. Defaults to null. + /// The response content type. Defaults to "application/problem+json". + /// A that can be used to further customize the endpoint. + public static RouteHandlerBuilder ProducesValidationProblem( + this RouteHandlerBuilder builder, + int statusCode = StatusCodes.Status400BadRequest, + string? description = null, + string? contentType = null) + { + if (string.IsNullOrEmpty(contentType)) + { + contentType = ContentTypeConstants.ProblemDetailsContentType; + } + + return Produces(builder, statusCode, typeof(HttpValidationProblemDetails), description, contentType); + } + /// /// Adds an with a type /// to for all endpoints produced by . @@ -175,6 +300,32 @@ public static TBuilder ProducesValidationProblem( return builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode, typeof(HttpValidationProblemDetails), [contentType])); } + /// + /// Adds an with a type + /// to for all endpoints produced by . + /// + /// The . + /// The response status code. Defaults to . + /// The description of the response. Defaults to null. + /// The response content type. Defaults to "application/problem+json". + /// A that can be used to further customize the endpoint. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static TBuilder ProducesValidationProblem( +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + this TBuilder builder, + int statusCode = StatusCodes.Status400BadRequest, + string? description = null, + string? contentType = null) + where TBuilder : IEndpointConventionBuilder + { + if (string.IsNullOrEmpty(contentType)) + { + contentType = ContentTypeConstants.ProblemDetailsContentType; + } + + return builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode, typeof(HttpValidationProblemDetails), description, [contentType])); + } + /// /// Adds the to for all endpoints /// produced by . From f7416da49f6bf719f0e68cbf28b3927609ec8a23 Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Fri, 16 Aug 2024 16:19:26 +0200 Subject: [PATCH 08/18] Changed code to follow overload rules and fix compile issues --- .../Metadata/ProducesResponseTypeMetadata.cs | 51 +++---------------- .../src/PublicAPI.Unshipped.txt | 2 + .../src/RequestDelegateFactory.cs | 8 +-- .../Http.Results/src/AcceptedAtRouteOfT.cs | 2 +- src/Http/Http.Results/src/AcceptedOfT.cs | 2 +- src/Http/Http.Results/src/BadRequestOfT.cs | 2 +- src/Http/Http.Results/src/ConflictOfT.cs | 2 +- .../Http.Results/src/CreatedAtRouteOfT.cs | 2 +- src/Http/Http.Results/src/CreatedOfT.cs | 2 +- .../src/InternalServerErrorOfT.cs | 2 +- src/Http/Http.Results/src/NotFoundOfT.cs | 2 +- src/Http/Http.Results/src/OkOfT.cs | 2 +- .../src/UnprocessableEntityOfT.cs | 2 +- .../Http.Results/src/ValidationProblem.cs | 2 +- 14 files changed, 25 insertions(+), 58 deletions(-) diff --git a/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs index b0cfb7cd2534..1fb0791aa72d 100644 --- a/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs @@ -21,10 +21,12 @@ public sealed class ProducesResponseTypeMetadata : IProducesResponseTypeMetadata /// The HTTP response status code. /// The of object that is going to be written in the response. /// Content types supported by the response. - public ProducesResponseTypeMetadata(int statusCode, Type? type = null, string[]? contentTypes = null) + /// The description of the response. + public ProducesResponseTypeMetadata(int statusCode, Type? type = null, string[]? contentTypes = null, string? description = null) { StatusCode = statusCode; Type = type; + Description = description; if (contentTypes is null || contentTypes.Length == 0) { @@ -50,58 +52,22 @@ static void ValidateContentType(string type) } } + // 9.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH /// /// Initializes an instance of . /// /// The HTTP response status code. /// The of object that is going to be written in the response. - /// The descrption of the response. /// Content types supported by the response. - public ProducesResponseTypeMetadata(int statusCode, Type? type = null, string? description = null, string[]? contentTypes = null) - { - StatusCode = statusCode; - Type = type; - Description = description; - - if (contentTypes is null || contentTypes.Length == 0) - { - ContentTypes = Enumerable.Empty(); - } - else - { - for (var i = 0; i < contentTypes.Length; i++) - { - MediaTypeHeaderValue.Parse(contentTypes[i]); - ValidateContentType(contentTypes[i]); - } - - ContentTypes = 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."); - } - } - } + public ProducesResponseTypeMetadata(int statusCode, Type? type = null, string[]? contentTypes = null) : this(statusCode, type, contentTypes, description: null) { } // Only for internal use where validation is unnecessary. - private ProducesResponseTypeMetadata(int statusCode, Type? type, IEnumerable contentTypes) + private ProducesResponseTypeMetadata(int statusCode, Type? type, IEnumerable contentTypes, string? description = null) { Type = type; StatusCode = statusCode; ContentTypes = contentTypes; - } - - // Only for internal use where validation is unnecessary. - private ProducesResponseTypeMetadata(int statusCode, Type? type, string? description, IEnumerable contentTypes) - { - Type = type; - StatusCode = statusCode; Description = description; - ContentTypes = contentTypes; } /// @@ -127,9 +93,8 @@ private ProducesResponseTypeMetadata(int statusCode, Type? type, string? descrip /// public override string ToString() { - return DebuggerHelpers.GetDebugText(nameof(StatusCode), StatusCode, nameof(ContentTypes), ContentTypes, nameof(Type), Type, nameof(Description), Description, includeNullValues: false, prefix: "Produces"); + return DebuggerHelpers.GetDebugText(nameof(StatusCode), StatusCode, nameof(ContentTypes), ContentTypes, nameof(Type), Type, includeNullValues: false, prefix: "Produces"); } - internal static ProducesResponseTypeMetadata CreateUnvalidated(Type? type, int statusCode, IEnumerable contentTypes) => new(statusCode, type, contentTypes); - internal static ProducesResponseTypeMetadata CreateUnvalidated(Type? type, int statusCode, string? description, IEnumerable contentTypes) => new(statusCode, type, description, contentTypes); + internal static ProducesResponseTypeMetadata CreateUnvalidated(Type? type, int statusCode, IEnumerable contentTypes, string? description) => new(statusCode, type, contentTypes, description); } diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 6940023e6978..7bb919f91531 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -13,3 +13,5 @@ Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata.Name.get -> string! Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata.ParameterInfo.get -> System.Reflection.ParameterInfo! Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.Description.get -> string? Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata.Description.get -> string? +*REMOVED*Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.ProducesResponseTypeMetadata(int statusCode, System.Type? type = null, string![]? contentTypes = null) -> void +Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.ProducesResponseTypeMetadata(int statusCode, System.Type? type = null, string![]? contentTypes = null, string? description = null) -> void diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index f30dbe63ca10..16975e82bb34 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -274,7 +274,7 @@ private static RequestDelegateFactoryContext CreateFactoryContext(RequestDelegat var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices ?? EmptyServiceProvider.Instance; var endpointBuilder = options?.EndpointBuilder ?? new RdfEndpointBuilder(serviceProvider); var jsonSerializerOptions = serviceProvider.GetService>()?.Value.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; - var formDataMapperOptions = new FormDataMapperOptions();; + var formDataMapperOptions = new FormDataMapperOptions(); ; var factoryContext = new RequestDelegateFactoryContext { @@ -1039,15 +1039,15 @@ private static void PopulateBuiltInResponseTypeMetadata(Type returnType, Endpoin if (returnType == typeof(string)) { - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(type: typeof(string), statusCode: 200, PlaintextContentType)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(type: typeof(string), statusCode: 200, PlaintextContentType, description: null)); } else if (returnType == typeof(void)) { - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(returnType, statusCode: 200, PlaintextContentType)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(returnType, statusCode: 200, PlaintextContentType), description: null); } else { - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(returnType, statusCode: 200, DefaultAcceptsAndProducesContentType)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(returnType, statusCode: 200, DefaultAcceptsAndProducesContentType), description: null); } } diff --git a/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs b/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs index e9467dbe1e54..4030c9071c9a 100644 --- a/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs +++ b/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs @@ -122,6 +122,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status202Accepted, ContentTypeConstants.ApplicationJsonContentTypes)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status202Accepted, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); } } diff --git a/src/Http/Http.Results/src/AcceptedOfT.cs b/src/Http/Http.Results/src/AcceptedOfT.cs index 6777d3549d58..2b4d40f32975 100644 --- a/src/Http/Http.Results/src/AcceptedOfT.cs +++ b/src/Http/Http.Results/src/AcceptedOfT.cs @@ -100,6 +100,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status202Accepted, ContentTypeConstants.ApplicationJsonContentTypes)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status202Accepted, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); } } diff --git a/src/Http/Http.Results/src/BadRequestOfT.cs b/src/Http/Http.Results/src/BadRequestOfT.cs index 4b932786eb3b..e15ca45e7402 100644 --- a/src/Http/Http.Results/src/BadRequestOfT.cs +++ b/src/Http/Http.Results/src/BadRequestOfT.cs @@ -65,6 +65,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status400BadRequest, ContentTypeConstants.ApplicationJsonContentTypes)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status400BadRequest, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); } } diff --git a/src/Http/Http.Results/src/ConflictOfT.cs b/src/Http/Http.Results/src/ConflictOfT.cs index bbfb3c6a503e..df892f898f35 100644 --- a/src/Http/Http.Results/src/ConflictOfT.cs +++ b/src/Http/Http.Results/src/ConflictOfT.cs @@ -65,6 +65,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status409Conflict, ContentTypeConstants.ApplicationJsonContentTypes)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status409Conflict, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); } } diff --git a/src/Http/Http.Results/src/CreatedAtRouteOfT.cs b/src/Http/Http.Results/src/CreatedAtRouteOfT.cs index d13d6154413b..bd64e5b5a745 100644 --- a/src/Http/Http.Results/src/CreatedAtRouteOfT.cs +++ b/src/Http/Http.Results/src/CreatedAtRouteOfT.cs @@ -125,6 +125,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status201Created, ContentTypeConstants.ApplicationJsonContentTypes)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status201Created, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); } } diff --git a/src/Http/Http.Results/src/CreatedOfT.cs b/src/Http/Http.Results/src/CreatedOfT.cs index 58d0638b218d..3f434c18384f 100644 --- a/src/Http/Http.Results/src/CreatedOfT.cs +++ b/src/Http/Http.Results/src/CreatedOfT.cs @@ -99,6 +99,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status201Created, ContentTypeConstants.ApplicationJsonContentTypes)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status201Created, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); } } diff --git a/src/Http/Http.Results/src/InternalServerErrorOfT.cs b/src/Http/Http.Results/src/InternalServerErrorOfT.cs index 36ec9fed9512..d4bbb742a601 100644 --- a/src/Http/Http.Results/src/InternalServerErrorOfT.cs +++ b/src/Http/Http.Results/src/InternalServerErrorOfT.cs @@ -65,6 +65,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status500InternalServerError, ContentTypeConstants.ApplicationJsonContentTypes)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status500InternalServerError, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); } } diff --git a/src/Http/Http.Results/src/NotFoundOfT.cs b/src/Http/Http.Results/src/NotFoundOfT.cs index ad9c3a1032eb..541282d0bb16 100644 --- a/src/Http/Http.Results/src/NotFoundOfT.cs +++ b/src/Http/Http.Results/src/NotFoundOfT.cs @@ -64,6 +64,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status404NotFound, ContentTypeConstants.ApplicationJsonContentTypes)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status404NotFound, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); } } diff --git a/src/Http/Http.Results/src/OkOfT.cs b/src/Http/Http.Results/src/OkOfT.cs index 108674fdc2ac..fda5f1ae2d47 100644 --- a/src/Http/Http.Results/src/OkOfT.cs +++ b/src/Http/Http.Results/src/OkOfT.cs @@ -64,6 +64,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status200OK, ContentTypeConstants.ApplicationJsonContentTypes)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status200OK, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); } } diff --git a/src/Http/Http.Results/src/UnprocessableEntityOfT.cs b/src/Http/Http.Results/src/UnprocessableEntityOfT.cs index 475a0bc3ad2c..82fb8d5da12b 100644 --- a/src/Http/Http.Results/src/UnprocessableEntityOfT.cs +++ b/src/Http/Http.Results/src/UnprocessableEntityOfT.cs @@ -65,6 +65,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status422UnprocessableEntity, ContentTypeConstants.ApplicationJsonContentTypes)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status422UnprocessableEntity, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); } } diff --git a/src/Http/Http.Results/src/ValidationProblem.cs b/src/Http/Http.Results/src/ValidationProblem.cs index ba738cfbcf3e..f7dd403132be 100644 --- a/src/Http/Http.Results/src/ValidationProblem.cs +++ b/src/Http/Http.Results/src/ValidationProblem.cs @@ -76,6 +76,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(HttpValidationProblemDetails), StatusCodes.Status400BadRequest, ContentTypeConstants.ProblemDetailsContentTypes)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(HttpValidationProblemDetails), StatusCodes.Status400BadRequest, ContentTypeConstants.ProblemDetailsContentTypes, description: null)); } } From 443babdf1e3a533e6fb059421f07f48b684f073e Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Mon, 16 Sep 2024 14:55:13 +0200 Subject: [PATCH 09/18] Fix minor typo --- src/Http/Http.Extensions/src/RequestDelegateFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 16975e82bb34..2366477142d6 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -274,7 +274,7 @@ private static RequestDelegateFactoryContext CreateFactoryContext(RequestDelegat var serviceProvider = options?.ServiceProvider ?? options?.EndpointBuilder?.ApplicationServices ?? EmptyServiceProvider.Instance; var endpointBuilder = options?.EndpointBuilder ?? new RdfEndpointBuilder(serviceProvider); var jsonSerializerOptions = serviceProvider.GetService>()?.Value.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; - var formDataMapperOptions = new FormDataMapperOptions(); ; + var formDataMapperOptions = new FormDataMapperOptions(); var factoryContext = new RequestDelegateFactoryContext { From 7700ccc5d849cd20c0cbdfe31ba6d0d4b5269a9e Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Mon, 16 Sep 2024 14:59:41 +0200 Subject: [PATCH 10/18] =?UTF-8?q?Remove=20code=20from=20OpenApiRouteHandle?= =?UTF-8?q?rBUilderExtensions=20based=20on=20Safia=C5=9B=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also removed new constructors based on Safiaś comment --- .../Metadata/IProducesResponseTypeMetadata.cs | 2 +- .../Metadata/ProducesResponseTypeMetadata.cs | 20 +-- .../src/PublicAPI.Shipped.txt | 1 - .../src/PublicAPI.Unshipped.txt | 5 +- .../src/RequestDelegateFactory.cs | 6 +- .../Http.Results/src/AcceptedAtRouteOfT.cs | 2 +- src/Http/Http.Results/src/AcceptedOfT.cs | 2 +- src/Http/Http.Results/src/BadRequestOfT.cs | 2 +- src/Http/Http.Results/src/ConflictOfT.cs | 2 +- .../Http.Results/src/CreatedAtRouteOfT.cs | 2 +- src/Http/Http.Results/src/CreatedOfT.cs | 2 +- .../src/InternalServerErrorOfT.cs | 2 +- src/Http/Http.Results/src/NotFoundOfT.cs | 2 +- src/Http/Http.Results/src/OkOfT.cs | 2 +- .../src/UnprocessableEntityOfT.cs | 2 +- .../Http.Results/src/ValidationProblem.cs | 2 +- .../OpenApiRouteHandlerBuilderExtensions.cs | 151 ------------------ 17 files changed, 22 insertions(+), 185 deletions(-) diff --git a/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs index 4f832cfea935..9b1b21ab9dff 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs @@ -21,7 +21,7 @@ public interface IProducesResponseTypeMetadata /// /// Gets the description of the response. /// - string? Description { get; } + string? Description { get; set; } /// /// Gets the content types supported by the metadata. diff --git a/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs index 1fb0791aa72d..2186b76be610 100644 --- a/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/ProducesResponseTypeMetadata.cs @@ -21,12 +21,10 @@ public sealed class ProducesResponseTypeMetadata : IProducesResponseTypeMetadata /// The HTTP response status code. /// The of object that is going to be written in the response. /// Content types supported by the response. - /// The description of the response. - public ProducesResponseTypeMetadata(int statusCode, Type? type = null, string[]? contentTypes = null, string? description = null) + public ProducesResponseTypeMetadata(int statusCode, Type? type = null, string[]? contentTypes = null) { StatusCode = statusCode; Type = type; - Description = description; if (contentTypes is null || contentTypes.Length == 0) { @@ -52,22 +50,12 @@ static void ValidateContentType(string type) } } - // 9.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH - /// - /// Initializes an instance of . - /// - /// The HTTP response status code. - /// The of object that is going to be written in the response. - /// Content types supported by the response. - public ProducesResponseTypeMetadata(int statusCode, Type? type = null, string[]? contentTypes = null) : this(statusCode, type, contentTypes, description: null) { } - // Only for internal use where validation is unnecessary. - private ProducesResponseTypeMetadata(int statusCode, Type? type, IEnumerable contentTypes, string? description = null) + private ProducesResponseTypeMetadata(int statusCode, Type? type, IEnumerable contentTypes) { Type = type; StatusCode = statusCode; ContentTypes = contentTypes; - Description = description; } /// @@ -83,7 +71,7 @@ private ProducesResponseTypeMetadata(int statusCode, Type? type, IEnumerable /// Gets or sets the description of the response. /// - public string? Description { get; private set; } + public string? Description { get; set; } /// /// Gets or sets the content types associated with the response. @@ -96,5 +84,5 @@ public override string ToString() return DebuggerHelpers.GetDebugText(nameof(StatusCode), StatusCode, nameof(ContentTypes), ContentTypes, nameof(Type), Type, includeNullValues: false, prefix: "Produces"); } - internal static ProducesResponseTypeMetadata CreateUnvalidated(Type? type, int statusCode, IEnumerable contentTypes, string? description) => new(statusCode, type, contentTypes, description); + internal static ProducesResponseTypeMetadata CreateUnvalidated(Type? type, int statusCode, IEnumerable contentTypes) => new(statusCode, type, contentTypes); } diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Shipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Shipped.txt index d6c04a9a103f..cf46eeb2f6c2 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Shipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Shipped.txt @@ -413,7 +413,6 @@ Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetails.set -> void Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetailsContext() -> void Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.ContentTypes.get -> System.Collections.Generic.IEnumerable! -Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.ProducesResponseTypeMetadata(int statusCode, System.Type? type = null, string![]? contentTypes = null) -> void Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.StatusCode.get -> int Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.Type.get -> System.Type? Microsoft.AspNetCore.Http.QueryString diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 7bb919f91531..d2d60a30c0cf 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -12,6 +12,7 @@ Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata.IsOptional.get -> b Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata.Name.get -> string! Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata.ParameterInfo.get -> System.Reflection.ParameterInfo! Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.Description.get -> string? +Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.Description.set -> void Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata.Description.get -> string? -*REMOVED*Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.ProducesResponseTypeMetadata(int statusCode, System.Type? type = null, string![]? contentTypes = null) -> void -Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.ProducesResponseTypeMetadata(int statusCode, System.Type? type = null, string![]? contentTypes = null, string? description = null) -> void +Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata.Description.set -> void +Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.ProducesResponseTypeMetadata(int statusCode, System.Type? type = null, string![]? contentTypes = null) -> void diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 2366477142d6..0f3369da5b1b 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -1039,15 +1039,15 @@ private static void PopulateBuiltInResponseTypeMetadata(Type returnType, Endpoin if (returnType == typeof(string)) { - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(type: typeof(string), statusCode: 200, PlaintextContentType, description: null)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(type: typeof(string), statusCode: 200, PlaintextContentType)); } else if (returnType == typeof(void)) { - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(returnType, statusCode: 200, PlaintextContentType), description: null); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(returnType, statusCode: 200, PlaintextContentType)); } else { - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(returnType, statusCode: 200, DefaultAcceptsAndProducesContentType), description: null); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(returnType, statusCode: 200, DefaultAcceptsAndProducesContentType)); } } diff --git a/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs b/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs index 4030c9071c9a..e9467dbe1e54 100644 --- a/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs +++ b/src/Http/Http.Results/src/AcceptedAtRouteOfT.cs @@ -122,6 +122,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status202Accepted, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status202Accepted, ContentTypeConstants.ApplicationJsonContentTypes)); } } diff --git a/src/Http/Http.Results/src/AcceptedOfT.cs b/src/Http/Http.Results/src/AcceptedOfT.cs index 2b4d40f32975..6777d3549d58 100644 --- a/src/Http/Http.Results/src/AcceptedOfT.cs +++ b/src/Http/Http.Results/src/AcceptedOfT.cs @@ -100,6 +100,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status202Accepted, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status202Accepted, ContentTypeConstants.ApplicationJsonContentTypes)); } } diff --git a/src/Http/Http.Results/src/BadRequestOfT.cs b/src/Http/Http.Results/src/BadRequestOfT.cs index e15ca45e7402..4b932786eb3b 100644 --- a/src/Http/Http.Results/src/BadRequestOfT.cs +++ b/src/Http/Http.Results/src/BadRequestOfT.cs @@ -65,6 +65,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status400BadRequest, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status400BadRequest, ContentTypeConstants.ApplicationJsonContentTypes)); } } diff --git a/src/Http/Http.Results/src/ConflictOfT.cs b/src/Http/Http.Results/src/ConflictOfT.cs index df892f898f35..bbfb3c6a503e 100644 --- a/src/Http/Http.Results/src/ConflictOfT.cs +++ b/src/Http/Http.Results/src/ConflictOfT.cs @@ -65,6 +65,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status409Conflict, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status409Conflict, ContentTypeConstants.ApplicationJsonContentTypes)); } } diff --git a/src/Http/Http.Results/src/CreatedAtRouteOfT.cs b/src/Http/Http.Results/src/CreatedAtRouteOfT.cs index bd64e5b5a745..d13d6154413b 100644 --- a/src/Http/Http.Results/src/CreatedAtRouteOfT.cs +++ b/src/Http/Http.Results/src/CreatedAtRouteOfT.cs @@ -125,6 +125,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status201Created, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status201Created, ContentTypeConstants.ApplicationJsonContentTypes)); } } diff --git a/src/Http/Http.Results/src/CreatedOfT.cs b/src/Http/Http.Results/src/CreatedOfT.cs index 3f434c18384f..58d0638b218d 100644 --- a/src/Http/Http.Results/src/CreatedOfT.cs +++ b/src/Http/Http.Results/src/CreatedOfT.cs @@ -99,6 +99,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status201Created, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status201Created, ContentTypeConstants.ApplicationJsonContentTypes)); } } diff --git a/src/Http/Http.Results/src/InternalServerErrorOfT.cs b/src/Http/Http.Results/src/InternalServerErrorOfT.cs index d4bbb742a601..36ec9fed9512 100644 --- a/src/Http/Http.Results/src/InternalServerErrorOfT.cs +++ b/src/Http/Http.Results/src/InternalServerErrorOfT.cs @@ -65,6 +65,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status500InternalServerError, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status500InternalServerError, ContentTypeConstants.ApplicationJsonContentTypes)); } } diff --git a/src/Http/Http.Results/src/NotFoundOfT.cs b/src/Http/Http.Results/src/NotFoundOfT.cs index 541282d0bb16..ad9c3a1032eb 100644 --- a/src/Http/Http.Results/src/NotFoundOfT.cs +++ b/src/Http/Http.Results/src/NotFoundOfT.cs @@ -64,6 +64,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status404NotFound, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status404NotFound, ContentTypeConstants.ApplicationJsonContentTypes)); } } diff --git a/src/Http/Http.Results/src/OkOfT.cs b/src/Http/Http.Results/src/OkOfT.cs index fda5f1ae2d47..108674fdc2ac 100644 --- a/src/Http/Http.Results/src/OkOfT.cs +++ b/src/Http/Http.Results/src/OkOfT.cs @@ -64,6 +64,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status200OK, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status200OK, ContentTypeConstants.ApplicationJsonContentTypes)); } } diff --git a/src/Http/Http.Results/src/UnprocessableEntityOfT.cs b/src/Http/Http.Results/src/UnprocessableEntityOfT.cs index 82fb8d5da12b..475a0bc3ad2c 100644 --- a/src/Http/Http.Results/src/UnprocessableEntityOfT.cs +++ b/src/Http/Http.Results/src/UnprocessableEntityOfT.cs @@ -65,6 +65,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status422UnprocessableEntity, ContentTypeConstants.ApplicationJsonContentTypes, description: null)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(TValue), StatusCodes.Status422UnprocessableEntity, ContentTypeConstants.ApplicationJsonContentTypes)); } } diff --git a/src/Http/Http.Results/src/ValidationProblem.cs b/src/Http/Http.Results/src/ValidationProblem.cs index f7dd403132be..ba738cfbcf3e 100644 --- a/src/Http/Http.Results/src/ValidationProblem.cs +++ b/src/Http/Http.Results/src/ValidationProblem.cs @@ -76,6 +76,6 @@ static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, Endpoi ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(builder); - builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(HttpValidationProblemDetails), StatusCodes.Status400BadRequest, ContentTypeConstants.ProblemDetailsContentTypes, description: null)); + builder.Metadata.Add(ProducesResponseTypeMetadata.CreateUnvalidated(typeof(HttpValidationProblemDetails), StatusCodes.Status400BadRequest, ContentTypeConstants.ProblemDetailsContentTypes)); } } diff --git a/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs b/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs index 49cdbf94dd92..cf6faa2267f9 100644 --- a/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs @@ -55,29 +55,6 @@ public static RouteHandlerBuilder Produces( return Produces(builder, statusCode, typeof(TResponse), contentType, additionalContentTypes); } - /// - /// Adds an to for all endpoints - /// produced by . - /// - /// The type of the response. - /// The . - /// The response status code. Defaults to . - /// The response content type. Defaults to "application/json". - /// The description of the response. 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 RouteHandlerBuilder Produces( -#pragma warning restore RS0026 - this RouteHandlerBuilder builder, - int statusCode = StatusCodes.Status200OK, - string? contentType = null, - string? description = null, - params string[] additionalContentTypes) - { - return Produces(builder, statusCode, typeof(TResponse), contentType, description, additionalContentTypes); - } - /// /// Adds an to for all endpoints /// produced by . @@ -114,44 +91,6 @@ public static RouteHandlerBuilder Produces( return builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode, responseType ?? typeof(void), contentTypes)); } - /// - /// Adds an to for all endpoints - /// produced by . - /// - /// The . - /// The response status code. - /// The type of the response. Defaults to null. - /// The description 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 RouteHandlerBuilder Produces( -#pragma warning restore RS0026 - this RouteHandlerBuilder builder, - int statusCode, - Type? responseType = null, - string? description = null, - string? contentType = null, - params string[] additionalContentTypes) - { - if (responseType is Type && string.IsNullOrEmpty(contentType)) - { - contentType = ContentTypeConstants.JsonContentType; - } - - if (contentType is null) - { - return builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode, responseType ?? typeof(void), description)); - } - - var contentTypes = new string[additionalContentTypes.Length + 1]; - contentTypes[0] = contentType; - additionalContentTypes.CopyTo(contentTypes, 1); - - return builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode, responseType ?? typeof(void), description, contentTypes)); - } - /// /// Adds an with a type /// to for all endpoints produced by . @@ -170,25 +109,6 @@ public static RouteHandlerBuilder ProducesProblem(this RouteHandlerBuilder build return Produces(builder, statusCode, typeof(ProblemDetails), contentType); } - /// - /// Adds an with a type - /// to for all endpoints produced by . - /// - /// The . - /// The response status code. - /// The description of the response. Defaults to null. - /// The response content type. Defaults to "application/problem+json". - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder ProducesProblem(this RouteHandlerBuilder builder, int statusCode, string? description = null, string? contentType = null) - { - if (string.IsNullOrEmpty(contentType)) - { - contentType = ContentTypeConstants.ProblemDetailsContentType; - } - - return Produces(builder, statusCode, typeof(ProblemDetails), description, contentType); - } - /// /// Adds an with a type /// to for all endpoints produced by . @@ -210,28 +130,6 @@ public static TBuilder ProducesProblem(this TBuilder builder, int stat return builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode, typeof(ProblemDetails), [contentType])); } - /// - /// Adds an with a type - /// to for all endpoints produced by . - /// - /// The . - /// The response status code. - /// The description of the response. Defaults to null. - /// The response content type. Defaults to "application/problem+json". - /// A that can be used to further customize the endpoint. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters - public static TBuilder ProducesProblem(this TBuilder builder, int statusCode, string? description = null, string? contentType = null) -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters - where TBuilder : IEndpointConventionBuilder - { - if (string.IsNullOrEmpty(contentType)) - { - contentType = ContentTypeConstants.ProblemDetailsContentType; - } - - return builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode, typeof(ProblemDetails), description, [contentType])); - } - /// /// Adds an with a type /// to for all endpoints produced by . @@ -253,29 +151,6 @@ public static RouteHandlerBuilder ProducesValidationProblem( return Produces(builder, statusCode, typeof(HttpValidationProblemDetails), contentType); } - /// - /// Adds an with a type - /// to for all endpoints produced by . - /// - /// The . - /// The response status code. Defaults to . - /// The description of the response. Defaults to null. - /// The response content type. Defaults to "application/problem+json". - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder ProducesValidationProblem( - this RouteHandlerBuilder builder, - int statusCode = StatusCodes.Status400BadRequest, - string? description = null, - string? contentType = null) - { - if (string.IsNullOrEmpty(contentType)) - { - contentType = ContentTypeConstants.ProblemDetailsContentType; - } - - return Produces(builder, statusCode, typeof(HttpValidationProblemDetails), description, contentType); - } - /// /// Adds an with a type /// to for all endpoints produced by . @@ -300,32 +175,6 @@ public static TBuilder ProducesValidationProblem( return builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode, typeof(HttpValidationProblemDetails), [contentType])); } - /// - /// Adds an with a type - /// to for all endpoints produced by . - /// - /// The . - /// The response status code. Defaults to . - /// The description of the response. Defaults to null. - /// The response content type. Defaults to "application/problem+json". - /// A that can be used to further customize the endpoint. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters - public static TBuilder ProducesValidationProblem( -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters - this TBuilder builder, - int statusCode = StatusCodes.Status400BadRequest, - string? description = null, - string? contentType = null) - where TBuilder : IEndpointConventionBuilder - { - if (string.IsNullOrEmpty(contentType)) - { - contentType = ContentTypeConstants.ProblemDetailsContentType; - } - - return builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode, typeof(HttpValidationProblemDetails), description, [contentType])); - } - /// /// Adds the to for all endpoints /// produced by . From 9614111d48d3ee351ef3d99af1a21dfb8e53e142 Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Mon, 16 Sep 2024 15:42:37 +0200 Subject: [PATCH 11/18] Fix incorrect XML Comment --- src/Mvc/Mvc.Core/src/ProducesDefaultResponseTypeAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mvc/Mvc.Core/src/ProducesDefaultResponseTypeAttribute.cs b/src/Mvc/Mvc.Core/src/ProducesDefaultResponseTypeAttribute.cs index 2606a17f5ff8..1ba001648769 100644 --- a/src/Mvc/Mvc.Core/src/ProducesDefaultResponseTypeAttribute.cs +++ b/src/Mvc/Mvc.Core/src/ProducesDefaultResponseTypeAttribute.cs @@ -40,7 +40,7 @@ public ProducesDefaultResponseTypeAttribute(Type type) public int StatusCode { get; } /// - /// Gets the description of the response. + /// Gets or sets the description of the response. /// public string? Description { get; set; } From 365c7d6c7c59355ff1c16208ffa640924aae5008 Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Mon, 16 Sep 2024 16:15:21 +0200 Subject: [PATCH 12/18] Fixed some more Public API issues --- src/Mvc/Mvc.ApiExplorer/src/PublicAPI.Unshipped.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Mvc/Mvc.ApiExplorer/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.ApiExplorer/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..3c89ceb1b24c 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.ApiExplorer/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Mvc.ApiExplorer.ApiResponseType.Description.get -> string? (forwarded, contained in Microsoft.AspNetCore.Mvc.Abstractions) +Microsoft.AspNetCore.Mvc.ApiExplorer.ApiResponseType.Description.set -> void (forwarded, contained in Microsoft.AspNetCore.Mvc.Abstractions) From a384d69cbd7b2375de6c6467976109d7efc9b426 Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Mon, 16 Sep 2024 16:41:12 +0200 Subject: [PATCH 13/18] Add unit test --- .../Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs index 92827d144b77..41e13c229863 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs @@ -1107,7 +1107,8 @@ public void GetApiDescription_PopulatesResponseInformation_WhenSetByFilter(strin var action = CreateActionDescriptor(methodName); var filter = new ContentTypeAttribute("text/*") { - Type = typeof(Order) + Type = typeof(Order), + Description = "Example" }; action.FilterDescriptors = new List @@ -1124,6 +1125,7 @@ public void GetApiDescription_PopulatesResponseInformation_WhenSetByFilter(strin Assert.NotNull(responseTypes.ModelMetadata); Assert.Equal(200, responseTypes.StatusCode); Assert.Equal(typeof(Order), responseTypes.Type); + Assert.Equal("Example", responseTypes.Description); foreach (var responseFormat in responseTypes.ApiResponseFormats) { From c071c356b9a4ec155df6cc1abec2aa817466b440 Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Mon, 16 Sep 2024 16:51:52 +0200 Subject: [PATCH 14/18] Add some more unit tests --- .../Mvc.Core/test/ProducesAttributeTests.cs | 23 +++++++++++++++++++ .../ProducesResponseTypeAttributeTests.cs | 23 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/Mvc/Mvc.Core/test/ProducesAttributeTests.cs b/src/Mvc/Mvc.Core/test/ProducesAttributeTests.cs index d29fa66a9f37..15cbd07e0bc1 100644 --- a/src/Mvc/Mvc.Core/test/ProducesAttributeTests.cs +++ b/src/Mvc/Mvc.Core/test/ProducesAttributeTests.cs @@ -151,6 +151,29 @@ public void ProducesAttribute_WithTypeOnly_DoesNotSetContentTypes() Assert.Empty(producesAttribute.ContentTypes); } + [Fact] + public void ProducesAttribute_SetsDescription() + { + // Arrange + var producesAttribute = new ProducesAttribute(typeof(Person)) + { + Description = "Example" + }; + + // Act and Assert + Assert.Equal("Example", producesAttribute.Description); + } + + [Fact] + public void ProducesAttribute_WithTypeOnly_DoesNotSetDescription() + { + // Arrange + var producesAttribute = new ProducesAttribute(typeof(Person)); + + // Act and Assert + Assert.Null(producesAttribute.Description); + } + private static ResultExecutedContext CreateResultExecutedContext(ResultExecutingContext context) { return new ResultExecutedContext(context, context.Filters, context.Result, context.Controller); diff --git a/src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs b/src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs index 204925a95df2..3cb9e69dd87c 100644 --- a/src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs +++ b/src/Mvc/Mvc.Core/test/ProducesResponseTypeAttributeTests.cs @@ -67,6 +67,29 @@ public void ProducesResponseTypeAttribute_WithTypeOnly_DoesNotSetContentTypes() Assert.Null(producesResponseTypeAttribute.ContentTypes); } + [Fact] + public void ProducesResponseTypeAttribute_SetsDescription() + { + // Arrange + var producesResponseTypeAttribute = new ProducesResponseTypeAttribute(typeof(Person), StatusCodes.Status200OK) + { + Description = "Example" + }; + + // Act and Assert + Assert.Equal("Example", producesResponseTypeAttribute.Description); + } + + [Fact] + public void ProducesResponseTypeAttribute_WithTypeOnly_DoesNotSetDescription() + { + // Arrange + var producesResponseTypeAttribute = new ProducesResponseTypeAttribute(typeof(Person), StatusCodes.Status200OK); + + // Act and Assert + Assert.Null(producesResponseTypeAttribute.Description); + } + private class Person { public int Id { get; set; } From aa366b7f12ce6685d8d06a89cff020a0c28706c6 Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Tue, 17 Sep 2024 09:45:18 +0200 Subject: [PATCH 15/18] Remove unnecessary set from interface --- .../src/Metadata/IProducesResponseTypeMetadata.cs | 2 +- src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs index 9b1b21ab9dff..4f832cfea935 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs @@ -21,7 +21,7 @@ public interface IProducesResponseTypeMetadata /// /// Gets the description of the response. /// - string? Description { get; set; } + string? Description { get; } /// /// Gets the content types supported by the metadata. diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index d2d60a30c0cf..b0cd1eabb9f5 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -14,5 +14,4 @@ Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata.ParameterInfo.get - Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.Description.get -> string? Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.Description.set -> void Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata.Description.get -> string? -Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata.Description.set -> void Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.ProducesResponseTypeMetadata(int statusCode, System.Type? type = null, string![]? contentTypes = null) -> void From 24128f6c347b62114ead6f330e1ca086b7308003 Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Tue, 1 Oct 2024 22:15:33 +0200 Subject: [PATCH 16/18] Remove unnecessary change in public api shipped file --- src/Http/Http.Abstractions/src/PublicAPI.Shipped.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Shipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Shipped.txt index cf46eeb2f6c2..d6c04a9a103f 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Shipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Shipped.txt @@ -413,6 +413,7 @@ Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetails.set -> void Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetailsContext() -> void Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.ContentTypes.get -> System.Collections.Generic.IEnumerable! +Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.ProducesResponseTypeMetadata(int statusCode, System.Type? type = null, string![]? contentTypes = null) -> void Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.StatusCode.get -> int Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.Type.get -> System.Type? Microsoft.AspNetCore.Http.QueryString From bb3e4427b04c8177e4e1aa968366c26e21f9cc7f Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Tue, 1 Oct 2024 22:24:02 +0200 Subject: [PATCH 17/18] Also update unshipped file so the build succeeds! --- src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index b0cd1eabb9f5..ccdd75e4934b 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -14,4 +14,3 @@ Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata.ParameterInfo.get - Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.Description.get -> string? Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.Description.set -> void Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata.Description.get -> string? -Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.ProducesResponseTypeMetadata(int statusCode, System.Type? type = null, string![]? contentTypes = null) -> void From d5dae48b2f598326cb5229eddb31a51e7e7875bd Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Wed, 9 Oct 2024 10:18:15 +0200 Subject: [PATCH 18/18] Apply the new Description in response models --- .../src/Services/OpenApiDocumentService.cs | 3 +- .../OpenApiDocumentServiceTests.Responses.cs | 65 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/OpenApi/src/Services/OpenApiDocumentService.cs b/src/OpenApi/src/Services/OpenApiDocumentService.cs index c594ebb7ac08..3f442e7c5d69 100644 --- a/src/OpenApi/src/Services/OpenApiDocumentService.cs +++ b/src/OpenApi/src/Services/OpenApiDocumentService.cs @@ -356,10 +356,9 @@ private async Task GetResponseAsync( IOpenApiSchemaTransformer[] schemaTransformers, CancellationToken cancellationToken) { - var description = ReasonPhrases.GetReasonPhrase(statusCode); var response = new OpenApiResponse { - Description = description, + Description = apiResponseType.Description ?? ReasonPhrases.GetReasonPhrase(statusCode), Content = new Dictionary() }; diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Responses.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Responses.cs index 9c1d5b3c61ab..2f4e1f2e8e88 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Responses.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Responses.cs @@ -304,4 +304,69 @@ await VerifyOpenApiDocument(builder, document => }); }); } + + [Fact] + public async Task GetOpenApiResponse_UsesDescriptionSetByUser() + { + // Arrange + var builder = CreateBuilder(); + + const string expectedCreatedDescription = "A new todo item was created"; + const string expectedBadRequestDescription = "Validation failed for the request"; + + // Act + builder.MapGet("/api/todos", + [ProducesResponseType(typeof(TimeSpan), StatusCodes.Status201Created, Description = expectedCreatedDescription)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Description = expectedBadRequestDescription)] + () => + { }); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = Assert.Single(document.Paths["/api/todos"].Operations.Values); + Assert.Collection(operation.Responses.OrderBy(r => r.Key), + response => + { + Assert.Equal("201", response.Key); + Assert.Equal(expectedCreatedDescription, response.Value.Description); + }, + response => + { + Assert.Equal("400", response.Key); + Assert.Equal(expectedBadRequestDescription, response.Value.Description); + }); + }); + } + + [Fact] + public async Task GetOpenApiResponse_UsesStatusCodeReasonPhraseWhenExplicitDescriptionIsMissing() + { + // Arrange + var builder = CreateBuilder(); + + // Act + builder.MapGet("/api/todos", + [ProducesResponseType(typeof(TimeSpan), StatusCodes.Status201Created, Description = null)] // Explicitly set to NULL + [ProducesResponseType(StatusCodes.Status400BadRequest)] // Omitted, meaning it should be NULL + () => + { }); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = Assert.Single(document.Paths["/api/todos"].Operations.Values); + Assert.Collection(operation.Responses.OrderBy(r => r.Key), + response => + { + Assert.Equal("201", response.Key); + Assert.Equal("Created", response.Value.Description); + }, + response => + { + Assert.Equal("400", response.Key); + Assert.Equal("Bad Request", response.Value.Description); + }); + }); + } }