-
Notifications
You must be signed in to change notification settings - Fork 714
-
I've got a large existing API which I want to add versioning to. So I set AssumeDefaultVersionWhenUnspecified = true
and DefaultApiVersion = new ApiVersion(1, 0)
and add [ApiVersion(1)]
to all my controllers and everything is great - my clients can continue as they are requesting api/foo
etc, or start requesting api/v1/foo
.
Now I want to release a version 2 of the API with breaking changes to a couple of methods. I create new versions of those methods at same routes but with [ApiVersion(2)]
attributes. My clients update their code and set their API version to be 2 and now all their api/v2/foo
requests at those routes get served by the new methods and any client still requesting api/v1/foo
or just api/foo
continue to be served by the old method as before.
However, now all requests to api/v2/bar
and any other existing route get an UnsupportedApiVersion
error! Obviously we wouldn't want a long trail of ApiVersion attributes building up all over the place as new versions are introduced, so how do I make sure requests fall back to the highest available version not exceeding their requested version?
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 3 comments 2 replies
-
I think I understand what you are asking. I gather that you have something like:
[ApiVersion(1.0)] [ApiController] [Route("api/v{version:apiVersion}/[controller]")] public class BarController : ControllerBase { [HttpGet] public IActionResult Get() => Ok(); }
and now you want to want to have it be supported in 2.0
. There is no implicit (e.g. fallback) way of making that happen; it's by design.
This is an implementation detail on the server side more than anything. API versions must be explicit. That's the only way that provable decisions can be made. How would the framework know that you want this it be auto-magically included? Your intention might have been to completely remove the API in 2.0
.
One of the best things you can do is define a sound versioning policy upfront - say N-2
versions. That's one way of keeping the number of versions from growing uncontrollably. API Versioning only cares about IApiVersionProvider
. Attributes just happen to be one way that is realized. You can extend those attributes or even roll your own. You can also use conventions and/or create your own conventions. For example, the out-of-the-box VersionByNamespaceConvention
derives and applies an API version to a controller from its .NET namespace; no attribution required.
What some people are looking for is what I call Symmetrical Versioning. Typically, it's more flexible to allow your APIs to evolve with their own, independent versions over time, but some people want every release to have the same API versions across the board, even if there is no change. In other words, they want the versions to be symmetrical. If you're building a broad service or platform, from the client's perspective that can make sense. The implementation as to how you achieve that can vary. The most straight forward and easiest to rationalize about is to explicitly version each API. It is also possible to automate that process by examining a set of known API versions from configuration or by what already exists and then fill in the blanks. I provided a skeleton of how that might be achieved in 900.
Keep in mind that you still have to account for APIs that might be completely sunset and make sure you don't accidentally pick up new APIs in older versions. This is the type of use case the API Explorer extensions can provide for verification in tests that have nothing to do with OpenAPI (formerly Swagger).
Hopefully, that gives you some ideas to get started and the potential risks of filling in the blanks to make all of the versions symmetrical.
Beta Was this translation helpful? Give feedback.
All reactions
-
Thanks for your helpful response. I don't wish to give a generous open source developer a hard time but I must say that I find it really surprising that this kind of fallback to previous isn't a built in option, or indeed the default behavior. Without it, do we really have to decorate every method with an attribute for every version in the version history then? Either that or require that clients specify the version they require endpoint by endpoint, rather than for the API as a whole, and in that case you're left with some pretty strange swagger documentation, where the doc pages for each version only include the endpoints changed in that version, so you can't see the whole thing in one go.
I tried the customisation suggested in #900 (comment), but as far as I can see this only has the same effect that decorating each controller with its version history would do, which means that the individual methods fall back to the first not the latest previous version, i.e. in the following a request to /api/v3/test/a
would be served by A1()
not A2()
.
[ApiVersion(1)]
[ApiVersion(2)]
[ApiVersion(3)]
public class TestController
{
[HttpGet, Route("a")]
public ActionResult A1(ApiVersion version)
{
return Ok($"Method: A1, ApiVersion: {version}");
}
[HttpGet, Route("b")]
public ActionResult B1(ApiVersion version)
{
return Ok($"Method: B1, ApiVersion: {version}");
}
[HttpGet, Route("a")]
[ApiVersion(2)]
public ActionResult A2(ApiVersion version)
{
return Ok($"Method: A2, ApiVersion: {version}");
}
[HttpGet, Route("b")]
[ApiVersion(3)]
public ActionResult B3(ApiVersion version)
{
return Ok($"Method: B3, ApiVersion: {version}");
}
}
Beta Was this translation helpful? Give feedback.
All reactions
-
There's a bit to unpack here.
First and foremost, I'll repeat that things pretty much have to work this way by default. How can the library distinguish between a version that has been removed and a version that is supposed to be auto-magically filled in? The only convention is its absence. It would be even more awkward if you had to provide some explicit configuration to indicate the end of an API or its version.
While you can version by specific endpoint, it will yield some likely unexpected results. Collation is done at the API-level, not the endpoint level. This is another scenario that can't really be reliably done another way. Grouping by route template, although it seems natural, cannot be achieved in a consistently reliable way. Consider test/{id}
and test/{id:int}
. They're are semantically equivalent route templates, but they are not the same. API Versioning makes no attempt to parse or understand the route templates. This gets more complex when you have test/a
and test/b
. These could be endpoints mapped on the TestController
, but they could also be the AController
and BController
mapped with a test
prefix. There's no way to know that. Most APIs consist of many endpoints. Your example would be the Test API with the A and B endpoints. This is also how it would appear in OpenAPI. When you use controllers, it - therefore - makes sense to use the controller name as the name of the API and is also the consistent value that can be used for collation.
While interleaving versions on a single controller is a supported scenario, it's probably not the best option for server-side code maintenance. The problem it was meant to solve is "What if I have just one, small change in a new version? Do I really need a whole other controller?". If you're adding several changes per version, then the implementation will become quite complex to understand. Interleaving also requires that you have to touch the code belonging to earlier versions, which means there is a chance you can affect existing versions. Separating APIs and their versions into separate types provides the cleanest implementation and is the least likely to change anything about the existing implementation.
...do we really have to decorate every method with an attribute for every version...
API versions do have to be explicit, but how you apply them is up to you. There are numerous methods provided out-of-the-box, which can be extended. You can even provide your own methods, which some people have done to support ranges. Conventions enable a scenario so that you don't have to apply any attributes at all. A common approach is to copy and paste a folder from an existing API version to a new one as the baseline, rename the folder, and then make the necessary changes - if any. If the API version is derived from the .NET namespace, which typically derives from the folder name, then adding a new version is as simple as copying a folder and removing it is as simple as deleting a folder.
Services (e.g. APIs) should be allowed to evolve independent so it's perfectly natural that an Orders and Customers API might have asymmetric versions over time. #900 was only meant to serve as an example and baseline of where to start. It's likely not a complete solution. As you've noticed, if you interleave and try to collate by endpoint, you will get unexpected results. While routing can be defined down to the action/endpoint level, collation is not. All controller actions are expected to have an implied relationship. Two unrelated actions on the same controller is technically possible, I suppose, but that's strange.
Trying to explain how the routing system works is quite involved, but suffice it to say, it's a directed graph and the branch of nodes to be considered as candidates begins with an API version. This is one of many requirements that necessitate explicit API versions (and it also makes it much faster). I hate to be pedantic, but fallback is probably the wrong word. What you're asking is "Please make 'Test' API 1.0 automatically map to 2.0 and 3.0 if they are defined". There's a whole other litany of challenges in trying to create a general solution. What should happen if an API wasn't even introduced until 2.0? Should it automatically implement 1.0? What happens if an API explicitly maps to 1.0 and 3.0; should it automatically implement 2.0? This isn't a simple problem. By attribute or convention, things are explicit and API Versioning knows what to do. Inferring your intention otherwise is unclear and application-specific. You want your API versions symmetrical across all of your APIs, but there is someone else out there who is perfectly fine with asymmetrical versions. Asymmetrical versions happen organically, not because it's some forced dogma. If there was a reliable way to have symmetrical versions, I would probably add it as a feature. The fact that there are so many edge cases is why I have not. I'm not fully convinced the risks offset the benefits. There's a real chance that an unintended API is supported and goes unnoticed. Such a mistake is not easily undone as it breaks the public contract if you fix it. I haven't ruled it out in the future, but I also haven't found sufficient guardrails to make sure people don't go off the tracks.
How things appear in the Swagger UI is a whole other matter. Instead of fanning out and filling in blanks, you could be doing that in OpenAPI. The group in the Swagger UI is just a name, but it makes sense it's the API version. It's within the realm of possibility to show all APIs in each version, but their actual API version might be different; for example, when you view 3.0, it might provide documentation for v1/test/a
. Personally, I loathe versioning by URL segment because it's not RESTful; however, since you have elected that approach, it is possible to group by logical name - say Test
and have all of the different versions listed in a single OpenAPI document. This is possible because the URLs are distinct. It is technically possible to pivot and list APIs this way using other versioning methods, but the Swagger UI doesn't support that because it would require multiple different OpenAPI documents. The URLs in an OpenAPI document must be distinct. You would likely need a custom UI for that, which I've seen, but most people are not interesting in putting forth that much effort.
Beta Was this translation helpful? Give feedback.
All reactions
-
We have a similar need and just came across this. @commonsensesoftware really appreciate the detailed write up! What if we don't want to use convention api version?
API versions do have to be explicit, but how you apply them is up to you. There are numerous methods provided out-of-the-box, which can be extended. You can even provide your own methods, which some people have done to support ranges
would you be able to elaborate a bit more on how to explicitly declare the API versions without using the 'ApiVersionAttribute' (or the IApiVersionProvider to be more specific) annotation?
Is there a place we would have access to the apiversion specified by the client, and then run our logic (based on the client specified version) to select a fallback endpoint? e.g., when client specifies a preview version then it should fallback to the latest preview <= the specified preview; when client specifies a stable version then it should fallback to the latest stable <= the specified stable (a stable should never fallback to a preview and vice-versa). I was thinking about using a middleware that's very early in the pipeline to override the apiversion to a version that's declared / implemented. Do you think this would work or there are other more elegant approaches?
Beta Was this translation helpful? Give feedback.
All reactions
-
There's no getting around not having the IApiVersionProvider
; that's a core abstraction. How it's implemented and used can vary greatly. If you don't want to use attributes, you can use conventions. You can also extend the built-in attributes or you can even use your own attributes, but those will have to be connected somehow, which is usually through a convention.
It is possible to access request information via the IApiVersioningFeature. While it's possible, mutating it is generally a bad idea. First, no rules or other logic may have run yet. There is no guarantee when it gets processed nor whether it may be processed again later in the pipeline. Keep in mind that is possible for the client to submit multiple API versions at once. If you jump into the middle of things, you become responsible for a lot of things. An API version is effectively considered selected once the RequestedApiVersion
has been set. Remember, the client could send a malformed RawRequestedApiVersion
which will be non-null, but RequestedApiVersion
will be null
since it failed to parse. Finally, the biggest challenge is knowing what's possible. As a library, API Versioning can't make many assumptions, but as the service owner, you can. If the API versions are not symmetrical across all APIs, it's difficult or impossible to reason about API versions. The selected API version is part of the route selection process and won't necessarily be set until a matching route is selected too. Changing/setting the selected API version will influence routing when set early enough.
I'll reiterate that there is no such thing as fallback or backward compatible. If you change the incoming API version to anything other than the API version that the client requested, you could be pulling the carpet out from underneath them and they don't even know it. The sanest thing you can do is return 308
with Location
and redirect the client to the desired endpoint and version. If your goal is simply managing the server-side implementation, there's numerous methods of refactoring possible to minimize that without causing chaos for clients.
Beta Was this translation helpful? Give feedback.