Skip to content

New WithOpenApi() method still breaks ASP.NET API Versioning #953

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
1 task done
perkops opened this issue Jan 20, 2023 · 3 comments
Closed
1 task done

New WithOpenApi() method still breaks ASP.NET API Versioning #953

perkops opened this issue Jan 20, 2023 · 3 comments

Comments

@perkops
Copy link

perkops commented Jan 20, 2023

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

Following up on issue (#920) there is still an issue present with the new WithOpenApi method.

I tried to clone https://github.com/joaofbantunes/AspNetApiVersioningWithOpenApiRepro from previous issue and cut it down to a bare minimum API.

var ordersV1 = orders.MapGroup( "/api/orders" )
    .HasApiVersion( 1.0 );

ordersV1.MapGet( "/{id:int}", ( int id ) => new OrderV1() { Id = id, Customer = "John Doe" } )
    .WithOpenApi()
    .Produces<OrderV1>();

ordersV1.MapPost( "/", ( HttpRequest request, OrderV1 order ) =>
    {
        order.Id = 42;
        var scheme = request.Scheme;
        var host = request.Host;
        var location = new Uri( $"{scheme}{Uri.SchemeDelimiter}{host}/api/orders/{order.Id}" );
        return Results.Created( location, order );
    } )
    .Accepts<OrderV1>( "application/json" )
    .Produces<OrderV1>( 201 )
    .Produces( 400 )
    .MapToApiVersion( 1.0 );

ordersV1.MapPatch( "/{id:int}", ( int id, OrderV1 order ) => Results.NoContent() )
    .Accepts<OrderV1>( "application/json" )
    .Produces( 204 )
    .Produces( 400 )
    .Produces( 404 )
    .MapToApiVersion( 1.0 );

When adding the WithOpenApi method to the ordersV1.MapGet endpoint, the api-version query parameter disappears from swagger. On the MapPatch endpoint, where WithOpenApi is not added, the api-version query parameter still is present.
image

The primary reason for me to utilize .WithOpenApi method atm, is to properly add a Summary to my endpoints, seeing as .WithSummary method does not work properly with Swagger.

image

Expected Behavior

ASP.NET API Versioning features working the same, regardless of WithOpenApi usage.

Steps To Reproduce

Clone the repo from previous issue: https://github.com/joaofbantunes/AspNetApiVersioningWithOpenApiRepro

Cut the sample down to a minimum:

var ordersV1 = orders.MapGroup( "/api/orders" )
    .HasApiVersion( 1.0 );

ordersV1.MapGet( "/{id:int}", ( int id ) => new OrderV1() { Id = id, Customer = "John Doe" } )
    .WithOpenApi()
    .Produces<OrderV1>();

ordersV1.MapPost( "/", ( HttpRequest request, OrderV1 order ) =>
    {
        order.Id = 42;
        var scheme = request.Scheme;
        var host = request.Host;
        var location = new Uri( $"{scheme}{Uri.SchemeDelimiter}{host}/api/orders/{order.Id}" );
        return Results.Created( location, order );
    } )
    .Accepts<OrderV1>( "application/json" )
    .Produces<OrderV1>( 201 )
    .Produces( 400 )
    .MapToApiVersion( 1.0 );

ordersV1.MapPatch( "/{id:int}", ( int id, OrderV1 order ) => Results.NoContent() )
    .Accepts<OrderV1>( "application/json" )
    .Produces( 204 )
    .Produces( 400 )
    .Produces( 404 )
    .MapToApiVersion( 1.0 );

Try to incomment/outcomment the WithOpenApi method on the ordersV1.MapGet and observe differences in Swagger Page.

Exceptions (if any)

No response

.NET Version

7.0.102

Anything else?

Microsoft.AspNetCore.OpenApi v 7.0.2 being used in the example - updated from v. 7.0.0

@commonsensesoftware
Copy link
Collaborator

This happens because WithOpenApi creates an OpenApiOperation instance behind the scenes. The consequence of that behavior is that there is some internal short-circuit logic that completely bypasses everything that the API Explorer extensions provide. API Versioning doesn't directly depend on or (currently) provide OpenAPI-specific extensions. Since the API Explorer is bypassed, the additional exploration behaviors are skipped. This is why you don't see API version parameter; it never got a chance to be processed.

I agree that setting the summary or description of the endpoint is command and trivial, but there have been some long-time gaps between Swashbuckle (which I presume you're using) and the API Explorer. One of those gaps is not using the provided description when the value doesn't come from XML Comments. The common case for this scenario in API Versioning is adding the description to for the API version parameter itself, which is code-generated. This means that even if you were to set a description another way through API Explorer it won't automatically get picked up by Swashbuckle.

Another issue is that WithOpenApi creates a single OpenApiOperation whereas API Versioning would normally fan-out explored ApiDescription instances by cloning them per API version. This means that if a configured endpoint maps to more than one API version, it is currently not possible to have two different sets of documentation a la multiple OpenApiOperation instances. If you wanted two different descriptions by API version, this is not easily achievable using WithOpenApi today. The current design and behavior assumes there is a one-to-one mapping of endpoint to OpenAPI documentation. This can happen in other scenarios too, such as if the same endpoint is deprecated in one API version, but not the other.

I've had a discussion with the ASP.NET team and there maybe some improvements to OpenAPI integration coming in the .NET 8.0 timeframe. There are no formal details just yet. API Versioning may create a new, OpenAPI specific library that can bridge some of these gaps. Historically, API Versioning has tried to stay away from specific uses such as OpenAPI and extend the API Explorer so that consumers can use it however they like. The ASP.NET team has expressed they are more interested in investing in OpenAPI specific extensions as opposed to the API Explorer. I'm onboard and committed to making everything work smoother together, but some level of extensions and hooks into the OpenAPI support of today is needed to provide parity closer to what is available via the API Explorer today.

So what can you do right now? I suspect you will not be able to use WithOpenApi this way, but I'll see if I can't flush out a workable solution. You will likely have to use a Swashbuckle IOperationFilter to achieve the necessary result. This is already required for adding the description to the API version parameter (as seen in the example projects). If the description can be set via the API Explorer, then the same approach and code should work without further modification.

Hopefully that gets you started and unblocked.

parameter.Description ??= description.ModelMetadata?.Description;

cc: @captainsafia

@commonsensesoftware
Copy link
Collaborator

commonsensesoftware commented Jan 20, 2023

The description metadata is actually independent of OpenAPI. The following would set the description on the endpoint:

ordersV1.MapGet( "/{id:int}", ( int id ) => new OrderV1() { Id = id, Customer = "John Doe" } )
        .WithDescription( "Gets a single order." )
        .Produces<OrderV1>();

While this will set the IEndpointDescriptionMetadata, it will go unused without some additional work. You can wire this up to Swashbuckle with:

public class SwaggerDefaultValues : IOperationFilter
{
    public void Apply( OpenApiOperation operation, OperationFilterContext context )
    {
        var apiDescription = context.ApiDescription;
        var metadata = apiDescription.ActionDescriptor.EndpointMetadata;

        // if the description hasn't be set, try to use the value from the configured metadata
        operation.Description ??= metadata.OfType<IEndpointDescriptionMetadata>().FirstOrDefault()?.Description;
    }
}

@perkops
Copy link
Author

perkops commented Jan 23, 2023

Hi @commonsensesoftware
Thank you very much for the very detailed explanation and suggestions to get me unblocked.

I tried out your suggestion and extended upon it, to also set Summary if it was missing in the SwaggerDefaultValues operationFilter.

        var apiDescription = context.ApiDescription;

        var metadata = apiDescription.ActionDescriptor.EndpointMetadata;

        operation.Description ??= metadata.OfType<IEndpointDescriptionMetadata>().FirstOrDefault()?.Description;
        operation.Summary ??= metadata.OfType<IEndpointSummaryMetadata>().FirstOrDefault()?.Summary;

I could also get it working by utilizing the Swashbuckle.AspNetCore.Annotations package and utilize the .WithMetaData(new SwaggerOperationAttribute("summary", "description") and (EnableAnnotations in AddSwaggerGen).
However I found that the first solution from your side was more clean so I went with that :)

But it would be great if the .WithOpenApi would honor past pipeline configurations @captainsafia going forward in .NET 8.

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

No branches or pull requests

2 participants