Skip to content

Add Description to ProducesResponseType (and others) for better OpenAPI documents #55656

@sander1095

Description

@sander1095

Background and Motivation

The purpose of this API Change is to make it easier for developers to add the Description properties to their OpenAPI documents using the [ProducesResponseType], [Produces] and [ProducesDefaultResponseType] attributes in controller actions.

Developers currently use these properties to enrich the OpenAPI document of the API. It tells the reader the possible return values from an endpoint, like a status code, response model type and content type:

See code + OpenAPI example
[HttpGet("{id:int:min(1)}")]
[ProducesResponseType<Talk>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<Talk> GetTalk(int id)
{
    var talk = _talks.FirstOrDefault(x => x.Id == id);
    if (talk == null)
    {
        return NotFound();
    }

    return Ok(talk);
}

OpenAPI document is generated using NSwag (I used a JSON to YAML converter)

paths:
  "/api/talks/{id}":
    get:
      tags:
      - Talks
      operationId: Talks_GetTalk
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
          format: int32
        x-position: 1
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Talk"
        '404':
          description: ''
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ProblemDetails"

However, there is currently no way to map OpenAPIs description using attributes, which is the easiest way to enrich your methods with OpenAPI information which also works well with the OpenAPI analyzers.

It's important to make it easy for developers to set up the Description property because this adds a lot of important information to a specific response. For example, if an API returns HTTP 422, it would be useful to add a description explaining why/when this error is returned. Without this, handling this error in the client becomes more difficult because the meaning is lost.

There are other ways to set the Description right now, but I'm not a big fan of them. I explain why in Alternative designs below.

I've wanted this feature in ASP.NET Core for a while. I've created this API proposal after talking to @captainsafia about this at the MVP Summit 2024, and she agreed that this would be a worthy addition. I had already created #54064 for this at some point. I hope the rest of the team/community agrees!

Proposed API

// Assembly: Microsoft.AspNetCore.Mvc.Core

namespace Microsoft.AspNetCore.Mvc.ApiExplorer;

public interface IApiResponseMetadataProvider : IFilterMetadata
{
+	string? Description { get; }
}
// Assembly: Microsoft.AspNetCore.Mvc.Core

namespace Microsoft.AspNetCore.Mvc;

public class ProducesAttribute : Attribute, IResultFilter, IOrderedFilter, IApiResponseMetadataProvider
{
+	public string? Description { get; set; }
}
// Assembly: Microsoft.AspNetCore.Mvc.Core

namespace Microsoft.AspNetCore.Mvc;

public class ProducesResponseTypeAttribute : Attribute, IResultFilter, IOrderedFilter, IApiResponseMetadataProvider
{
+	public string? Description { get; set; }
}
// Assembly: Microsoft.AspNetCore.Mvc.Abstractions

namespace Microsoft.AspNetCore.Mvc.ApiExplorer;

public class ApiResponseType
{
+   public string? Description { get; set; }

     // Existing code here
}
// Assembly:  Microsoft.AspNetCore.Http.Abstractions;

namespace Microsoft.AspNetCore.Http.Metadata;

public interface IProducesResponseTypeMetadata
{
+	string? Description { get; }
}
// Assembly:  Microsoft.AspNetCore.Http.Abstractions;

namespace Microsoft.AspNetCore.Http;

public sealed class ProducesResponseTypeMetadata : IProducesResponseTypeMetadata
{
+	string? Description { get; private set; }
}
// Assembly: Microsoft.AspNetCore.Routing

namespace Microsoft.AspNetCore.Http;

public static class OpenApiRouteHandlerBuilderExtensions
{
+   public static RouteHandlerBuilder Produces(
+       this RouteHandlerBuilder builder,
+       int statusCode,
+       Type? responseType = null,
+		 string? description = null,
+       string? contentType = null,
+        params string[] additionalContentTypes)

+   public static RouteHandlerBuilder Produces<T>(
+       this RouteHandlerBuilder builder,
+       int statusCode,
+		 string? description = null,
+       string? contentType = null,
+        params string[] additionalContentTypes)

+ public static RouteHandlerBuilder ProducesProblem(
+		this RouteHandlerBuilder builder,
+		int statusCode,
+		string? description = null,
+		string? contentType = null)

+ public static TBuilder ProducesProblem<TBuilder>(
+		this TBuilder builder,
+		int statusCode,
+		string? description = null,
+		string? contentType = null)

+    public static RouteHandlerBuilder ProducesValidationProblem(
+        this RouteHandlerBuilder builder,
+        int statusCode = StatusCodes.Status400BadRequest,
+		  string? description = null,
+        string? contentType = null)

+    public static TBuilder ProducesValidationProblem(
+        this TBuilder builder,
+        int statusCode = StatusCodes.Status400BadRequest,
+		  string? description = null,
+        string? contentType = null)
}

Usage Examples

-/// <response code="200">A talk entity when the request is succesful</response>
-/// <response code="422">The entity is unprocessable because SOME_REASON_HERE</response>
[HttpGet("{id:int:min(1)}")]
-[Produces<Talk>]
+[Produces<Talk>(Description = "A talk entity when the request is succesful")]
-[ProducesResponseType<Talk>(StatusCodes.Status200OK)]
+[ProducesResponseType<Talk>(StatusCodes.Status200OK, Description = "A talk entity when the request is succesful")]
-[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
+[ProducesResponseType(StatusCodes.Status422UnprocessableEntity, Description = "The entity is unprocessable SOME_REASON_HERE")]
// I do not think there is currently a way to specify a description for the default error response type, so this is a nice bonus!
-[ProducesDefaultResponseType]
+[ProducesDefaultResponseType(Description = "The response for all other errors that might be thrown")]
public ActionResult<Talk> GetTalk(int id)
{
    var talk = _talks.FirstOrDefault(x => x.Id == id);
    if (talk == null)
    {
        return NotFound();
    }

    if (someCondition)
    {
        return UnprocessableEntity();
    }

    return Ok(talk);
}

Alternative Designs

XML comments

An alternative design is to do nothing and use the built-in solution, which are XML comments:

/// <response code="201">Returns the newly created item</response>
/// <response code="400">If the item is null</response>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Create(TodoItem item)
{
    _context.TodoItems.Add(item);
    await _context.SaveChangesAsync();

    return CreatedAtAction(nameof(Get), new { id = item.Id }, item);
}

This will fill the description properties with the values from the <response> tags.

However:

  • This becomes quite verbose when you're combining [ProducesResponseType] (and the other ones mentioned above) and XML comments.
  • We're duplicating XML comments and produces response type attributes, which isn't ideal because now we need to manage the same code twice, which will get out of sync over time.
  • I do not believe that the <response> can set a description for [ProducesDefaultResponseType]

To summarize: I do not think that XML comments is the correct one.

Library specific attributes

Both Swashbuckle.AspNetCore and NSwag have their own versions of [ProducesResponseType] called [SwaggerResponse] that do support the OpenAPI Description property.

However, there are several downsides to this:

  • They are library specific, wheras [ProducesResponseType] is library agnostic. By using [ProducesResponseType] I could switch from Swashbuckle to NSwag and the OpenAPI document generation should keep working, which isn't guaranteed if I used a library specific attribute.
  • Trying to combine NSwag's [SwaggerResponse] with [ProducesResponseType] to set up response descriptions doesn't work. When [SwaggerResponse] is used, all instances of [ProducesResponseType] are ignored for that method.
    • This means that some methods need to use [SwaggerResponse] for description support and others can still use [ProducesResponseType], which makes things more difficult to read because their method signature is different.
  • The OpenAPI analyzer doesn't work with library-specific attributes like [SwaggerResponse], which means I need to disable those warnings for specific methods, which is annoying and makes code more difficult to read.

If ASP.NET Core's [ProducesResponseType] would support things like Description, the library-specific versions attributes might not be needed anymore, which makes it even easier to switch libraries in the future.

Risks

Swashbuckle.AspNetCore's SwaggerResponse [ProducesResponseType] and already has its own Description property, which means that this change might cause some issues for them. However, .NET 9 won't ship Swashbuckle anymore by default, reducing the impact this has. Regardless, Swashbuckle's authors should be informed of this change.

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-approvedAPI was approved in API review, it can be implementedarea-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etcarea-mvcIncludes: MVC, Actions and Controllers, Localization, CORS, most templates

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions