2

Is there any way to achieve the following:

  • controllers are versioned with ApiVersionAttribute using major.minor pattern, so 1.1, 2.3 and so on
  • the corresponding routes contain only the major part in the path, for example /v1/WeatherForecast

I have tried adding something like below to the default project created for ASP.NET Core Web API (.NET 6).

WeatherForecastController.cs:

[ApiController]
[ApiVersion("1.1")]
[Route("v1/[controller]")]
public class WeatherForecastController : ControllerBase
{
}

Program.cs

builder.Services.AddApiVersioning(
    options =>
    {
        options.ReportApiVersions = true;
        options.AssumeDefaultVersionWhenUnspecified = true;
    });

And now I cannot call the endpoint /v1/WeatherForecast, I get the error:

{
    "error": {
        "code": "UnsupportedApiVersion",
        "message": "The HTTP resource that matches the request URI 'https://localhost:7170/v1/WeatherForecast' is not supported.",
        "innerError": null
    }
}

Thanks

kanils_
  • 385
  • 1
  • 17
aguyngueran
  • 1,301
  • 10
  • 23
  • As I understand, you want to specify v1 and recive for example 1.1. I'm right? – kanils_ Oct 28 '22 at 13:11
  • Actually in your route u use constant version `[Route("v1/[controller]")]`. At first try to change it with `[Route("v{v:apiVersion}/[controller]")]` it should actually use the version you are mapping. Or if isn't a problem. Also add `[ApiVersion("1.0")]` – kanils_ Oct 28 '22 at 13:14
  • I need to have APIVersion of 1.1 ( so that this is the version shown for instance by Swagger) but have /v1/ only in the path. – aguyngueran Oct 28 '22 at 18:20
  • Try to match version with `v{v:apiVersion}` and add `[ApiVersion("1.0")]`. It should be work. – kanils_ Oct 29 '22 at 09:28
  • The point is, I want api version 1.1 to be available at /v1/, I know it works without any problems with version 1.0. – aguyngueran Oct 29 '22 at 16:13
  • So Y want to map to highest api version(v1.1), when use /v1, yeah? – kanils_ Oct 30 '22 at 11:41
  • Yes, it can be rephrased like you have written. – aguyngueran Oct 30 '22 at 15:53
  • Okey it's more clear. I have solution for your case, will add tomorrow. – kanils_ Oct 30 '22 at 20:16

1 Answers1

2

It sounds like you might be conflating API version with some internal, server notion of version. The API version is part of your contract.

By design, a client cannot ask for 1.0, but get 1.1. This is misleading and confusing to the client. As a server, you cannot pull the carpet out from under the client and expect that it won't break the client (unless you own both sides). Backward-compatible is a fallacy. A server cannot guarantee that adding or removing data attributes doesn't break a client. It might not, but you can't guarantee it.

The reason nothing is matching above is because API Versioning doesn't make any assumptions or reasoning about your route templates. If you want the version in the URL path (not RESTful), that's your choice, but you must use the route constraint in the template as @kanils_ suggested. It should look like:

[ApiController]
[ApiVersion("1.1")]
[Route("v{version:apiVersion}/[controller]")]
public class WeatherForecastController : ControllerBase
{
}

You're free to call the parameter whatever you want, but version is commonly used. This will now enable https://localhost:7170/v1.1/WeatherForecast. There is no definition for 1.0 so v1 will not resolve.

options.AssumeDefaultVersionWhenUnspecified = true isn't going to do what you think it will do (in all likelihood). This is a highly abused feature. It is meant to support grandfathering in existing APIs before they were formally given an official version. This behavior exists for exactly one version. If this feature wasn't supported, then all existing clients that don't know to include a version in the request would break. A route template cannot have a default route parameter value unless it's at the end of the template. This means the template will never match unless the API version is specified in the URL. Versioning by URL segment is the only method that suffers from this problem.

I don't endorse, agree with, or recommend using anything other than explicit API versions, but this can be made to work. You wouldn't return application/xml if a client asks for application/json and expect it to be understood. An API version is no different. Since you're versioning via URL segment, this will require double route registration. You need a neutral route as a catch all and each specific route. This will give clients an explicit URL who want something stable and a floating route for everything else. If a route is a moving target for a client, then it's honestly not any better than no versioning at all IMO.

Start with:

namespace V1
{
  [ApiController]
  [ApiVersion("1.0")]
  [Route("v{version:apiVersion}/[controller]")]
  public class WeatherForecastController : ControllerBase
  {
  }
}

namespace V1_1
{
  [ApiController]
  [ApiVersion("1.1")]
  [Route("[controller]")] // ← 2nd 'floating' route template
  [Route("v{version:apiVersion}/[controller]")]
  public class WeatherForecastController : ControllerBase
  {
  }
}

In the configuration add:

builder.Services.AddApiVersioning(
    options =>
    {
        options.ReportApiVersions = true;
        options.AssumeDefaultVersionWhenUnspecified = true;

        // OPTION 1: explicitly set the default value;
        //           this is the 'assumed' version by default.
        //           the default value is new ApiVersion(1.0)
        //
        // options.DefaultApiVersion = new ApiVersion(1.1);

        // OPTION 2: most people using this setup want to 'float'
        //           an API to the most current version available
        //
        options.ApiVersionReader =
            new CurrentImplementationApiVersionSelector(options);
    });

When a client asks for WeatherForecast, the request is directed to the same location as v1.1/WeatherForecast. This is because:

  • No API version is parsed from the template
  • AssumeDefaultVersionWhenUnspecified = true
  • CurrentImplementationApiVersionSelector resolved 1.1 as the current (e.g. highest) available version for that API

When you eventually add a new version, say 1.2 or 2.0, then you will have to move the floating [Route("[controller]")] route template from the old current controller to the new one. There are probably more convenient ways of managing that, but that approach certainly works.

Since the literal /v1 is unknown or recognized by API Versioning, you can probably combine it with this approach and it will work, but it's a head-scratcher IMHO.

In all of these configurations, you can't (and shouldn't) eliminate the explicit routes, but you can achieve your implicit route goals. Absence of documentation can hide them.

Chris Martinez
  • 3,185
  • 12
  • 28
  • thanks for the explanation. The idea of having 1.0 clients to be served by 1.1 implementation was based on semantic versioning idea, which of course might be challenging to keep. Even though it looked simple in terms of needs, I also see some problems. If the client has not specified the version, how can the server be sure that he knows how to deal with it? What if this is the old server running 1.0 and the client is expecting 1.1? I cannot even detect this on the server side without requiring the client to specify the version. I gonna switch to query string/header version. – aguyngueran Nov 01 '22 at 19:45
  • 1
    You nailed it. You're not the first to thing about or try semantic versioning with APIs, but it's not appropriate IMHO. Another approach that could be used is redirects (e.g. one version to another), but that still doesn't mean the client can handle that. Versioning in the URL also causes problems for HATEOAS. Resources need not be all the same version. How would the server know which version of resource the client needs/wants? It _might_ be different from the current resource's version. – Chris Martinez Nov 02 '22 at 17:35
  • 1
    Hopefully, this answered your question, even if you chose a different path. Don't forget to vote. ;) I'm happy to revise the or provide additional guidance if necessary. – Chris Martinez Nov 02 '22 at 17:38
  • Question for you both, how could client would use 1.1 when server running 1.0? Also client is dependent on server, not server on client, afaik. Client could use 1.1 only if server supports it. – kanils_ Nov 02 '22 at 17:44
  • @ChrisMartinez by design client could request v1 and get v1.1 the latest version. And this is not confusing for client. Why he should think about specific version, when he could take the latest and work with it. And also as a server you should specify versions for backward compatibility and this is not a fallacy. But actually you're right server couldn't guarantee that it could break a client. It should be guaranteed by a dev. – kanils_ Nov 02 '22 at 17:58
  • 1
    @kanils_ an API version is not the same as a binary version. An API version should be thought of as the format of a resource. This is why Fielding says the only truly RESTful way to _version_ a resource is through media type negotiation. This makes complete sense. The _Tolerant Reader_ pattern can be used to deal with changes, particularly additive, but a server cannot _guarantee_ a client will do that. Despite what people may think, a minor version does not imply any backward compatibility whatsoever. _Any_ change to the wire payload is potentially breaking and is why versioning is necessary. – Chris Martinez Nov 02 '22 at 18:49
  • 1
    The client is responsible for asking the server for the _representation_ (e.g. version) it wants and the server is obligated to serve it, if possible. If a client asks for `1.0`, then the server _should_ **only** provide that response. If it can't be served, then the server _should_ return an appropriate response indicating it can't provide what the client asked for. This is no different than `406` and `415` when media type negotiation fails. A version of a resource is a contract and the server must adhere to it. If it didn't, then what's the point of versioning? Just always use the latest. – Chris Martinez Nov 02 '22 at 18:55
  • @ChrisMartinez, your response was helpful to me because it provided a PoV in a space with many options. My takeaway is that I should stick to major vs including a minor, even if you might have frequent breaking changes early on, correct? Use different namespace for each version. You don't seem to agree with versioning in the URL path. It did seem like the most common method, though my approach of searching the internet for stylistically good posts is not very scientific. What do you recommend? – lcj Nov 22 '22 at 12:37
  • @lcj If fine if you want to use a major and minor version. What I'm saying is don't _"lie"_ to the client. The original question wanted `/v1` in the URL, but map to `1.1`. This is a _lie_ from the client's perspective. If that's something you care about doing on the server side, so be it, but it's not part of the public contract. Yes, versioning by URL is common, but it's not RESTful. It violates the _Uniform Interface_ constraint. The URL _is_ the ID. `v1/order/1` and `v2/order/1` are not different orders. They are different _representations_. – Chris Martinez Nov 23 '22 at 16:51
  • @lcj All of Azure versions by query string. Early Azure APIs used to version by header, which is still supported. Versioning by media type is the truly RESTful way to do it. GitHub is an example of an API that does this. Versioning by query string, however, tends to be the most pragmatic and without violating any REST constraints. The API Versioning library supports all of these methods, including combining multiple together. – Chris Martinez Nov 23 '22 at 16:55