-
Notifications
You must be signed in to change notification settings - Fork 714
-
Hi,
I'm working on a Web API 2 (.NET Framework 4.7.1, not .NET Core) with Swagger, including API versioning. I'm using the URL path versioning approach - as long as I attach the supported ApiVersion-Attributes to the controller, it works fine.
[ApiVersion("1.0")]
[ApiVersion("1.1")]
[RoutePrefix("api/v{version:apiVersion}/Item")]
public class ItemController : TItemControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
[Route("GetData/{userId:int}/{password}/{key}", Name = nameof(GetDataV10))]
[ResponseType(typeof(V10.Dto.TItemDataDto))]
public IHttpActionResult GetDataV10(int userId, string password, string key)
{
return Ok(new V10.Dto.TItemDataDto());
}
[HttpGet]
[MapToApiVersion("1.1")]
[Route("GetData/{userId:int}/{password}/{key}", Name = nameof(GetDataV11))]
[ResponseType(typeof(V11.Dto.TItemDataDto))]
public IHttpActionResult GetDataV11(int userId, string password, string key)
{
return Ok(new V11.Dto.TItemDataDto());
}
}//class;
The WebAPI startup looks like this:
public static void Register(HttpConfiguration config)
{
var constraintResolver = new DefaultInlineConstraintResolver() {
ConstraintMap = { ["apiVersion"] = typeof(ApiVersionRouteConstraint) }
};
config.AddApiVersioning(options => {
options.ReportApiVersions = true;
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
options.ApiVersionReader = new UrlSegmentApiVersionReader();
});
config.MapHttpAttributeRoutes(constraintResolver);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: new { apiVersion = new ApiVersionRouteConstraint() }
);
var apiExplorer = GlobalConfiguration.Configuration.AddVersionedApiExplorer(
options => {
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
}
But I have more than one single controller in the WebAPI, so everytime I add/specify a new version, I have to attach the new ApiVersion attribute to all of the controllers. Because of this effort, I tried to integrate "controller conventions" to be able to configure the ApiVersion-attributes per controller more centrally. So I changed the upper WebAPI configuration like this:
config.AddApiVersioning(options => {
options.ReportApiVersions = true;
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
options.ApiVersionReader = new UrlSegmentApiVersionReader();
// Configure the ApiVersion attributes using conventions:
options.Conventions.Controller<ItemController>().HasApiVersion(new ApiVersion(1, 0));
options.Conventions.Controller<ItemController>().HasApiVersion(new ApiVersion(1, 1));
});
Then, so I thought, I could remove the ApiVersion attributes from the controller, like
[RoutePrefix("api/v{version:apiVersion}/Item")]
public class ItemController : TItemControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
[Route("GetData/{userId:int}/{password}/{key}", Name = nameof(GetDataV10))]
[ResponseType(typeof(V10.Dto.TItemDataDto))]
public IHttpActionResult GetDataV10(int userId, string password, string key)
{
return Ok(new V10.Dto.TItemDataDto());
}
[HttpGet]
[MapToApiVersion("1.1")]
[Route("GetData/{userId:int}/{password}/{key}", Name = nameof(GetDataV11))]
[ResponseType(typeof(V11.Dto.TItemDataDto))]
public IHttpActionResult GetDataV11(int userId, string password, string key)
{
return Ok(new V11.Dto.TItemDataDto());
}
}//class;
But I got the following error, if I try to load the Swagger UI:
The method 'get' on path '/api/v/Item/GetData/{userId}/{password}/{key}' is registered multiple times (check the DefaultUrlTemplate setting [default for Web API: 'api/{controller}/{id}'; for MVC projects: '{controller}/{action}/{id?}']).
Obviously, the controllers RoutePrefix defition [RoutePrefix("api/v{version:apiVersion}/Item")]
won't be substituted anymore.
Has anybody a clue, what causes this problem?
Thanks in advance.
Best regards,
Tom
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 1 comment 9 replies
-
Hmmm... do you happen to have the world's simplest repro? I'm not able to recreate the conditions. Which version of the library are you using? This might be related to something that has already been fixed.
My initial guess is that this is related to mixing convention-based routing with direct routing (aka attribute routing). Due to limitations in the Web API routing system, this is strongly discouraged. There are edge cases where things will not work as expected. This could be one of the them. I also noticed that you included the ApiVersionRouteConstraint
in the mapped route, but it's not used in the template.
It's not possible to have an unversioned route. All actions will have an implicit and explicit corresponding API version. The only exception is version-neutral, which accepts all versions, which should be used with caution.
Another callout is that I see the API Explorer being added from GlobalConfiguration.Configuration
instead of the passed config
. It's not clear if they are the same instance, but they should be. This could result in missing configuration or a configuration sequence problem.
The issue is definitely not related to configurating versions by convention. The API Explorer extensions use the API version associated with the action. When options.SubstituteApiVersionInUrl = true
, the extensions replace the route template token with the associated API version. If this association was incorrect, you wouldn't be able to route to the action either. The crux of the issue is that there are two actions with the same route template in the same OpenAPI document and API version.
For your convenience, I've modified the provided example with your configuration to demonstrate things working. Perhaps that can help you work backward and determine where your application is different.
Some additional notes:
- The default, configured API version is
1.0
. Settingoptions.DefaultApiVersion = new ApiVersion(1, 0);
isn't wrong, but it's unnecessary options.AssumeDefaultVersionWhenUnspecified = true
will not do what you think it will do when you version by URL segment- It's not possible to have a default value in a route template that isn't at the end; it's the same as
{id}
inorder/{id}/item/{line}
. - It appeared you might have been trying that with the conventional route, but it doesn't match the actions shared
- It's not possible to have a default value in a route template that isn't at the end; it's the same as
Beta Was this translation helpful? Give feedback.
All reactions
-
Hi Chris,
I managed to solve the error!
As I said, I'm using .NET Framework 4.7.1 and I installed the nuget package Asp.Versioning.WebApi.ApiExplorer
(version 7.1.0).
I noticed something strange in the implementation of the class ApiVersionRouteConstraint
:
That cannot be correct. So I checked the implementation of this class and found a difference in the implementations between the .NET versions 4.5 and 4.7.2:
net45:
IApiVersionParser apiVersionParser = HttpRequestMessageExtensions.GetConfiguration(request).GetApiVersionParser();
...
if (apiVersionParser.TryParse(value, ref requestedApiVersion))
{
apiVersionRequestProperties.RequestedApiVersion = requestedApiVersion;
return true;
}
net472:
IApiVersionParser apiVersionParser = HttpRequestMessageExtensions.GetConfiguration(request).GetApiVersionParser();
...
if (apiVersionParser.TryParse(value, out ApiVersion apiVersion))
{
apiVersionRequestProperties.RequestedApiVersion = apiVersion;
return true;
}
At runtime the class ApiVersionRouteConstraint
tries to invoke TryParse of the IApiVersionParser implementation with the ref parameter, but cannot find it and throws the exception.
Because the method signature of the net472 implementation matches the wanted implementation the class ApiVersionRouteConstraint
is looking for, I switched my project to .NET Framework 4.7.2 and that worked.
I think, that means, that the net45 implementation of the class ApiVersionRouteConstraint
in the assembly Asp.Versioning.WebApi, Version=7.1.0
is in this case not fully compatible to the implementation of the class ApiVersionParser
in the assembly Asp.Versioning.Abstractions, Version=7.0.0
.
Is that correct?
Best regards,
Tom
Beta Was this translation helpful? Give feedback.
All reactions
-
🎉 1
-
This is a great find and I think it's leading us to the right track; however, I don't think this is quite the problem.
First, it looks like you are showing something that is decompiled. Source Link is supposed be enabled so you can step into things. If that's not the case, you should definitely file an issue and I'll get that fixed straight away. The decompiled version doesn't represent the source:
Line 46 in 3fc0719
That's not a huge deal, I just wanted to highlight the difference. Honestly, this probably would have no effect at runtime. There is little difference between ref
and out
from a runtime perspective. It's more about rules the compiler can enforce. ref
means that the reference can be passed by reference and mutated. It is also like having [In]
and [Out]
applied. out
means that the reference can only come out, but not in; effectively having [Out]
only. out
is, therefore, a subset of ref
.
By chance, do you explicitly have a reference to Asp.Versioning.Abstractions
? I suspect - yes and I think that's where things are going wrong. In Abstractions, the implementation for .NET Standard 2.0 and .NET looks like:
Line 23 in 3fc0719
but for .NET Standard 1.0 it looks like:
Line 23 in 3fc0719
This is because ReadOnlySpan<T>
isn't available in .NET Standard 1.0.
Asp.Versioning,.WebApi
only supports net45
and net472
. .NET Standard 2.0 is technically supportable by net461
and above, but it is strongly recommended that you only use net472
and above due to implementation gaps. This is why Asp.Versioning.WebApi
targets net472
instead of net461
.
You said you are targeting .NET 4.7.1, which means Asp.Versioning.WebApi
should resolve to the lower net45
, which in turn uses Asp.Versioning.Abstractions
with netstandard1.0
as a transitive dependency. If, however, you directly reference Asp.Versioning.Abstractions
instead, NuGet will do you wrong and reference the netstandard2.0
version because net471
is technically supportable by netstandard2.0
. Now, you're in a situation where you are using Asp.Versioning.WebApi
with net45
and Asp.Versioning.Abstractions
with netstandard2.0
, which are incompatible. Since the method signatures are different and there isn't an overload, you'll end up with MissingMethodException
. An overload isn't required because String
is implicitly convertible to ReadOnlySpan<char>
.
Ultimately, I believe that is the problem. There are two solutions:
- Move up and baseline to .NET 4.7.2
- Remove the direct reference to
Asp.Versioning.Abstractions
so that it resolves to .NET Standard 1.0
Let me know how that goes and we can look into next steps.
Beta Was this translation helpful? Give feedback.
All reactions
-
Hi Chris,
first things first: It is totally fine for me to switch to .NET Framework 4.7.2 - the WebAPI and the versioning are working then.
But I just wanted to clear things up, so here is some information about the project before I switched to .NET 4.7.2:
I don't referenced explicitly to Asp.Versioning.Abstractions
, only to Asp.Versioning.WebApi.ApiExplorer
, the assembly files Asp.Versioning.Abstractions
and Asp.Versioning.WebApi
are included as transitive packages.
Here is a screenshot of the directly referenced nuget packages:
grafik
And here is a screenshot of the included transitive packages of Asp.Versioning.WebApi.ApiExplorer
:
grafik
Yes, you are right, the first screenshot of my last post showed some decompiled code of the class ApiVersionRouteConstraint
, if I used "Go to Definition" in Visual Studio (SourceLink didn't work for me here). The code has been loaded from the assembly
C:\...\.nuget\packages\asp.versioning.webapi7円.1.0\lib\net45\Asp.Versioning.WebApi.dll
and it contains the mentioned method call if(apiVersionParser.TryParse(value, ref requestedApiVersion))
.
If I step into the method TryParse
, it then showed the following code (SourceLink worked here!) from the assembly
C:\...\.nuget\packages\asp.versioning.abstractions7円.0.0\lib\netstandard2.0
:
/// <summary>
/// Attempts to parse the specified text.
/// </summary>
/// <param name="text">The text to parse as an API version.</param>
/// <param name="apiVersion">The parsed API version or null.</param>
/// <returns>True if the parsing was successful; otherwise false.</returns>
bool TryParse(
ReadOnlySpan<char> text,
#if !NETSTANDARD
[MaybeNullWhen( false )]
#endif
out ApiVersion apiVersion );
Now, I'm in the situation you mentioned, where I'm using Asp.Versioning.WebApi
with net45 and Asp.Versioning.Abstractions
with netstandard2.0, which are incompatible, but I don't know, why this happens. I got the following versioning assemblies in the output folder after I compiled the WebAPI:
- Asp.Versioning.WebApi.ApiExplorer.dll (TargetFramework(".NETFramework,Version=v4.5"))
- Asp.Versioning.WebApi.dll (TargetFramework(".NETFramework,Version=v4.5"))
- Asp.Versioning.Abstractions.dll (TargetFramework(".NETStandard,Version=v2.0"))
If I switched the WebAPI project to .NET 4.7.2 and step into class ApiVersionRouteConstraint
, the code has been loaded from the assembly C:\...\.nuget\packages\asp.versioning.webapi7円.1.0\lib\net472\Asp.Versioning.WebApi.dll
. Now it contains the following method call: if(apiVersionParser.TryParse(value, out ApiVersion apiVersion))
. If I step into the method TryParse
, the same netstandard2.0-assembly as above will be loaded and so the code stays the same, as it showed for .NET 4.7.1.
After I compiled again, I now got the following versioning assemblies in the output folder:
- Asp.Versioning.WebApi.ApiExplorer.dll (TargetFramework(".NETFramework,Version=v4.7.2"))
- Asp.Versioning.WebApi.dll (TargetFramework(".NETFramework,Version=v4.7.2"))
- Asp.Versioning.Abstractions.dll (TargetFramework(".NETStandard,Version=v2.0"))
Best regards,
Tom
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
Thanks for all of the information. I'm not sure I can explain why this is happening. It seems like you are doing the right thing. I suspect NuGet isn't doing what's expected, but I can't be sure why. Here's the published, explicitly supported TFMs for Asp.Versioning.WebApi.ApiExplorer
:
There's only 2 explicitly supported TFMs. If you're targeting .NET 4.7.1, then it's not supposed to match net472
and should match net45
. If net45
is matched for Asp.Versioning.WebApi
, then the transitive dependency to Asp.Versioning.Abstractions
is expected to match netstandard1.0
. net45
is neither compatible with netstandard2.0
nor net8.0
.
It seems like we've boiled things down to confirming what is happening, but not why it is happening. The other thing that is strange is that the test suite for ASP.NET Web API specifically targets net452
(the lowest supported by xUnit) and net472
to ensure these differences are covered. I checked what was linked and you can see that net452
linked to the .NET 4.5 version:
whereas net472
links to the .NET Standard 2.0 version:
I definitely see how this can happen if Asp.Versioning.Abstractions
were directly referenced. The only other way I can think of that this can happen would be if you have an explicit assembly binding redirect, which I doubt. Beyond that, I'm stumped.
A few people have reported this issue and I couldn't figure out how this was happening. Honestly, I didn't think people would be still be using such old versions of the .NET Framework. If someone moved up from 4.5, I kind of figured it would probably be 4.7.2+. The Microsoft guidance recommends .NET Framework 4.7.2 for .NET Standard 2.0, but it's possible to go down to .NET 4.6.1, which I've done in the past. I'm wondering if I need to add another TFM or reduce net472
back down to net461
.
The guidance specifically says:
"The versions listed here represent the rules that NuGet uses to determine whether a given .NET Standard library is applicable. While NuGet considers .NET Framework 4.6.1 as supporting .NET Standard 1.5 through 2.0, there are several issues with consuming .NET Standard libraries that were built for those versions from .NET Framework 4.6.1 projects. For .NET Framework projects that need to use such libraries, we recommend that you upgrade the project to target .NET Framework 4.7.2 or higher."
NuGet should be matching what's needed as a transitive dependency, not the direct dependency. It's possible there is a NuGet bug. Ultimately, this how I landed where we are. I'm not sure the best course of action - yet.
Beta Was this translation helpful? Give feedback.
All reactions
-
Hi Chris,
thank you very much for all your detailed explanations, I really appreciate that and it helped me a lot!
As I said, I moved to net472 and everything is working fine now.
Best regards,
Tom
Beta Was this translation helpful? Give feedback.