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.06k stars 703 forks source link

Url Helpers not working with the api version in URL #426

Closed bartosz-jarmuz closed 5 years ago

bartosz-jarmuz commented 5 years ago

Hello, I have an WebApi2 API controller which looks more or less like that:

[RoutePrefix("api/v{version:apiVersion}/programs")]
public class ProgramsController : ApiController
{
    [HttpGet, Route("{telemetryKey}/versions/latest", Name = "LatestVersionInfoRoute")]
    public async Task<LatestVersionResponse> GetLatestVersionInfo(Guid telemetryKey)
    {
        //do stuff 
    }
}

unfortunately, the URL Helpers returns an empty string for this route @Url.HttpRouteUrl("LatestVersionInfoRoute", new { telemetryKey = Guid.NewGuid()})

The route I expect is api/v1/programs/[THE GUID]/versions/latest

Other than that the URL helper problem, everything works OK, i.e. swagger documentation shows proper route (and the route works) - both with namespace convention and with [ApiVersion] attribute.

When I change the 'variable' apiVersion part of the prefix to a constant 'v1', then the routing works OK.

The Register method in WebApiConfig.cs class contains the following code

 public static void Register(HttpConfiguration config)
        {
            var constraintResolver = new DefaultInlineConstraintResolver()
            {
                ConstraintMap =
                {
                    ["apiVersion"] = typeof( ApiVersionRouteConstraint )
                }
            };
            config.AddApiVersioning(opt =>
            {
                opt.Conventions.Add(new VersionByNamespaceConvention());
            });
            config.MapHttpAttributeRoutes(constraintResolver);
            config.Routes.MapHttpRoute(name: "DefaultApi", routeTemplate: 
"api/v{version:apiVersion}/{controller}/{id}", defaults: new { id = RouteParameter.Optional });

Is there a way to use the UrlHelpers? If not, what is the alternative, if I want to call the webapi endpoints from within views in my MVC app?

commonsensesoftware commented 5 years ago

Let's make sure we're on the same page first. I see information for both convention-based routing and attribute-based routing. Which one are you using or do you want to use? It also appears that you're using MVC with Razor. They can be used together, I just want to make sure I understand they are both being used and what part of the question maps to what.

When you version by URL segment, that route parameter number be supplied. I don't see the {version} parameter in your action method and it's template doesn't seem to match the convention-based route. Regardless, I would expect @Url.HttpRouteUrl to require the {version} parameter; something like: { new { version = "1.0", telemetryKey = Guid.NewGuid() }.

How you determine the value for version is up to you. The UrlHelper appears to be being used from the MVC side so as a client, there isn't really a magic way to know what the API version should be. Telemetry APIs tend to be version-neutral. If that case applies to you, then consider making your API version neutral.

bartosz-jarmuz commented 5 years ago

Hello, Yeah, I tried both approaches (originally just the namespacec convention, then with the ApiVersion attribute, then both together). It worked the same way in all cases.

I am indeed using MVC with Razor - basically, parts of my webapp communicate with the API controllers rather than mvc controllers. So yes, it is being used from the client.

My action method does not have the version parameter, because I assume it is enough if its set on the controller, by controller being in the proper namespace. However, now that I think about it, indeed I am being stupid not passing the version to the UrlHelper in any way.

I had a quick try and it seems to work when I add the version parameter. Thank you very much. Is there any way to default to a certain/latest version if parameter is not specified?

One thing - could you quickly elaborate on 'Telemetry APIs tend to be version-neutral'?

Thanks again

commonsensesoftware commented 5 years ago

Is there any way to default to a certain/latest version if parameter is not specified?

Using the URL segment method, not really. There isn't a way to default a value in the middle of a path. If you simply always want the latest, you can achieve your goal with floating, double routes. First, update your configuration like so:

public static void Register(HttpConfiguration config)
{
  config.AddApiVersioning(opt =>
  {
    opt.AssumeDefaultVersionWhenUnspecified = true;
    opt.ApiVersionSelector = new CurrentImplementationApiVersionSelector(opt);
    opt.Conventions.Add(new VersionByNamespaceConvention());
  });
  // omitted for brevity

Now update your controller to:

[RoutePrefix("api")]
public class ProgramsController : ApiController
{
    // api/programs/{key}/versions/latest
    // api/v{version}/programs/{key}/versions/latest
    [HttpGet]
    [Route("v{version:apiVersion}/programs/{telemetryKey}/versions/latest")]
    [Route("programs/{telemetryKey}/versions/latest", Name = "LatestVersionInfoRoute")]
    public async Task<LatestVersionResponse> GetLatestVersionInfo(Guid telemetryKey)
    {
        //do stuff 
    }
}

How this works:

  1. By using AssumeDefaultVersionWhenUnspecified = true a client can make a request without providing an API version (this was really meant for backward compatibility)
  2. By changing the default IApiVersionSelector to CurrentImplementationApiVersionSelector, the current (e.g. highest) available API version will be selected for a matching route
  3. By defining multiple routes, your action can now match without having to specify the API version route parameter in the path
    1. I call this a floating route because when the current version changes, this route definition needs to be moved to a new action, possibly on a new controller

I'm not a fan of this technique or the URL segment method in general, but it will work. Since you seem to own both the client and server side of the API, some of the potential issues are less worrisome.

...could you quickly elaborate on 'Telemetry APIs tend to be version-neutral'?

Telemetry and other health check type APIs are common. Many of these APIs do not vary between versions and/or have no versioning at all. These APIs can be version-neutral, meaning that they accept any and all API versions, including none at all. Consider the following:

[ApiVersionNeutral]
[RoutePrefix("api/ping")]
public class PingController : ApiController
{
  [Route]
  public IHttpActionResult Get() => Ok();
}

This API always responds to api/ping no matter the API version. This type of API might be used simply to verify that the endpoint can be reached and never changes between API versions. This behavior tends to be common with telemetry APIs too. If that's your scenario, API version-neutral might work for you here.

Kind in mind that a given route cannot be both versioned and version-neutral at the same time. When you opt into this behavior, the expectation is that the API doesn't have specific needs about a particular API version. If you want to know the API version requested, you can do something like this for the URL segment method:

[ApiVersionNeutral]
[RoutePrefix("api")]
public class PingController : ApiController
{
  [Route("ping")]
  [Route("v{version:apiVersion}/ping")]
  public IHttpActionResult Get(ApiVersion apiVersion) => Ok();
}

When GET api/v1/ping is requested, the apiVersion will be 1.0. When GET api/ping is requested, the apiVersion will be null or ApiVersioningOptions.DefaultApiVersion depending on your configuration.

I hope that helps.

bartosz-jarmuz commented 5 years ago

Thank you very much, for both parts of the answer - especially that the second one does not even concern your great library. I will close this issue. I'll just add that I really admire the usefulness of this plugin, as well as great documentation and support. Cheers!