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