Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Substitution of {version:apiVersion} fails when using controller conventions #1079

Unanswered
FunThom asked this question in Q&A
Discussion options

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

You must be logged in to vote

Replies: 1 comment 9 replies

Comment options

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.

Repro-1079.zip

Some additional notes:

  • The default, configured API version is 1.0. Setting options.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} in order/{id}/item/{line}.
    • It appeared you might have been trying that with the conventional route, but it doesn't match the actions shared
You must be logged in to vote
9 replies
Comment options

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:

grafik

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

Comment options

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:

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:

bool TryParse( ReadOnlySpan<char> text, [MaybeNullWhen( false )] out ApiVersion apiVersion );

but for .NET Standard 1.0 it looks like:

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:

  1. Move up and baseline to .NET 4.7.2
  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.

Comment options

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

Comment options

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:

image

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:

image

whereas net472 links to the .NET Standard 2.0 version:

image

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.

Comment options

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Q&A
Labels
None yet

AltStyle によって変換されたページ (->オリジナル) /