Skip to content

Improve OpenAPI/Swagger response metadata for returned IResults #33924

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

Closed
Tracked by #34514
halter73 opened this issue Jun 28, 2021 · 10 comments · Fixed by #34906
Closed
Tracked by #34514

Improve OpenAPI/Swagger response metadata for returned IResults #33924

halter73 opened this issue Jun 28, 2021 · 10 comments · Fixed by #34906
Assignees
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-minimal-actions Controller-like actions for endpoint routing old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels Priority:1 Work that is critical for the release, but we could probably ship without
Milestone

Comments

@halter73
Copy link
Member

Today, when you return an IResult from a minimal action, you lose all metadata about the type of the response (see #33433). You can add the metadata back with something like [ProducesResponseType(typeof(Person), 201], but in many cases the response type could be inferred from the IResult implementation if we designed a way for this to work.

The most obvious idea is to do like ActionResult<TValue> though implicit conversions with interfaces might make this tricky.

@halter73 halter73 added area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-minimal-actions Controller-like actions for endpoint routing labels Jun 28, 2021
@pranavkm pranavkm added area-runtime and removed area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates labels Jun 29, 2021
@BrennanConroy BrennanConroy added this to the Backlog milestone Jul 9, 2021
@ghost
Copy link

ghost commented Jul 9, 2021

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

@halter73
Copy link
Member Author

Another option is adding extension methods on MinimalActionEndpointConventionBuilder that adds the Produces*Attributes as endpoint metadata.

Here's an example from @DamianEdwards showing how this could look.

app.MapPost("/todos", async (Todo todo, TodoDb db) =>
{
    if (!MinimalValidation.TryValidate(todo, out var errors))
        return Results.ValidationProblem(errors);

    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todos/{todo.Id}", todo);
})
.ProducesValidationProblem()
.Produces<Todo>();

@DamianEdwards
Copy link
Member

DamianEdwards commented Jul 17, 2021

That example (and a few more) are from a functional prototype/example at https://github.com/DamianEdwards/MinimalApiPlayground/blob/main/src/MinimalApiPlayground/Program.cs

@martincostello
Copy link
Member

Something like that would be good.

I've done a similar thing with the project I've been trying minimal actions out with: https://github.com/martincostello/dotnet-minimal-api-integration-testing/blob/2c9bf003d8213e73d06729d0001d48952f6fa741/src/TodoApp/ApiModule.cs#L31-L32

I've suggested a similar thing for ignoring the endpoints too in #34068.

Though at the same time it's made me wonder at what point the dividing line between minimal actions and "growing up" to moving "back" to a controller is?

Not a criticism as it's a new different style of implementing APIs, it's just that with a bunch of annotations on an action that's only a few lines of code it's suddenly a lot less...minimal 🙂

@DamianEdwards
Copy link
Member

@martincostello I actually think one of the benefits of the minimal API style is that the extension methods are (admittedly subjectively) less ceremony and far easier to discover than the attributes. The attribute names are somewhat fixed now but it's easy for us to add method overloads that do more/less/allow differing expression.

@DamianEdwards
Copy link
Member

Also I do think at some point we'll enable hopefully much of this as a convention/automatically so that one doesn't need to describe the response shape manually at all, e.g. via a source generator.

@DamianEdwards
Copy link
Member

DamianEdwards commented Jul 20, 2021

Strawman set of extension methods for setting endpoint metadata. Additional context provided in the doc/code comments. These methods and their new supporting types (where required) can also be seen in the playground repo.

using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;

namespace Microsoft.AspNetCore.Http;

public static class OpenApiEndpointConventionBuilderExtensions
{
    /// <summary>
    /// Adds an EndpointNameMetadata item to the Metadata for all endpoints produced by the builder.<br />
    /// The name is used to lookup the endpoint during link generation and as an operationId when generating OpenAPI documentation.<br />
    /// The name must be unique per endpoint.
    /// </summary>
    /// <param name="builder">The Microsoft.AspNetCore.Builder.IEndpointConventionBuilder.</param>
    /// <param name="name">The endpoint name.</param>
    /// <returns>The Microsoft.AspNetCore.Builder.IEndpointConventionBuilder.</returns>
    public static IEndpointConventionBuilder WithName(this IEndpointConventionBuilder builder, string name)
    {
        // Once Swashbuckle issue is fixed this will set operationId in the swagger doc
        // https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2165
        builder.WithMetadata(new EndpointNameMetadata(name));

        return builder;
    }

    /// <summary>
    /// Adds an EndpointGroupNameMetadata item to the Metadata for all endpoints produced by the builder.
    /// </summary>
    /// <param name="builder"></param>
    /// <param name="groupName"></param>
    /// <returns>The Microsoft.AspNetCore.Builder.IEndpointConventionBuilder.</returns>
    public static IEndpointConventionBuilder WithGroupName(this IEndpointConventionBuilder builder, string groupName)
    {
        // Swashbuckle uses group name to match APIs with OpenApi documents by default
        // See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs#L59
        // Minimal APIs currently doesn't populate the ApiDescription with a group name but we will change that so this can work as intended.
        // Note that EndpointGroupNameMetadata doesn't exist in ASP.NET Core today so we'll have to add that too.
        builder.WithMetadata(new EndpointGroupNameMetadata(groupName));

        return builder;
    }

    /// <summary>
    /// Adds metadata indicating the type of response an endpoint produces.
    /// </summary>
    /// <typeparam name="TResponse">The type of the response.</typeparam>
    /// <param name="builder">The Microsoft.AspNetCore.Builder.IEndpointConventionBuilder.</param>
    /// <param name="statusCode">The response status code. Defatuls 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>The Microsoft.AspNetCore.Builder.IEndpointConventionBuilder.</returns>
    public static IEndpointConventionBuilder Produces<TResponse>(this IEndpointConventionBuilder builder,
        int statusCode = StatusCodes.Status200OK,
        string? contentType = "application/json",
        params string[] additionalContentTypes)
    {
        return Produces(builder, statusCode, typeof(TResponse), contentType, additionalContentTypes);
    }

    /// <summary>
    /// Adds metadata indicating the type of response an endpoint produces.
    /// </summary>
    /// <param name="builder">The Microsoft.AspNetCore.Builder.IEndpointConventionBuilder.</param>
    /// <param name="statusCode">The response status code. Defatuls to StatusCodes.Status200OK.</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>The Microsoft.AspNetCore.Builder.IEndpointConventionBuilder.</returns>
    public static IEndpointConventionBuilder Produces(this IEndpointConventionBuilder builder,
        int statusCode = StatusCodes.Status200OK,
        Type? responseType = null,
        string? contentType = null,
        params string[] additionalContentTypes)
    {
        if (responseType is Type && string.IsNullOrEmpty(contentType))
        {
            contentType = "application/json";
        }

        builder.WithMetadata(new ProducesMetadataAttribute(responseType, statusCode, contentType, additionalContentTypes));

        return builder;
    }

    /// <summary>
    /// Adds metadata indicating that the endpoint produces a Problem Details response.
    /// </summary>
    /// <param name="builder">The Microsoft.AspNetCore.Builder.IEndpointConventionBuilder.</param>
    /// <param name="statusCode">The response status code. Defatuls to StatusCodes.Status500InternalServerError.</param>
    /// <param name="contentType">The response content type. Defaults to "application/problem+json".</param>
    /// <returns>The Microsoft.AspNetCore.Builder.IEndpointConventionBuilder.</returns>
    public static IEndpointConventionBuilder ProducesProblem(this IEndpointConventionBuilder builder,
        int statusCode = StatusCodes.Status500InternalServerError,
        string contentType = "application/problem+json")
    {
        return Produces<ProblemDetails>(builder, statusCode, contentType);
    }

    /// <summary>
    /// Adds metadata indicating that the endpoint produces a Problem Details response including validation errors.
    /// </summary>
    /// <param name="builder">The Microsoft.AspNetCore.Builder.IEndpointConventionBuilder.</param>
    /// <param name="statusCode">The response status code. Defatuls to StatusCodes.Status400BadRequest.</param>
    /// <param name="contentType">The response content type. Defaults to "application/problem+json".</param>
    /// <returns>The Microsoft.AspNetCore.Builder.IEndpointConventionBuilder.</returns>
    public static IEndpointConventionBuilder ProducesValidationProblem(this IEndpointConventionBuilder builder,
        int statusCode = StatusCodes.Status400BadRequest,
        string contentType = "application/problem+json")
    {
        return Produces<HttpValidationProblemDetails>(builder, statusCode, contentType);
    }
}

Design considerations

Further details that were considered during the design of this proposal:

  • Having specific methods for each response type that could be produced by the methods on Microsoft.AspNetCore.Http.Results, e.g. ProducesCreated, ProducesConflict, ProducesNotFound, etc. As the eventual goal is for the experience to determine the correct information RE what endpoints produce and set the relevant metadata by default, thus requiring these methods to not be required to be called in the vast majority of cases, it was decided that we shouldn't create such a large set of API surface area only to have it not be used (generally) in the future.
  • After the methods were reduced to just a minimal set, as detailed in the previous point, it was apparent that specifying that an API produces Problem Details required an extremely verbose and likely unintuitive call, i.e. Produces<ProblemDetails>(500, "application/problem+json") and Produces<HttpValidationProblemDetails>(400, "application/problem+json"). For this reason it was decided that higher level methods for problem details should be included, e.g. ProducesProblem, ProducesValidationProblem. These offer a nice symmetry with the Results.Problem and Results.ValidationProblem results methods too.

@davidfowl
Copy link
Member

Ignored method get a random group name?

@DamianEdwards
Copy link
Member

DamianEdwards commented Jul 20, 2021

@davidfowl I'm not recommending that, but that would make Swashbuckle's existing logic work.

We should instead create a new first-class metadata type for indicating an endpoint should be ignored from ApiExplorer's POV (#34068) and either not publish endpoints with that metadata into ApiExplorer or update Swashbuckle and nSwag to not include those endpoints in document/code-generation if the metadata is present.

I've removed that from the code block above as it was causing confusion.

Some related detail coming together in #34514

@ghost
Copy link

ghost commented Jul 20, 2021

Thanks for contacting us.

We're moving this issue to the Next sprint planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

@rafikiassumani-msft rafikiassumani-msft added the Priority:1 Work that is critical for the release, but we could probably ship without label Jul 22, 2021
@davidfowl davidfowl added old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels and removed area-runtime labels Jul 31, 2021
captainsafia added a commit that referenced this issue Aug 6, 2021
* Support setting content types in ProducesResponseTypeAttribute to close #34542

* Add WithName extension method to resolve #34538

* Support setting endpoints on group names to resolve #34541

* Add OpenAPI extension methods to resolve #33924

* Add tests for new OpenAPI methods

* Add endpoint metadata attributes

* Update PublicAPI files with deltas

* Add support for SuppressApi to close #34068

* Update tests to account for supporting setting content types

* Fix up PublicAPI analyzer warnings

* Clean up source files

* Address feedback from API review

* Fix typo and update type signature

* Apply feedback from second API review

* Update docstrings

* Apply suggestions from code review

Co-authored-by: Martin Costello <martin@martincostello.com>

* Address non-test related feedback

* Handle setting content types for ProducesResponseType attribute

* Address feedback from peer review

* Add test for ProducesResponseType override scenario

Co-authored-by: Martin Costello <martin@martincostello.com>
@ghost ghost locked as resolved and limited conversation to collaborators Sep 7, 2021
@amcasey amcasey added the area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc label Jun 2, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-minimal-actions Controller-like actions for endpoint routing old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels Priority:1 Work that is critical for the release, but we could probably ship without
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants