dotnet / aspnet-api-versioning

Provides a set of libraries which add service API versioning to ASP.NET Web API, OData with ASP.NET Web API, and ASP.NET Core.
MIT License
3.03k stars 702 forks source link

UnsupportedApiVersion from route without ApiVersioning #174

Closed thiagomajesk closed 7 years ago

thiagomajesk commented 7 years ago

Hi, I'm having trouble with routes that doesn't use the ApiVersioning attribute. Considering the controllers bellow:

[ApiVersion("1.0"), RequireHttps]
[Route("api/v{version:apiVersion}/[controller]")]
public class FooController : Controller
{
    /* [...] */
}

I've configured it like this: services.AddApiVersioning(o => o.AssumeDefaultVersionWhenUnspecified = true);. And then, accessing a bar/create route in the BarController that does not use the ApiVersioning attribute, I get UnsupportedApiVersion error. I have assumed that the ApiVersioning attribute would only override the route for the controller I specified, but that doesn't seem to be true.

commonsensesoftware commented 7 years ago

API versioning doesn't really get involved with routing. A design goal has always been to not force developers to learn anything new about routing in ASP.NET. The [ApiVersion] attribute is purely metadata. You can also use conventions.

Once you opt into API versioning, there is no such thing as an unversioned API anymore. Controllers that don't have any attributes still (and conceptually always did) have an API version. The assumed value in these cases is ApiVersioningOptions.DefaultAPiVersion, which has the initial value of 1.0. The primary reason for this behavior is so that existing APIs (e.g. controllers) can be grandfathered in without change.

There an option to use [ApiVersionNeutral], but you should know that this doesn't mean unversioned. This means exactly the opposite - I accept any API version. This is the closest behavior you can get to unversioned.

The ApiVersioningOptions.AssumeDefaultVersionWhenUnspecified allows for a client to make a request without specifying an API version. When this scenario happens, the API version used will be based on the behavior of the configured ApiVersioningOptions.ApiVersionSelector. The default configuration always selects ApiVersioningOptions.DefaultAPiVersion. All of the other out-of-the-box implementations also fallback to this value when other selections do not yield a result.

You may be thinking you have configured all of this, so why isn't bar/create working. This has everything to do with routing. Since you are routing by using a URL segment, that segment must be specified in order for ASP.NET to route to it. There is no other way, that I know of, that enables you to have a default value in the middle of a URL (though you can at the trail end). I've outlined this issue in detail in the Known Limitations - URL Path Segment Routing with a Default API Version topic.

In short, the only way that you can make bar/create work is to have two route templates (including the rest of your existing configuration):

[Route( "[controller]") ]
[Route( "v{version:apiVersion}/[controller]" )]
public class BarController : Controller
{
  [HttpPost( "create" )]
  public IActionResult Create( Bar bar ) => Status( 201 );
}

If the BarController doesn't care which API version is requested, then you can use:

[ApiVersionNeutral]
[Route( "[controller]") ]
public class BarController : Controller
{
  [HttpPost( "create" )]
  public IActionResult Create( Bar bar ) => Status( 201 );
}

I hope that helps. Let me know if you have more questions.

thiagomajesk commented 7 years ago

@commonsensesoftware

Once you opt into API versioning, there is no such thing as an unversioned API anymore.

Its' weird that this is the default behaviour though. I still think this should be opt-in per controller. Applying a version to one restfull controller from 20 "common" ones, requires me to opt-out one by one using ApiVersionNeutral.

So, would you say that the prefered way to do that without messing up with routes (both templates and query strings) would be using the versioning through media type?

Update: Just notice, btw that even when using the ApiVersionNeutral in one controller, I get: The HTTP resource that matches the request URI does not support the API version '1.0'.

commonsensesoftware commented 7 years ago

Sorry for the delayed response, for some reason I was thinking that I was waiting for a response from you. Whoops! @_@

It might seem bizarre that versioning is explicit once you opt in, but really it's not. First, you always had an API version, you just never formally gave it a name or value. You might have called it "Current Version" or simply "The API", but it was an API version.

The reason that this is the default behavior is because if you're opting in an existing codebase most service authors want the majority of their services versioned. This could result in a lot of touch points. In fact, it's the opposite if the scenario you are facing. Another issue if you have service implementations from controllers that exist in external libraries. It may seem strange, but it's a real scenario that I've seen.

To make the transition easier, there are a couple of provided options:

Combining these options enables you give a name to the initial version of services you have. By assuming a default API version when no other API version is specified, that enables clients to continue calling the service without an API version. A client might also specify an API version, but it's unlikely since they never knew what the value was. The service is unaware of this behavior. The infrastructure resolves the expected version and hands it to the controller. From the controller's perspective, the client did provide an API version.

This behavior can only ever exist for exactly one API version, which I call the default or initial API version. The inverse of this configuration has a number of inconsistencies. For example, if a service could be unversioned, what does it mean if a client sends an API version? There is really no expressed intention. In the described configuration, if no API version or an API version matching the default is provided, then things work. If the client provides an unsupported API version, then things fail - predictably. It's also a valid use case to accept any API version, which where the concept of API version-neutral comes in. Because of the variability, this behavior is something you must explicitly opt into.

This also gives you a better transition story if you move one of these unversioned services to become versioned. All you need to do is add the additional, explicit API version to your set. Saying that you are version-neutral (or versionless) has different implications. It's not so easy to transition from any API version to a specific set of API versions. In fact, the infrastructure won't allow it. It's ambiguous from a controller-to-route URL perspective. I had this exact thing happen with a team where they thought it was simpler to make a service version-neutral because they didn't care about the initial version. When they introduced a new API with changes, they switched back to having specific API versions. To their surprise, they actually broke a client and caused a live site issue. This happened because the client was providing a well-formed API version, but not one that the service ever supported. It was just a value understood and forward by the client incorrectly.

This lends to the same rationale why API version ranges are not intrinsically supported. The options are either accept any API version or an explicit, finite set. The goal was to honor the principle of least astomishment. I weighted the principle on the client more than the service side. It occassionally leads to service author misunderstanding or confusion, but hopefully it helps them avoid a sinkhole they didn't know was there.

Unfortunately, some of this falls down when you version by URL segment. This purely on how URLs and routing work. As I mentioned, there is no way to have a default value in the middle of a URL. The default configuration of the framework allows specifying the API version by query string or URL segment. This means that just allowing the assumption of a default API version should work for you. For example:

services.AddApiVersioning( options => options.AssumeDefaultVersionWhenUnspecified = true );
[Route( "[controller]") ]
public class BarController : Controller
{
  // GET bar/create (assumed to be version 1.0)
  // GET bar/create?api-version=1.0
  [HttpPost( "create" )]
  public IActionResult Create( Bar bar ) => Status( 201 );
}

It's worth noting that if you use the approach and then you ever add another version, they must be all explicitly specified. You can have either one implicitly applied API version or all explicit API versions. This makes things slightly easier to reason about. In addition, it's the only way to sunset a deprecated API. Imagine that you moved on to only have 2.0 and 3.0. How do you get rid of 1.0? Explicit configuration always supersedes.

There are strong options about the preferred way of applying API versioning. I'm personally not a fan of the URL segment approach because the URLs are not stable. API versioning by media type might be the purest, but it's not the simplest for clients. The best balance, in my opinion, is the query string method. Query strings never influence routing so the URLs are stable over time. Query strings are much easier for consumption by clients; especially, JavaScript clients. Beyond that, I think the rest is largely preference and dogma.

I see that you may have been having an issue trying to get an API version-neutral route working. If you weren't able to resolve your issue, let me know. Any code snippets or even a full repro will help provide fast answers.

Thanks

thiagomajesk commented 7 years ago

@commonsensesoftware No problem at all. Thanks for the feedback and thanks for the explanation :)

About the issue regrading version-neutral routes: I'll try to make a repro of this problem and post it here, it may take a few days though, since I'm about to release my web app.

commonsensesoftware commented 7 years ago

Sounds like you're on your way. Should you have a repro or something else you'd like to share, feel free to do so. Sample contributions from the community help a lot with ironing out edge cases. Should something pop up or you find the issue isn't resolved, we can reopen this issue. Thanks.