Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CurrentImplementationApiVersionSelector while versioning by URL path #43

Closed
nomttr opened this issue Nov 3, 2016 · 2 comments
Closed
Assignees

Comments

@nomttr
Copy link

nomttr commented Nov 3, 2016

Is the CurrentImplementationApiVersionSelector working while using URL path versioning? In case of header versioning everything works fine but in case of URL path versionig always the first version(1.0) of the controller is selected even if the another version(2.0) is specified as DefaultApiVersion.

Controllers:

[ApiVersion("1.0", Deprecated = true)]
[Route("api/{version:apiVersion}/Uri")]
public class UriController : ApiController
{
    [HttpGet]
    public string Get()
    {
        return String.Format("Hello from Uri 1.0");
    }
}   

[ApiVersion("2.0")]
[Route("api/{version:apiVersion}/Uri")]
public class Uri2Controller : ApiController
{
    [HttpGet]
    public string Get()
    {
        return String.Format("Hello from Uri 2.0");
    }
}

[ApiVersion("3.0")]
[Route("api/{version:apiVersion}/Uri")]
public class Uri3Controller : ApiController
{
    [HttpGet]
    public string Get()
    {
        return String.Format("Hello from Uri 3.0");
    }
}

WebApiConfig:

        config.AddApiVersioning(options =>
        {
            options.ReportApiVersions = true;
            options.DefaultApiVersion = new ApiVersion(2, 0);
            options.AssumeDefaultVersionWhenUnspecified = true;
            options.ApiVersionSelector = new CurrentImplementationApiVersionSelector(options);
            
        });

        var constraintResolver = new DefaultInlineConstraintResolver()
        {
            ConstraintMap =
            {
                ["apiVersion"] = typeof( ApiVersionRouteConstraint )
            }
        };
        config.MapHttpAttributeRoutes(constraintResolver);

image

@commonsensesoftware
Copy link
Collaborator

There are a couple of different issues going on here so let me try to break up the response into pieces.

URL Path Versioning

When you version your API in the URL path, you can't logically have a default version. The API version is instrinsically part of the path. In your example, you have the route template api/{version:apiVersion}/Uri. If an API version is not specified using this method, the URL becomes api//Uri.

Most service authors that use URL path versioning prefix the API version segment value with the letter 'v'. This would make the route template api/v{version:apiVersion}/Uri. Now, if an API version isn't specified, the path will become api/v/Uri. I think it's pretty obvious that this won't exist.

The method in which the requested API version is internally set is - ever so slightly - different that any other method. All other methods go through the IApiVersionReader to extract the API version from the incoming request message. Trying to understand and parse the URL path in the same manner would be difficult and brittle. Since ASP.NET already provides a structured way to parse route templates, URL path versioning uses a route constraint instead. This allows you to craft your URLs however you want and just indicate which segment the API version should be extracted from.

You can use the route template as you defined it, but there is no way to assume a default because there are no matching candidates.

Default Version Selection

It may not be intrinsically obvious, but the DefaultApiVersion and ApiVersionSelector options are somewhat mutually exclusive. The purpose of the DefaultApiVersion is to simply define an assumed version without hard-coding the value for all fallback cases because every service always has some logical version number, even if it's not formally defined. The IApiVersionSelector is the extension point to select the appropriate API version given a request and the discovered API version model. The infrastructure does all the hard work of collecting the available API versions and you select the best one.

In your case, you elected to use the CurrentImplementationApiVersionSelector and have the following API versions defined:

  • 1.0 (implemented, but deprecated)
  • 2.0
  • 3.0

This selector will always choose 3.0 from these choices. The only time it would ever use the default configured API version of 2.0 is if there were no candidate API versions to select from. For example, if your versions were all pre-release:

  • 1.0-alpha
  • 2.0-beta
  • 3.0-rc

Any version with a status is not considered implemented. Additionally, deprecated does not mean not implemented.

If your intent was to have all clients that don't request a version start on 2.0, then you can just set the DefaultApiVersion. The default value for the ApiVersionSelector is an instance of the DefaultApiVersionSelector, which just always uses the value of the configured DefaultApiVersion.

Faking a Default API Version with URL Paths

There is one way that you can give the impression of an implicitly-version URL path. To enable this, you need to make multiple routes per controller. API versioning stays out of the way of routing and only gets involved with resolving the appropriate controllers and actions.

For example, if your controllers were updated with additional routes:

[ApiVersion( "1.0", Deprecated = true )]
[Route( "api/uri" )]
[Route( "api/{version:apiVersion}/uri" )]
public class UriController : ApiController { /* ommitted */ }

[ApiVersion( "2.0" )]
[Route( "api/uri" )]
[Route( "api/{version:apiVersion}/uri" )]
public class Uri2Controller : ApiController { /* ommitted */ }

[ApiVersion( "3.0" )]
[Route( "api/uri" )]
[Route( "api/{version:apiVersion}/uri" )]
public class Uri3Controller : ApiController { /* ommitted */ }

Your controllers will be resolved using the following URLs:

URL Version Controller
api/uri 3.0 Uri3Controller
api/1/uri 1.0 UriController
api/2/uri 2.0 Uri2Controller
api/3/uri 3.0 Uri3Controller
api/uri?api-version=1.0 1.0 UriController
api/uri?api-version=2.0 2.0 Uri2Controller
api/uri?api-version=3.0 3.0 Uri3Controller

The last 3 entries are there because the default configuration uses an IApiVersionReader that extracts the API version from the api-version query string parameter. If you want to remove this behavior, you can implement a reader that always returns null.

public class IgnoreApiVersionReader : IApiVersionReader
{
    public string Read( HttpRequestMessage request ) => null;
}

You may have noticed that the example used the names UriController, Uri2Controller, and Uri3Controller. In setting up an example that would simulate your scenario, I found an edge case bug. I'll create a new issue for this bug and correlate it back to this thread. For now, using different type names will make this setup work.

I hope that helps.

@nomttr
Copy link
Author

nomttr commented Nov 4, 2016

You well explained the problem with URL path versioning and DefaultApiVersion. Multiple routes on controller makes it bit confusing so i'm considering to choose e.g Custom Header versioning as this one fits better my scenario. Anyway thanks a lot for answer and solution. Cheers!

commonsensesoftware pushed a commit that referenced this issue Nov 6, 2016
…on binding does not correspond to the matched controller. Fixes #44 and resolves #43.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants