0
votes

I have ASP.NET Web Api 2 application which already has controllers. Now, we have new controllers that need to be added but with prefix (v10)

/api/products/1          // Old controller
/api/v1/proucts/1        // the new controller

I tried to version the API with ApiVersion attribute:

[ControllerName("Products")]
[ApiVersion("1.0")]
[RoutePrefix("api/v10/[controller]")]
public class ProductsV1Controller : ApiController
{
...
}

And the old controller is:

public class ProductsController : ApiController
{
...
}

The routing without the version is still working and accessing the old controller, but when I call this routing:

api/v10/products/1

It returns 404 Page not found. The same get method is written in both controllers just for testing purposes.

In my Startup config:

httpConfig.MapHttpAttributeRoutes();

httpConfig.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional },
            constraints: null);

does anyone know how to configure the routing to navigate correctly?

1
try to change[RoutePrefix("api/v10/[controller]")] to [Route("api/v{v:apiVersion}/products")] and call it like : api/v1.0/products/Mohammed Sajid
Unfortunately, it didn't work! The same issueSamy Sammour
Are you using .netcore or .net framework?Mohammed Sajid
.Net framework. I did manage to fix it. I just had to add the Route annotation to the method itself after I give in the controller explicitly the routing prefix. Apparently, the routing prefix requires the routing annotation in each method in order for this to work. But thanks, your tip was helpful to find the solutionSamy Sammour
Here is again the link where the had to add even an empty Route Annotation to the get method just to mark the method: docs.microsoft.com/en-us/aspnet/web-api/overview/…Samy Sammour

1 Answers

0
votes

@Sajid was mostly correct. When you version by URL segment, you need to use the API version route constraint. There's a couple of issues at play. It looks like you are migrating from an unversioned API to a versioned API, which is a supported scenario. The example values you gave are incongruent. It looks like you are going from v1 to v10. The values are irrelevant, but realize that your existing API does have some logical name, even if you never previously assigned it a value.

Issue 1

You didn't specify your setup, but it should be something like:

var constraintResolver = new DefaultInlineConstraintResolver()
{
  ConstraintMap = { ["apiVersion"] = typeof( ApiVersionRouteConstraint ) }
};
configuration.MapHttpAttributeRoutes( constraintResolver );
configuration.AddApiVersioning(
  options =>
  {
    // required because the original API doesn't have a version in the URL
    options.AssumeDefaultVersionWhenUnspecified = true;

    // this is already the default value, but you might need to change it.
    // this is the value that will be 'assumed' by the original, unversioned
    // API. it is addressable by /api/products. if you meant to start at 1.0,
    // then you can use a lower value such as 0.9
    options.DefaultApiVersion = new ApiVersion(1, 0);
  });

Issue 2

The token [controller] is not supported in Web API. This is a concept, along with [action], were introduced in ASP.NET Core routing.

You didn't fully elaborate on how you solved the issue, but your changes almost certainly highlight how mixing styles can be confusing. It's unclear if the API was matched by an attribute route or a convention-based route.

Issue 3

Your route template does not include the API Version route constraint (see Issue 1 for registration). This is required so API Versioning knows how to extract the API Version from the URL. API Versioning does not do magic string parsing with regular expressions, etc.

The template should be: [RoutePrefix("api/v{version:apiVersion}/products")] as mentioned by @Sajid. Keep in might that the literal v is not part of an API Version, but is common in the URL segment versioning method. version is the name of the route parameter, but can be any value you want. apiVersion is the key of the registered route constraint, but you can change it in the template if you also change it in the registration (see Issue 1).

Issue 4

The annotated API version is 1.0, but the route template seems to imply that you meant 10.0. You can use any value you want, but they need to be congruent. If you do mean to use 1.0 don't forget to the change the options.DefaultApiVersion or you'll get a runtime exception because the original API and your new API will be considered duplicates as they have the same API version. There is no concept of no API Version once you opt in.

For example:

// ~/api/v10/products
[ApiVersion("10.0")]
[RoutePrefix("api/v{version:apiVersion}/products")]
public class ProductsV1Controller : ApiController { }

Issue 5

I strongly advise against mixing routing styles. It is supported, but it can be confusing to troubleshoot and for maintainers. I would choose either Direct Routing (aka Attribute Routing) or Convention-Based Routing. There are edge cases where mixing them will not work, but they are rare.

This means you should choose one of the following:

// setup (see Issue 1)
configuration.MapHttpAttributeRoutes( constraintResolver );

// old controller
[RoutePrefix("api/products")]
public class ProductsController : ApiController { }

// new controller; ControllerNameAttribute isn't need because it's not used
[ApiVersion("10.0")]
[RoutePrefix("api/v{version:apiVersion}/products")]
public class ProductsV1Controller : ApiController { }
// setup
httpConfig.Routes.MapHttpRoute(
  name: "VersionedApi",
  routeTemplate: "api/v{apiVersion}/{controller}/{id}",
  defaults: new { id = RouteParameter.Optional },
  constraints: new { apiVersion = new ApiVersionRouteConstraint() });

httpConfig.Routes.MapHttpRoute(
  name: "DefaultApi",
  routeTemplate: "api/{controller}/{id}",
  defaults: new { id = RouteParameter.Optional },
  constraints: null);

// old controller
public class ProductsController : ApiController { }

// new controller
[ApiVersion("10.0")]
[ControllerName("Products")] // required because the convention-based name is "ProductsV1"
public class ProductsV1Controller : ApiController { }

Warning: You can mix these styles, but troubleshooting will become more difficult. You need extensive test coverage to ensure things are correct. One-off manual testing may yield false positives.


I hope that helps