Skip to content
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

Error "InvalidOperationException: Sequence contains more than one matching element" in Swagger when using minimal api with the header versioning #54623

Closed
1 task done
vdevc opened this issue Mar 19, 2024 · 4 comments
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc External This is an issue in a component not contained in this repository. It is open for tracking purposes. feature-openapi ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. Status: Resolved
Milestone

Comments

@vdevc
Copy link

vdevc commented Mar 19, 2024

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

I am trying to use Asp.Versioning package with Header versioning together with minimal apis endpoints.
Every endpoint is like this example

routeGroupBuilder.MapGet("/", ([FromHeader(Name = "x-api-version")] string apiVersion, [FromRoute] int customerId,
    [FromServices] LinkGenerator linkGenerator ) => {
    return new List<string> { "Individual1", "Individual2" };
})
.WithName("GetIndividuals")
.WithDescription("Get a list of individuals for a customer.")
.WithSummary("Get a list of individuals for a customer.")
.WithOpenApi()
.Produces<List<string>>(200)
.Produces<List<string>>(404);

Whenever I launch my project I get the following exception

Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware: Error: An unhandled exception has occurred while executing the request.

System.InvalidOperationException: Sequence contains more than one matching element
   at System.Linq.ThrowHelper.ThrowMoreThanOneMatchException()
   at System.Linq.Enumerable.TryGetSingle[TSource](IEnumerable`1 source, Func`2 predicate, Boolean& found)
   at System.Linq.Enumerable.SingleOrDefault[TSource](IEnumerable`1 source, Func`2 predicate)
   at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateOpenApiOperationFromMetadata(ApiDescription apiDescription, SchemaRepository schemaRepository)
   at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateOperation(ApiDescription apiDescription, SchemaRepository schemaRepository)
   at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateOperations(IEnumerable`1 apiDescriptions, SchemaRepository schemaRepository)
   at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GeneratePaths(IEnumerable`1 apiDescriptions, SchemaRepository schemaRepository)
   at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetSwaggerDocumentWithoutFilters(String documentName, String host, String basePath)
   at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetSwaggerAsync(String documentName, String host, String basePath)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

Please note: this issue was initially reported in issue #53831 having the same effect within a similar context. It has now been splitted on a request from @captainsafia

Expected Behavior

Swagger should not throw the exception.

Steps To Reproduce

You can find a repro here: https://github.com/vdevc/FromHeaderBindingIssue

Exceptions (if any)

System.InvalidOperationException: Sequence contains more than one matching element

.NET Version

Verified with SDKs 8.0.100, 8.0.101, 8.0.201, 8.0.202 and 8.0.203. The behaviour is consistent between all the versions.

Anything else?

No response

@captainsafia
Copy link
Member

I took a look at this and I think the issue is at the intersection of Asp.Versioning and some of the code that is used to merge OpenApiOperations in metadata into the Swashbuckle generated document (see here).

For whatever reason, the Asp.Versioning configuration in the setup populates the x-api-version header argument into the operation twice.

/customers/{customerId}/businesses": {
      "get": {
        "tags": [
          "Businesses"
        ],
        "operationId": "GetBusinesses",
        "parameters": [
          {
            "name": "x-api-version",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "x-api-version",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],

This seems buggy to mean and the bug is magnified by the SingleOrDefault check in the code above that assumes there is only one parameter with a given name per operation.

There's two avenues to fix it:

  • Remove the SingleOrDefault check in the code above
  • Figure out why the x-api-version parameter is being set twice in the operations

Also, FWIW, this issue only manifests when WithOpenApi is called on an endpoint so a viable workaround is to remove it. Especially, if it is not being used to make any modifications.

cc: @martincostello @commonsensesoftware for any thoughts on this

@captainsafia captainsafia added feature-openapi area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc External This is an issue in a component not contained in this repository. It is open for tracking purposes. and removed area-web-frameworks *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels labels May 6, 2024
@captainsafia captainsafia added this to the Discussions milestone May 6, 2024
@captainsafia
Copy link
Member

Update: I realized that the problem might be the fact that the x-api-version parameter is being referenced in the method signature of the handler:

.MapGet("/", (
                [FromHeader(Name = "x-api-version")] string apiVersion,
                [FromRoute] int customerId,
                [FromServices] ILoggerFactory loggerFactory,
                [FromServices] LinkGenerator linkGenerator
            ) =>
            {
                return new List<string> { "Business 1", "Business 2" };
            })

I bet this is duplicative with what Asp.Versioning is doing in its ApiExplorer overloads. Removing the apiVersion argument above should resolve the issue as well.

@commonsensesoftware
Copy link

@captainsafia is correct.

@vdevc explicitly defining the API version with the x-api-version header in your action is unsupported and unknown to API Versioning. It doesn't do any magic string parsing or matching on the method signature. It's special and analogous to something like CancellationToken in an action. This is one reason the parameter doesn't not have to be in your action for it to work. In addition, you have to consider that API Versioning supports multiple sources of the API version. Your action signature is not required to know which API version was selected if there are multiple nor how it was selected. For example, you might support the x-api-version header and the api-version query string. By the time things get to your action, it doesn't matter which one was specified.

If you want the requested API version passed into your action, you have two options.

Option 1

Enable binging the incoming API version to your endpoints with:

builder.Services.AddApiVersioning().EnableApiVersionBinding();

Since model binders are not supported in Minimal APIs and ApiVersion cannot use the TryBindAsync API, API Versioning does some internal sorcery to make it work. Hopefully, this will improve at a future date 🤞🏽. You can now write your action as:

.MapGet("/", (
                [FromRoute] int customerId,
                [FromServices] ILoggerFactory loggerFactory,
                [FromServices] LinkGenerator linkGenerator,
                ApiVersion apiVersion,
            ) =>
            {
                return new List<string> { "Business 1", "Business 2" };
            })

Today, this works via DI and would be equivalent to [FromServices] ApiVersion apiVersion, but I don't recommend using [FromServices] to be more explicit as that may change in the future and could break things. The order and name of the action parameter is irrelevant and can be anything you want. You can be guaranteed that by the time your action is invoked, the provided ApiVersion will never be null.

Option 2

The other option is to use the HttpContext extension method. This is less ergonomic, but doesn't any special magic. You can use the same approach in other places outside of your action.

.MapGet("/", (
                [FromRoute] int customerId,
                [FromServices] ILoggerFactory loggerFactory,
                [FromServices] LinkGenerator linkGenerator,
                HttpContext context,
            ) =>
            {
                var apiVersion = context.GetRequestedApiVersion()!;
                return new List<string> { "Business 1", "Business 2" };
            })

Note that it is possible for GetRequestedApiVersion() to return null, but that will never happen in the context of your action. If you want or need the unparsed, originally specified string value, you can use HttpContext.GetRawRequestedApiVersion(). That is the only way you can get that value.


Either approach will remove the duplicate x-api-version header. In addition, since HttpContext and ApiVersion are special, they will not be documented from the action signature. The ApiVersion parameter is documented according to your configuration (which you've already seen working).

@captainsafia captainsafia added the ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. label May 7, 2024
@vdevc
Copy link
Author

vdevc commented May 8, 2024

@commonsensesoftware @captainsafia
Thanks a lot for the detailed explanations. I must say that I completely missed the fact that was unsupported to have the header in the signature. However, I was aware that it was working without having the header as a parameter. At the same time I was'nt aware of the alternatives represented by the two described options.
Thanks!

@commonsensesoftware
Besides the fact that this solution works and permit the generation of the swagger UI, there's still a problem which I did not have a couple of years ago in the same code but with some differences (Net6, controllers instead of minimal apis, and a few others). The swagger UI does not generate the field for specifying the api version to use.
I can't say if this depends on the versioning package or on swashbuckle or anything else, however.

@vdevc vdevc closed this as completed May 8, 2024
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 External This is an issue in a component not contained in this repository. It is open for tracking purposes. feature-openapi ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. Status: Resolved
Projects
None yet
Development

No branches or pull requests

3 participants