I'm looking into adding versioning to an existing API we have. We are embedding the version in the URL.
The requirement for the versioning is that it should be easy to add a new version, but not everything changes and we don't want to go through all controllers and add new version attributes when we get a new version. Is there any way to tell Microsoft.AspNetCore.Mvc.Versioning that a particular method should be available regardless of versions?
I have tried decorating my method with both with the version:apiVersion and with just a basic route.
[Route("v{version:apiVersion}/customers/{customerId}/estates/{estateId:int}/meters/{meterNumber}-{installationId}/consumption/detail")
[Route("customers/{customerId}/estates/{estateId:int}/meters/{meterNumber}-{installationId}/consumption/detail")]
My configuration is as such (the version number 3 is just for testing purposes):
services.AddApiVersioning(config =>
{
config.DefaultApiVersion = new ApiVersion(3, 0);
config.AssumeDefaultVersionWhenUnspecified = true;
config.ReportApiVersions = true;
});
When calling the endpoint with this configuration I get "The HTTP resource that matches the request URI 'http://{{host}}/api/customers/{customer}/estates/{estate}/meters/{meter}-{installation}/consumption/detail' is not supported
As soon as I add the [ApiVersion("3.0")] attribute everything works. But my thought was that if I currently run on version 2, but changes to other parts of the API warrant a new version and the default version of the API, I don't want to have to go this controller and "bump" the version. It should just continue to respond, unless I specify something specific.
If I change the AssumeDefaultVersionWhenUnspecified to false, I get an "An API version is required, but was not specified." But I expected that it would just grab the route without the version?
I have read about the limitations here: https://github.com/Microsoft/aspnet-api-versioning/wiki/Known-Limitations#url-path-segment-routing-with-a-default-api-version But it doesn't seem to work.
Setting AssumeDefaultVersionWhenUnspecified = true
is a highly abused feature. This is only meant to facilitate backward compatibility. Once you opt into API Versioning, all API controllers have some implicit or explicit API version. Assuming a version provides a mechanism to handle the "original", unnamed version that would otherwise break existing clients that don't know to include an API version in requests.
"Is there any way to tell Microsoft.AspNetCore.Mvc.Versioning that a particular method should be available regardless of versions?"
Yes, but I'm not entirely sure that is what you're looking for. An API can be version-neutral, which means it will accept any and all API versions, including none at all. This is useful for certain types of APIs such as a /ping
health check or DELETE
, which typically do not change over time. Such an API can be decorated with [ApiVersionNeutral]
. This can be for all APIs on a controller or a specific action. Note that once you choose this path, "there can be only one."
There is not a complete picture of your setup. It appears that you are adding API versioning to an existing set of APIs. One of the primary purposes of DefaultApiVersion
is to set what the default API version should be when no other information is available. If you applied no API versions whatsoever - for example, using attributes, then this would be the implicit API version of all APIs. This prevents you from having to retrofit all of your existing controllers and code. What's less obvious is that once you apply any explicit API version, then the implicit rule is ignored. This ensures that implicit versions can be sunset. The default version often does not matter. It's simply a way for you to indicate what the initial API version originally was. It's most relevant when grandfathering in existing APIs.
Given your configuration:
// implicitly 3.0 because DefaultApiVersion = 3.0 and there
// are no explicit versions from attributes or conventions
[Route("customers/{customerId}/estates/{estateId:int}/meters/{meterNumber}-{installationId}/consumption/detail")]
public class CustomerController : ControllerBase
{
}
Grandfathered controller with implicit versioning
// explicitly 2.0 due to attributes; DefaultApiVersion is ignored
[ApiVersion("2.0")]
[Route("v{version:apiVersion}/customers/{customerId}/estates/{estateId:int}/meters/{meterNumber}-{installationId}/consumption/detail")
[Route("customers/{customerId}/estates/{estateId:int}/meters/{meterNumber}-{installationId}/consumption/detail")]
public class CustomerController : ControllerBase
{
}
Controller with explicit versioning
With the exception of supporting the backward compatible, original API version, all APIs versions are explicit and discrete; this is by design. Fuzzy matching will not work in a predictable way so it's important to have exact matches.
You seem to be describing API version interleaving - e.g. multiple API versions implemented on a single controller implementation. I presume it might look like:
[ApiVersion("2.0")]
[ApiVersion("3.0")]
[Route("v{version:apiVersion}/customers/{customerId}/estates/{estateId:int}/meters/{meterNumber}-{installationId}/consumption/detail")
[Route("customers/{customerId}/estates/{estateId:int}/meters/{meterNumber}-{installationId}/consumption/detail")]
public class CustomerController : ControllerBase
{
// implicitly maps to 2.0 from the controller, but does not
// match 3.0 because there is an explicit 3.0 mapping
[HttpGet]
public IActionResult Get(
string customerId,
int estateId,
string meterNumber,
string installationId ) => Ok();
// explicitly maps to 3.0 only
[MapToApiVersion("3.0")]
[HttpGet]
public IActionResult GetV3(
string customerId,
int estateId,
string meterNumber,
string installationId ) => Ok();
// explicitly maps to 3.0 only
// a 2.0 request produces 405
[MapToApiVersion("3.0")]
[HttpPut]
public IActionResult Put(
string customerId,
int estateId,
string meterNumber,
string installationId
[FromBody] ComsumptionDetail detail ) => Ok();
}
Controller with version interleaving
The DefaultApiVersion
can only apply a single, implicit API version. This means that no interleaving with it can ever occur. Once you start explicitly adding versions, you must also start including the implicit version because there is no other way to indicate that the implicit version should no longer be matched.
In terms of route definitions and API version mapping, there are several methods that can be used to minimize code churn. The VersionByNamespaceConvention is a built-in convention you can apply which will derive an API version from the .NET namespace of the controller type. This can make it really easy to add, remove, and organize controllers with their API versions. They can also use interleaving because the mapping is additive, but that may be confusing to maintain. You can define your own conventions as well.
Double-route registration is a consequence of versioning by URL segment. It is the least RESTful method of versioning because it violates the Uniform Interface constraint. No other method of versioning has this issue. I advise against floating the default route and API version mapping because you are very likely to break clients at some point. If you're only using double-routes for backward compatibility of your original API version, then you should be fine. When you eventually create new controllers for future API versions, you'll only have one route with the API version in the template.