Closed vdevc closed 6 months ago
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 OpenApiOperation
s 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:
SingleOrDefault
check in the code abovex-api-version
parameter is being set twice in the operationsAlso, 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
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.
@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.
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
.
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).
@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.
Is there an existing issue for this?
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
Whenever I launch my project I get the following exception
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
and8.0.203
. The behaviour is consistent between all the versions.Anything else?
No response