Why does adding versioning to a webApi project removes number from the controller path name?
Replication steps :
services.AddApiVersioning(config =>
{
config.AssumeDefaultVersionWhenUnspecified = true;
config.DefaultApiVersion = new ApiVersion(1, 0);
config.ReportApiVersions = true;
config.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader("version"),
new HeaderApiVersionReader("x-version"));
config.UseApiBehavior = false;
});
Step 5 will return a 404 not found error. However if you call https://localhost:x/WeatherForecast. It will work.
So why does adding versioning, change the url path?
It's not entirely clear if you are using <= 5.0
or 6.0
+.
The reason this behavior happens is because the only logical way to group controllers together is by their names. A controller name, therefore, becomes very important. This is problematic in code because two or more controllers in the same namespace cannot have the same type name. The assumption and long-defined convention has been to allow ASP.NET to remove the Controller
suffix and then remove any remaining numbers. This allows ValuesController
, Values2Controller
and Values3Controller
to all map to the logical controller name Values
by default. In most cases, that's probably want someone wants. If API Versioning doesn't do this, then there is no way to collate all API versions together for an API.
Contrary to popular belief, route templates are not considered for grouping controllers (e.g. APIs). There is too much ambiguity as to how a template can map to code. Take the simplest example of two different versions of the same API with different route templates: V1 = values/{id}
, V2 = values/{id:int}
. These are semantically equivalent, but not the same. API Versioning does not try to understand what the route template means nor compare their equivalence. It can easily get a lot more complicated; especially, for overlapping route templates. For example, should order/{oid}/customer/{cid}
be part of the Orders API or the Customer API? Only the service author knows for sure.
In the 5.0
release, a regression was accidentally introduced due to an over-optimization. The controller name is used in two places: the actual name of the controller and the name used to group controllers. It seems reasonable they'd be the same and why normalize (e.g. trim suffixes) more than necessary? It seemed like a good idea, but it caused unexpected behavior - such as this one. There are also legitimate reasons to have a number in the name of a controller; for example, S3Controller
.
In library versions <= 5.0
, developers had no control over the behavior of how names were normalized. In 5.1
and 6.0
+, this is now exposed via the IControllerNameConvention
service, which has two methods: one for normalizing the controller name and one for normalizing the group name. The following implementations are provided out of the box as properties on ControllerNameConvention
:
Default
: The default, out-of-the-box conventionsOriginal
: The original names without any normalization (could result in the wrong behavior)Grouped
: The group name is normalized, but the controller name is unmodifiedIf none of those work for you, then you can create your own custom convention. In 5.1
this is wired up via ApiVersioningOptions.ControllerNameConvention
, while in 6.0
+ IControllerNameConvention
is a transient service in the DI container.
There are two ways you can workaround the problem using the current version you are leveraging:
If you omit using the [controller]
token, the routing problem will be resolved; for example, api/weatherforecast
. You appear to have already discovered this.
The controller name is derived from a convention, even without API Versioning. It was understood this behavior could be a problem so API Versioning provides a way to explicit set it with the ControllerNameAttribute.
[ControllerName("WeatherForecast")]
[Route("api/[controller]")] // ← expands to 'api/WeatherForecast'
public class WeatherForecast2Controller : ControllerBase { }
This will solve the routing issues, but it will not fix the controller name issue. That should only matter if you are planning on documenting your API with OpenAPI (formerly Swagger). For example, S3Controller
will simply show up as S
, even though the route might be api/s3
.