Skip to content

Improving current methods for setting OpenAPI response descriptions with Minimal APIs #58724

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
sander1095 opened this issue Oct 31, 2024 · 2 comments
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc design-proposal This issue represents a design proposal for a different issue, linked in the description feature-openapi

Comments

@sander1095
Copy link
Contributor

Summary

The purpose of this design proposal is to allow developers that use Minimal API's to set OpenAPI response descriptions in a more ergonomic way. I created (and implemented) this for controllers with #55656. Some preparations were already done for this in the PR related to the controller's issue (#58193), and this design proposal aims to finish that work.

Motivation and goals

Being able to set a description for each possible response from an endpoint is very useful as it allows a developer to not only know WHAT responses to expect, but also WHEN to expect them, at least in a broad sense. This can improve error handling in client applications and improve understanding of an API.

My proposal contains quite some info, so I want to start with a usage example of the proposal, which extends the Produces extension method provided by the Microsoft.AspNetCore.OpenApi package with a new parameter for the response description:

app.MapGet("/todos/{id}", async (TodoDb db, int id) => /* Code here */)
  .Produces<Todo>(StatusCodes.Status200OK, "The description of the response", "application/json")
  .Produces(StatusCodes.Status403Forbidden, "Returned when the user isn't authorized to view the requested todo entity")
  .Produces(StatusCodes.Status404NotFound, "Returned when the todo doesn't exist or isn't public yet");

This would also work for ProducesProblem, etc...

I created an issue for the subject of this API proposal already: #57963 . I decided to recreate this issue with an design proposal to get a discussion started more quickly and with more context.

Setting response descriptions with the MVC approach

Before we go in-depth about this API proposal, I want to create some context about what I mean with the controller support from #55656. This feature will only be released with .NET 10, so keep that in mind when reading the following code, which wouldn't compile with .NET 8 or 9.

To set a response description in a controller, a developer could write the following:

Click here to see the code
[HttpGet("{id:int:min(1)}")]
[ProducesResponseType<Talk>(StatusCodes.Status200OK, Description = "Returns the requested talk entity")]
[ProducesResponseType(StatusCodes.Status403Forbidden, Description = "Returned when the user isn't authorized to view the requested talk entity")]
[ProducesResponseType(StatusCodes.Status404NotFound, Description = "Returned when the talk doesn't exist or isn't public yet")]
[ProducesDefaultResponseType(Description = "The response for undocumented or unexpected responses")]
public ActionResult<Talk> GetTalk(int id)
{
    // Code here
}

which could result in the following OpenAPI document (Parts like problem details and other irrelevant code has been removed for brevity):

paths:
  /talks/{id}:
    get:
      operationId: "GetTalk"
      parameters:
        - name: "id"
          in: "path"
          required: true
          schema:
            type: "integer"
      responses:
        '200':
          description: "Returns the requested talk entity"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Talk"
        '403':
          description: "Returned when the user isn't authorized to view the requested talk entity"
        '404':
          description: "Returned when the talk doesn't exist or isn't public yet"
        'default':
          description: "The response for undocumented or unexpected responses"

In all the projects I've seen and used, response descriptions aren't used often in ASP.NET Core projects. I believe this to be case because setting them used to be quite verbose, needing XML comments or OpenAPI transformers from Swashbuckle/NSwag.

With .NET 10 releasing in November 2025, this is no longer the case with controllers as this is now much easier to do. However, I believe that the support for response descriptions with Minimal API's isn't as smooth.

The current problem of setting response descriptions with Minimal API's

As mentioned before, this design proposal aims to improve the current ways of setting response descriptions. It is already possible, but I am not satisfied with the current options:

Using transformers

The documentation mentions that descriptions can be set using Document Transformers or Operation Transformers.

Let's take an Operation Transformer as an example. From the docs:

builder.Services.AddOpenApi(options =>
{
    options.AddOperationTransformer((operation, context, cancellationToken) =>
    {
        operation.Responses.Add("500", new OpenApiResponse { Description = "Internal server error" });
        return Task.CompletedTask;
    });
});

This would do the job. However:

  1. I believe that setting the description is quite important and we should reduce the amount of work required to set it. Therefore, I see this approach as too much work, or even too low-level, just to be able to set a description for an OpenAPI response.
  2. This code lives quite "far away" from the actual endpoint, leading to potential drift between the actual endpoint and the description in the OpenAPI document if they are not updated together.

Using ProducesResponseType attribute (.NET 10+)

Based on the docs, this functionality could already be implemented using the ProducesResponseType family of attributes, which I've implemented Description support for (#55656) and will be released in .NET 10.

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
});

However, I do not like this approach in the context of minimal API's:

  1. It's not very readable, especially when there would be (more) arguments or attributes for these arguments.
    1. I see Minimal API's as a way to create API's with less boilerplate code than the MVC approach. Using attributes for this instead of minimal-api-specific extension methods feels like it doesn't match that philosophy.
  2. It involves the Mvc namespace, which might not be desired when using the Minimal API approach
  3. It clashes with the OpenAPI extensions methods provided by the ASP.NET Core Minimal API package, which, in my opinion, are the preferred way to enrich endpoints with OpenAPI information when compared to attributes.

In scope

Enrich the Produces() family of extension methods provided by the Microsoft.AspNetCore.OpenApi package with a new parameter for the response description. This would allow developers to set response descriptions in a more ergonomic way.

Out of scope

Nothing comes to mind right now.

Risks / unknowns

One risk is that adding a Description parameter to the Produces() family of extension methods would clash with the current contentType and additionalContentTypes parameters:

public static Microsoft.AspNetCore.Builder.RouteHandlerBuilder Produces (this Microsoft.AspNetCore.Builder.RouteHandlerBuilder builder, int statusCode, Type? responseType = default, string? contentType = default, params string[] additionalContentTypes);

public static Microsoft.AspNetCore.Builder.RouteHandlerBuilder Produces<TResponse> (this Microsoft.AspNetCore.Builder.RouteHandlerBuilder builder, int statusCode = 200, string? contentType = default, params string[] additionalContentTypes);

// And others...

I'd like to add Description AFTER statusCode and/or responseType, but this is currently already taken by contentType, which is also a string, so this would be a source incompatible change.

I do think this is worth it because of the of the following reasons:

  • The current ways of setting response descriptions are not very ergonomic
  • The source compatibility problem is caused by contentType and additionalContentTypes overloads. I don't have exact numbers on the following claim, but I'd like to think that developers that use Minimal API's are more likely to use JSON, which means that the contentType argument wouldn't be used because the default is application/json anyway.

So, perhaps this proposal should start with a discussion about this source compatibility problem and what we want (and don't want) to do to get this response description feature implemented for Minimal API's.

Usage Examples

I believe that extending the OpenAPI extension methods like Produces() provided by the Microsoft.AspNetCore.OpenApi package are the way to go. They are already used to enrich endpoints with OpenAPI information, and I believe that they should be used to set response descriptions as well:

app.MapGet("/todos/{id}", async (TodoDb db, int id) => /* Code here */)
  .Produces<Todo>(StatusCodes.Status200OK, "The description of the response", "application/json") // <-- This introduces a source incompatible change as the description (string) is added as the second argument, which is currently contentType (string).
  .Produces(StatusCodes.Status403Forbidden, "Returned when the user isn't authorized to view the requested todo entity")
  .Produces(StatusCodes.Status404NotFound, "Returned when the todo doesn't exist or isn't public yet");
  • The description would live next to the endpoint definition
  • It's way less verbose than attributes or operation transformers.

As mentioned in the code comment, this introduces a source incompatible change. This is addressed further in the "Alternative Designs" and "Risks" sections.

Alternative Designs

These alternative designs aim to avoid the source-incompatibility problem mentioned above.

Create new extension methods to avoid source incompatibility problems

We could create new extension methods like ProducesWithDescription instead of altering the existing Produces extension methods (and others..) to avoid the source incompatibility problem.

app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
  .Produces<IList<Todo>>(StatusCodes.Status200OK, "application/json");

app.MapGet("/foo", async (TodoDb db) => await db.Todos.ToListAsync())
  .ProducesWithDescription<IList<Foo>>(StatusCodes.Status200OK, "The description of the response", "application/json", "text/json");

However, I do not like this much as it feels a little "bolted-on". It might also just add confusion for developers because of a larger number of available methods.

Extend the TypedResults methods with Description information

This might be a bit of a stretch, but perhaps we could extend the TypedResults methods with a Description parameter. This way, the description would be set in the same place as where the response is created:

app.MapGet("/todos", async (TodoDb db) =>
{
    var todos = await db.Todos.ToListAsync();
    return TypedResults.Ok(todos, "Returns a list of todo items");
});

I'm not sure if this is a good idea, though:

  1. Having the response description in the same code as actual endpoint code might increase verbosity. Developers might also think this is returned to the client, even though it's only used for OpenAPI.
  2. Now that this description is part of actual endpoint code, a developer could also put variables into it, making the description dynamic. We can't support something like that, as the OpenAPI description should be (mostly) static.

Introduce unclear breaking change by making description the first argument of the extension methods, causing a clearer breaking change

Another way to avoid source incompatibility problems is to create a breaking change and make the description the first argument of the OpenAPI Produces extension methods. This way there isn't any confusion about the string being an additional content type.

app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
  .Produces<IList<Todo>>("This is the description", StatusCodes.Status200OK, "application/json"));

However, I do not think this is very pretty either, and I believe that creating a breaking change for this is not worth it.

@sander1095 sander1095 added the design-proposal This issue represents a design proposal for a different issue, linked in the description label Oct 31, 2024
@dotnet-issue-labeler dotnet-issue-labeler bot added the old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels label Oct 31, 2024
@martincostello martincostello added feature-openapi area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc and removed old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels labels Oct 31, 2024
@captainsafia
Copy link
Member

@sander1095 Thanks for filing this!

Unfortunately, do to the nature of the compatability gurantees that we provide, it would be impossible for us to introduce a source, binary, or behavior change in this API as part of the release so those API shapes are ruled out.

I believe that this problem can be addressed by the proposal in #59180 which allows the use of operation transformers that are local to the endpoint.

@whysocket
Copy link
Contributor

Thanks for the proposal @sander1095.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc design-proposal This issue represents a design proposal for a different issue, linked in the description feature-openapi
Projects
None yet
Development

No branches or pull requests

4 participants