Skip to content

Conversation

IeuanWalker
Copy link

@IeuanWalker IeuanWalker commented Sep 23, 2025

Fix response description

  • You've read the Contributor Guide and Code of Conduct.
  • You've included unit or integration tests for your change, where applicable.
  • You've included inline docs for your change, where applicable.
  • There's an open issue for the PR that you are making. If you'd like to propose a new feature or change, please open an issue to discuss the change or find an existing issue.

Summary of the changes (Less than 80 chars)

Description

Previous implementation wasn't looking/ tracking the custom descriptions

Fixes #63766

Updated `ResponseEndpoints` to include a `/custom-description` endpoint that returns a "Hello World" message with custom metadata. Enhanced `OpenApiGenerator` to track and apply custom response descriptions, falling back to defaults when necessary. Fixed project file to ensure proper inclusion of Helix content. Added unit tests in `OpenApiOperationGeneratorTests` to validate the handling of custom and default response descriptions across various scenarios.
This commit introduces new endpoint mappings in `MapResponsesEndpoints.cs` to support custom descriptions using attributes and extension methods. The `OpenApiDocumentService` is updated to check for these custom descriptions in endpoint metadata, ensuring accurate representation in OpenAPI documentation. Additionally, a new test is added to verify the correct usage of custom descriptions in the OpenAPI response.
Removed unnecessary line breaks and adjusted comments in
`MapResponsesEndpoints.cs` and `OpenApiGeneratorTests.cs`.
Streamlined `GetOpenApiOperation` method calls for cleaner
code while maintaining existing functionality and logic.
Introduced two new endpoints: `/responses/custom-description-attribute` and `/responses/custom-description-extension-method`. Each has a `GET` method with a `200` response status, including custom descriptions and content types. The first endpoint returns a plain text response, while the second returns an HTML response, enhancing API documentation.
@Copilot Copilot AI review requested due to automatic review settings September 23, 2025 18:07
@IeuanWalker IeuanWalker requested review from a team and captainsafia as code owners September 23, 2025 18:07
@github-actions github-actions bot added the area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates label Sep 23, 2025
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes the handling of custom response descriptions in OpenAPI generation. Previously, the implementation wasn't properly tracking or using custom descriptions provided through ProducesResponseTypeMetadata.

  • Implements proper tracking and usage of custom descriptions from response metadata
  • Adds fallback logic to use default HTTP reason phrases when custom descriptions are null or empty
  • Adds comprehensive test coverage for various custom description scenarios

Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/OpenApi/src/Services/OpenApiGenerator.cs Adds tracking of custom descriptions from ProducesResponseTypeMetadata and uses them in response generation
src/OpenApi/src/Services/OpenApiDocumentService.cs Adds logic to check for custom descriptions in endpoint metadata when ApiResponseType.Description is null
src/OpenApi/sample/Endpoints/MapResponsesEndpoints.cs Adds sample endpoints demonstrating custom description usage
src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiGeneratorTests.cs Adds comprehensive unit tests for custom description scenarios
src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Responses.cs Adds integration test for custom description metadata
Various snapshot files Updates test snapshots to reflect new custom description endpoints
src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Microsoft.AspNetCore.OpenApi.Tests.csproj Minor formatting improvement to MSBuild item

@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Sep 23, 2025
@IeuanWalker IeuanWalker mentioned this pull request Sep 23, 2025
1 task
@martincostello
Copy link
Member

/cc @sander1095 as you added the Description property originally.

@sander1095
Copy link
Contributor

Hi!

I'm looking into it now, thanks for tagging. Will update this comment when I'm done!

Copy link
Contributor

@sander1095 sander1095 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UPDATE: I just turned off my laptop and realized that I looked at this completely wrong, so I'm back!

This works:

app.MapGet("/endpoint", () =>
{
    return "hi";
})
.WithMetadata(new ProducesResponseTypeAttribute(typeof(string), 200) { Description = "hey" });

But, like @IeuanWalker said in this original issue, this doesn't:

app.MapGet("/endpoint", () =>
{
    return "hi";
})
.WithMetadata(new ProducesResponseTypeMetadata(200, typeof(string)) { Description = "hey" });

I completely misread! I thought that @IeuanWalker was using ProducesResponseType, but he was using ProducesResponseTypeMetadata instead.

The reason: The ProducesResponseTypeMetadata description never gets populated (I believe!) I have even wondered if I should have added Description to that class in the first place.... See my link in the next paragraph...

ASSUMPTION: ProducesResponseTypeMetadata , at least for minimal API's, is used for discovering the metadata of an endpoint, so user specified data is ignored(??). In @IeuanWalker's case, they should have used ProducesResponseType instead, which is set by the user and overrides the metadata. However, I think if we do want to support ProducesResponseTypeMetadata with WithMetadata, changes need to be made to ApiResponseTypeProvider and/or EndpointMetadataApiDescriptionProvider, though this statement needs verification!

I discussed an issue with IProducesResponseTypeMetadata.Description never getting populated in one of my earlier PR's: #60539 (comment). I think this discussion is very relevant to what is happening here. To avoid guesswork and putting people on false trails, I can't currently say more, but I think this is what a team member (like @captainsafia ) should focus on! :D


Please treat my existing comments with care, as some comments (like suggestions/thoughts on code approaches) may now be irrelevant. I would suggest to wait with making changes until a team member has officially reviewed this changeset, too!

I do wonder if a lot of these changed need to be made in OpenApiDocumentService and OpenApiGenerator, vs something like EndpointMetadataApiDescriptionProvider.

Metadata is added by calling WithMetadata, but this is then ignored by existing code. I think it would make more sense that the existing OpenAPI -> ApiExplorer infrastructure would deal with it.

For example, line 414 in OpenApiDocumentService tries to grab the apiResponseType.Description. Shouldn't that have been filled by WithMetadata in the first place?

This is perhaps also the reason why I didn't catch this bug in the first place. When using the "intended" (currently tested) behavior, EndpointMetadataApiDescriptionProvider finds and deals with this description:

app.MapGet("/endpoint", [ProducesResponseType<string>(200, Description = "hello")] () =>
{
    return "Hi!";
});

Why is this not the case for WithMetadata? If I am right, and we can fix this in another place, we can prevent lots of duplicate code here, which is my main concern on this PR (and some extra tests/questions on behavioral differences).

@captainsafia can probably say more about what code should be modified (The current proposed changeset, or something more related to the integration between WithMetadata and EndpointMetadataApiDescriptionProvider).

Let me know if I can help out more!

.OfType<IProducesResponseTypeMetadata>()
.Where(m => m.StatusCode == statusCode)
.LastOrDefault()?.Description;

Copy link
Contributor

@sander1095 sander1095 Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't truly say if this is the right approach or not, but I can say that I think we're creating (even more) duplicate code here.

The reason why your bug exists, is because I skipped over this scenario when adding support for response descriptions in minimal API. (Thanks so much for spotting it, and creating a PR!)

There are a lot of places where the response description gets set. In OpenApiGenerator, in OpenApiDocumentService, and in EndpointMetadataApiDescriptionProvider. Compare that to Controllers, where https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs#L134-L135 is the only place I needed to make (significant) changes to get controllers to play nice with the new property.


In this case, this code here looks quite similar to the code I wrote in these 2 PR's (#60539 and #62695 ), but in the last PR I still added more check to deal with the issue of inferred types.

I am not sure if that code also belongs here, but it does look a bit like a copy, which can be a hazard for the future.

// Capture custom description if provided
if (!string.IsNullOrEmpty(responseMetadata.Description))
{
customDescriptions[statusCode] = responseMetadata.Description;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this will cause issues in cases where there are multiple of the same status codes, with different descriptions (and/or different response bodies). I think this needs test cases to check how that is dealt with. It has to be in line with existing controller and minimal API logic (do we grab the first or last match?)

My code for this in another place deals with this by checking for status, type and description. Perhaps that is relevant here: https://github.com/dotnet/aspnetcore/pull/62695/files

}

[Fact]
public void EmptyCustomDescriptionFallsBackToDefault()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Treating an empty string as null shouldnt be the case according to the existing Response Description support in Minimal API

RC1:

app.MapGet("/endpoint", [ProducesResponseType<string>(200, Description = "")] () =>
{
    return "Hi!";
});
"/endpoint": {
      "get": {
        "tags": [
          "openapi"
        ],
        "responses": {
          "200": {
            "description": "", // not null!
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Controllers:


[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    [HttpGet]
    [ProducesResponseType<string>(200, Description = "")]
    public IActionResult Get()
    {
        return Ok("hi");
    }
}
  "paths": {
    "/WeatherForecast": {
      "get": {
        "tags": [
          "WeatherForecast"
        ],
        "responses": {
          "200": {
            "description": "", // not null
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "string"
                }
              },
              "text/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }

{
// Look for custom description in endpoint metadata
var customDescription = apiDescription.ActionDescriptor.EndpointMetadata?
.OfType<IProducesResponseTypeMetadata>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you'd need to check more types. According to my initial PR, description support has been added to more attributes/classes that do not implement this interface:

#58193

Copy link
Contributor

@sander1095 sander1095 Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preferably, we'd not need this call at all, as I think this is the wrong place for this kind of check. See my overall PR review comment for more info.

@sander1095
Copy link
Contributor

Just a quick add-on:

  • I do believe using WithMetadata to create an instance of this attribute is an oddball, but maybe that's my inexperience (or current usage pattern vision for setting respones descriptions). The "normal" way of doing things would be:
    builder.MapGet("/api/todos",
    [ProducesResponseType(typeof(Todo), StatusCodes,Status200Ok, Description = "A list of todo items")]
    [ProducesResponseType(StatusCodes.Status400BadRequest, Description = "Some bad request description")]
    () =>
    { 
        // Code here
    });
    The ApiExplorer now correctly picks up the ProducesResponseType attributes and adds the descriptions. However, if the team expects that using WithMetadata should cause the same behaviour, then this PR is wholly justified!
  • I think the issue and PR need some more explanations about what causes this bug and how it fixes it (WithMetadata taking a main focus here)

@IeuanWalker
Copy link
Author

IeuanWalker commented Sep 23, 2025

thanks @sander1095, didn't realise when I got into it how many places i would be touching lol. Started adding in unit tests and realised I fixed one way and wasn't working another way, so started changing more code to fix it fully.

Will wait to see where the appropriate place is.


The reason I found it is because I'm in the middle of writing a source generator which will write the minimal api code for you, but let you have similar syntax to FastEndpoints, and the way it's currently implemented doesn't work with the attributes, so I have to use the extension methods

Endpoint class Generated code
image image

@sander1095
Copy link
Contributor

sander1095 commented Sep 23, 2025

@IeuanWalker Please check my updated main comment!

Regardless of this PR, Using ProducesResponseType with Minimal API to set response descriptions is not the recommended approach. That attribute (and the other attributes) are something that originated in controller-land, and the team also added support for them in Minimal API land. However, there are better approaches:

@IeuanWalker
Copy link
Author

IeuanWalker commented Sep 23, 2025

thanks @sander1095, wrote this transformer and it seems to do exactly what i needed now -

public static class OpenApiExtensions
{
	public static RouteHandlerBuilder WithResponse(this RouteHandlerBuilder builder, int statusCode, string description, string? contentType = null)
	{
		builder.WithResponse(null, statusCode, description, contentType);

		return builder;
	}

	public static RouteHandlerBuilder WithResponse<T>(this RouteHandlerBuilder builder, int statusCode, string description, string? contentType = null)
	{
		builder.WithResponse(typeof(T), statusCode, description, contentType);

		return builder;
	}

	static RouteHandlerBuilder WithResponse(this RouteHandlerBuilder builder, Type? type, int statusCode, string description, string? contentType = null)
	{
		if (contentType is null)
		{
			builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode, type ?? typeof(void)));
		}
		else
		{
			builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode, type ?? typeof(void), [contentType]));
		}

		builder.AddOpenApiOperationTransformer((operation, context, _) =>
		{
			operation.Responses[statusCode.ToString()].Description = description;
			return Task.CompletedTask;
		});

		return builder;
	}
}
image

Does seem strange that all this behaviour isnt part of the .produces extension method

@sander1095
Copy link
Contributor

Does seem strange that all this behaviour isnt part of the .produces extension method

I agree! I created
#58724 for this some time ago, but:

  • a new parameter cant be added without breaking source compatibility
  • the team would prefer a more direct openapi API, like using transformers, instead of parameters to these extension methods. The reason is that that is a lot more extensible. We would like description to be added, but others might also want OpenAPI's example. Or other properties. It never ends! ;)

PS: Keep in mind your code will break when the same status code appears multiple times, but with different descriptions. Your dictionary key should be status code + response type to support edge cases.

Please share your project if you haven't done so! Would love to learn.

@IeuanWalker
Copy link
Author

@sander1095, is it even possible to have the same status code appear multiple times, as it's a dictionary and the key is the status code?

if so, what would be the updated code?


Ye will do, im hoping to release it when .NET 10 releases, but got a holiday and work so will see how it goes lol

@sander1095
Copy link
Contributor

if so, what would be the updated code?

You are right, @IeuanWalker , this isn't possible and not a part of the spec either. My mistake.

Any HTTP status code can be used as the property name, but only one property per code, to describe the expected response for that HTTP status code. This field MUST be enclosed in quotation marks (for example, "200") for compatibility between JSON and YAML. To define a range of response codes, this field MAY contain the uppercase wildcard character X. For example, 2XX represents all response codes between 200 and 299. Only the following range definitions are allowed: 1XX, 2XX, 3XX, 4XX, and 5XX. If a response is defined using an explicit code, the explicit code definition takes precedence over the range definition for that code.

Therefore, 1 HTTP status code can have 1 response description. If someone has multiple descriptions for different response body for 1 status code, that isn't legal, and I think ASP.NET core currently just uses the last one defined. My apologies for creating confusion.

Copy link
Contributor

Looks like this PR hasn't been active for some time and the codebase could have been changed in the meantime.
To make sure no conflicting changes have occurred, please rerun validation before merging. You can do this by leaving an /azp run comment here (requires commit rights), or by simply closing and reopening.

@dotnet-policy-service dotnet-policy-service bot added the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Oct 1, 2025
@IeuanWalker
Copy link
Author

IeuanWalker commented Oct 3, 2025

Please share your project if you haven't done so! Would love to learn.

@sander1095, this is the project I was talking about, plan to release it fully the same time as .NET 10
https://github.com/IeuanWalker/MinimalApi.Endpoints

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates community-contribution Indicates that the PR has been added by a community member feature-openapi pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Response description

3 participants