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..2186b76be610 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.
+ ///
+ public string? Description { get; set; }
+
///
/// Gets or sets the content types associated with the response.
///
diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
index 0d2aab25f97d..ccdd75e4934b 100644
--- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
@@ -11,3 +11,6 @@ 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.ProducesResponseTypeMetadata.Description.set -> void
+Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata.Description.get -> string?
diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs
index 159982900eda..25f2513c7d4e 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
{
diff --git a/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiResponseType.cs b/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiResponseType.cs
index 71fc8ecf46d9..2bfe14ff7b45 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 or sets 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 4877cedefe18..d7e6296af6e9 100644
--- a/src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs
+++ b/src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs
@@ -166,11 +166,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/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)
diff --git a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs
index aa05ff5f8c01..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)
{
@@ -2874,6 +2876,8 @@ public ContentTypeAttribute(string mediaType)
public Type Type { get; set; }
+ public string Description { get; set; }
+
public void SetContentTypes(MediaTypeCollection contentTypes)
{
contentTypes.Clear();
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..1ba001648769 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 or sets 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..f9c55192040e 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 or sets the description of the response.
+ ///
+ public string? Description { get; set; }
+
///
void IApiResponseMetadataProvider.SetContentTypes(MediaTypeCollection contentTypes)
{
diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt
index b4560b45280f..9ad015d5f12f 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!
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; }
diff --git a/src/OpenApi/src/Services/OpenApiDocumentService.cs b/src/OpenApi/src/Services/OpenApiDocumentService.cs
index a9a7ecf6a1a6..c16407212f14 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);
+ });
+ });
+ }
}