Aspnet-api-versioning: api-supported-version not pick all versions when controller is split into multiple classes

Created on 3 Jul 2020  路  8Comments  路  Source: microsoft/aspnet-api-versioning

Problem

if multiple [ApiVersion] Atttibutes are applied to a common controller class, api-supported-version shows all available versions. If there are different controller classes, for each version, api-supported-version only show the version currently passed in. it does not discover all version.

Code

This code works fine.

[ApiController]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/WeatherForecast")]
public class WeatherForecastController : BaseApiController
{
....
image

However this does not

`
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/WeatherForecast")]
public class WeatherForecastController : BaseApiController
{}

[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/WeatherForecast")]
public class WeatherForecastV2Controller : BaseApiController
{}`
image

Runtime

using .net core 3.1 with Microsoft.AspNetCore.Mvc.Versioning version 4.1.1 default project

answered asp.net core question

Most helpful comment

Ah ... you've hit a very subtle convention that I never liked very much. Unfortunately, there aren't a lot of great solutions. This is an easy fix, but let me explain what's happening (and hopefully for the benefit of others).

The default _controller name_ convention in Web API and ASP.NET Core has the form: <name>Controller. This means that the _names_ of the WeatherForecastController and WeatherForecastV2Controller are WeatherForecast and WeatherForecastV2. The way that APIs are grouped together is by their names because, frankly, there isn't anything else to key off of. No .NET language will allow you to define the same type name in the same scope for obvious reasons. This leads to the scenario where you end up having something like WeatherForecastV2 to make the code behind the scenes happy. Unfortunately, now the controller names do not match.

There are several ways to address this problem:

  1. Organize the controllers into different namespaces. (e.g. Controllers.V1.WeatherForecast and Controllers.V2.WeatherForecast)
  2. Use the ControllerNameAttribute, which allows you to explicitly specify the name (ex: [ControllerName("WeatherForecast")])

I came to realize that this issue will likely surface many places so thought about whether it's possible to support this behavior by _convention_ - and I did. The convention is very, very simple: <name><number>Controller. This meant that there's no pattern matching, regular expressions, etc. Simply, if the controller name ends with a number, trim it off. In the many years of working with ASP.NET, I've never seen a controller name (or any service for that matter) whose intentional name ended in a number so this seemed a reasonable solution. Furthermore, if there ever was a scenario where the name ends in a number, it can also be forced via ControllerNameAttribute (which seems weird, but that's the only way to be explicit about things).

So why didn't it work for you? You appended V2 to your controller - as in _WeatherForecastV2Controller_. If your controller was simply named _WeatherForecast2Controller_, then the number would be trimmed off giving it the intended name of _WeatherForecast_. In some ways, this is analogous to the same problem as the [controller] token in the route template.

I hope that clarifies things, unblocks, and has you on your way.

All 8 comments

Ah ... you've hit a very subtle convention that I never liked very much. Unfortunately, there aren't a lot of great solutions. This is an easy fix, but let me explain what's happening (and hopefully for the benefit of others).

The default _controller name_ convention in Web API and ASP.NET Core has the form: <name>Controller. This means that the _names_ of the WeatherForecastController and WeatherForecastV2Controller are WeatherForecast and WeatherForecastV2. The way that APIs are grouped together is by their names because, frankly, there isn't anything else to key off of. No .NET language will allow you to define the same type name in the same scope for obvious reasons. This leads to the scenario where you end up having something like WeatherForecastV2 to make the code behind the scenes happy. Unfortunately, now the controller names do not match.

There are several ways to address this problem:

  1. Organize the controllers into different namespaces. (e.g. Controllers.V1.WeatherForecast and Controllers.V2.WeatherForecast)
  2. Use the ControllerNameAttribute, which allows you to explicitly specify the name (ex: [ControllerName("WeatherForecast")])

I came to realize that this issue will likely surface many places so thought about whether it's possible to support this behavior by _convention_ - and I did. The convention is very, very simple: <name><number>Controller. This meant that there's no pattern matching, regular expressions, etc. Simply, if the controller name ends with a number, trim it off. In the many years of working with ASP.NET, I've never seen a controller name (or any service for that matter) whose intentional name ended in a number so this seemed a reasonable solution. Furthermore, if there ever was a scenario where the name ends in a number, it can also be forced via ControllerNameAttribute (which seems weird, but that's the only way to be explicit about things).

So why didn't it work for you? You appended V2 to your controller - as in _WeatherForecastV2Controller_. If your controller was simply named _WeatherForecast2Controller_, then the number would be trimmed off giving it the intended name of _WeatherForecast_. In some ways, this is analogous to the same problem as the [controller] token in the route template.

I hope that clarifies things, unblocks, and has you on your way.

Ah ... you've hit a very subtle convention that I never liked very much. Unfortunately, there aren't a lot of great solutions. This is an easy fix, but let me explain what's happening (and hopefully for the benefit of others).

The default _controller name_ convention in Web API and ASP.NET Core has the form: <name>Controller. This means that the _names_ of the WeatherForecastController and WeatherForecastV2Controller are WeatherForecast and WeatherForecastV2. The way that APIs are grouped together is by their names because, frankly, there isn't anything else to key off of. No .NET language will allow you to define the same type name in the same scope for obvious reasons. This leads to the scenario where you end up having something like WeatherForecastV2 to make the code behind the scenes happy. Unfortunately, now the controller names do not match.

There are several ways to address this problem:

  1. Organize the controllers into different namespaces. (e.g. Controllers.V1.WeatherForecast and Controllers.V2.WeatherForecast)
  2. Use the ControllerNameAttribute, which allows you to explicitly specify the name (ex: [ControllerName("WeatherForecast")])

I came to realize that this issue will likely surface many places so thought about whether it's possible to support this behavior by _convention_ - and I did. The convention is very, very simple: <name><number>Controller. This meant that there's no pattern matching, regular expressions, etc. Simply, if the controller name ends with a number, trim it off. In the many years of working with ASP.NET, I've never seen a controller name (or any service for that matter) whose intentional name ended in a number so this seemed a reasonable solution. Furthermore, if there ever was a scenario where the name ends in a number, it can also be forced via ControllerNameAttribute (which seems weird, but that's the only way to be explicit about things).

So why didn't it work for you? You appended V2 to your controller - as in _WeatherForecastV2Controller_. If your controller was simply named _WeatherForecast2Controller_, then the number would be trimmed off giving it the intended name of _WeatherForecast_. In some ways, this is analogous to the same problem as the [controller] token in the route template.

I hope that clarifies things, unblocks, and has you on your way.

I realized the issue and resolution shortly. But i will call it a workaround rather. The way i worked out is having it on partial class and having the different method to have diff action name with maptoapi attribute. This works but reflects a design problem. I have not tried the ending in number part if that works out.

Leaving aside the implementation, the framework does allow me to register the routes in that way using attributes . In that case, approach to calculated versioned routes is wrong. There could be 2 totally diff controller names, having similar routes. Either that versioning calculation should be allowed, or this routing via attribute in such case be blocked so that it is more consistent either way. Later would not be possible, so in principle it should rather be on the basis of actual routes that are registered. I am wondering how come this issue not came up with others 馃え

I originally considered route templates years ago, but it isn't feasible - unfortunately. First, you aren't required to use RouteAttribute on your controller or its actions. That would mean that convention-based routes wouldn't work and/or I'd have to construct the route template from the convention. Every route template can be applied only at the action level too (weird, but allowed). Another key reason it will not work, however, has to do with the interpretation of the route template itself. For example, the route templates values/{id} in 1.0 and values/{id:int} in 2.0 are semantically identical over the wire, even though the the templates are not literally the same. Parsing, stripping, or otherwise trying to understand the route template is not a path I wanted to go down. The final and main reason the template cannot be used is that there is no way to match it to a request. Yes, in the strictest sense, it is technically possible to group controllers by their template, but what's the template for an incoming request? The way a controller is matched is through the [controller] route value (even without versioning), which holds the descriptor containing the name. A core design goal was to not change the way routing works. The API version is only used as a discriminator for multiple action matches.

Ultimately, API Versioning does not really care about attributes. Using attributes, such as ApiVersionAttribute, is just one way versions can be expressed and applied. They can be applied through _conventions_ too. In addition, you can roll your own custom attributes if they implement IApiVersionProvider.

Very few people have ever run into a problem with the naming convention. This is almost certainly because in the majority of real world applications, you would not have controller implementations side-by-side in the same namespace, in the same codebase. Most people would likely use different namespaces per version or separate assemblies. I only illustrate the naming convention in examples for simplicity and to demonstrate it's possible Regardless, the most sensible approach to match an API to a controller is to have similar names. It would be very rare to change the names between versions. Should someone _really_ want a different type name for the controller behind the scenes, that is an implementation detail and the ControllerNameAttribute addresses a way to provide the name you want to group by.

I hope that helps clear things up.

I originally considered route templates years ago, but it isn't feasible - unfortunately. First, you aren't required to use RouteAttribute on your controller or its actions. That would mean that convention-based routes wouldn't work and/or I'd have to construct the route template from the convention. Every route template can be applied only at the action level too (weird, but allowed). Another key reason it will not work, however, has to do with the interpretation of the route template itself. For example, the route templates values/{id} in 1.0 and values/{id:int} in 2.0 are semantically identical over the wire, even though the the templates are not literally the same. Parsing, stripping, or otherwise trying to understand the route template is not a path I wanted to go down. The final and main reason the template cannot be used is that there is no way to match it to a request. Yes, in the strictest sense, it is technically possible to group controllers by their template, but what's the template for an incoming request? The way a controller is matched is through the [controller] route value (even without versioning), which holds the descriptor containing the name. A core design goal was to not change the way routing works. The API version is only used as a discriminator for multiple action matches.

Ultimately, API Versioning does not really care about attributes. Using attributes, such as ApiVersionAttribute, is just one way versions can be expressed and applied. They can be applied through _conventions_ too. In addition, you can roll your own custom attributes if they implement IApiVersionProvider.

Very few people have ever run into a problem with the naming convention. This is almost certainly because in the majority of real world applications, you would not have controller implementations side-by-side in the same namespace, in the same codebase. Most people would likely use different namespaces per version or separate assemblies. I only illustrate the naming convention in examples for simplicity and to demonstrate it's possible Regardless, the most sensible approach to match an API to a controller is to have similar names. It would be very rare to change the names between versions. Should someone _really_ want a different type name for the controller behind the scenes, that is an implementation detail and the ControllerNameAttribute addresses a way to provide the name you want to group by.

I hope that helps clear things up.

Yes, Chris, That actually helps. Tried both the ways, diff namespace and controller name and it works correctly. I do anticipate some edge cases, but seems not that much real world scenarios.

Just to confirm if i missed something, is it documented somewhere. Any reference would be helpful.
Thanks anyways, your prompt assistance was really great. Have actually waited for months in several other issues before first response.

Hi @commonsensesoftware any pointers on the documentation. Otherwise we can close the issue.

Looks like I don't have anything in the wiki for this. I'll leave this open until I put up a page for it. I'm sure it will be helpful to others at some point.

I've finally added a section to the wiki for Controller Conventions: Naming. That should bring this issue to a close. Hopefully, this will make it more clear for others in the future. Thanks.

super, Danke.

Was this page helpful?
0 / 5 - 0 ratings