user1258028
user1258028

Reputation: 39

How to make .NET framework 4.8 versioning work with Web API

I am working on a .NET framework 4.8 Web API project and I'm trying to enable versioning on the REST API endpoints. All the endpoints are broken and I am unable to figure out what is wrong. Can anyone help?

namespace Testproject1.Controllers
{
    using Testproject.Controllers.Models;
    using Asp.Versioning;
    using System.Web.Http;

    [ApiVersion(1.0)]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class AgreementsController: ApiController
    {
        [Route("{accountId}")]
        public IHttpActionResult Get(string accountId) =>
            Ok(new Agreement(GetType().FullName, accountId, "1"));
    }
}

namespace Testproject1.Controllers
{
    using Testproject.Controllers.Models;
    using Asp.Versioning;
    using System.Web.Http;

    [ApiVersion(2.0)]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class AgreementsControllerv2 : ApiController
    {
        [Route("{accountId}")]
        public IHttpActionResult Get(string accountId) =>
            Ok(new Agreement(GetType().FullName, accountId, "2"));
    }
}

namespace Testproject1.Controllers.V1
{
    using Testproject.Controllers.Models;
    using Asp.Versioning;
    using System.Web.Http;

    [ApiVersion(2.0)]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class OrdersController : ApiController
    {
        // GET ~/v1/orders/{accountId}
        [Route("{accountId}")]
        public IHttpActionResult Get(string accountId) =>
            Ok(new Order(GetType().FullName, accountId, "1"));
    }
}

And my Startup looks like this:

var configuration = new HttpConfiguration();
var httpServer = new HttpServer(configuration);
configuration.MapHttpAttributeRoutes();

configuration.AddApiVersioning(
            options =>
            {
                options.ReportApiVersions = true;
                options.DefaultApiVersion = new ApiVersion(1, 0);
                options.AssumeDefaultVersionWhenUnspecified = true;
                options.ApiVersionReader = new UrlSegmentApiVersionReader();
            });

configuration.Routes.MapHttpRoute(
            name: "VersionedApi",
            routeTemplate: "api/v{version:apiVersion}/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional, version = RouteParameter.Optional });

builder.UseWebApi(httpServer);

I am getting following response

Version: 1
http://localhost:9999/api/v1.0/agreements/1

{
    "Message": "No HTTP resource was found that matches the request URI 'http://localhost:9999/api/v2.0/agreements/1'.",
    "MessageDetail": "No action was found on the controller 'Agreements' that matches the request."
}

Version 2: http://localhost:9999/api/v2.0/agreements/1

{
    "Message": "No HTTP resource was found that matches the request URI 'http://localhost:9999/api/v1.0/agreements/1'.",
    "MessageDetail": "No action was found on the controller 'Agreements' that matches the request."
}

Orders: http://localhost:9999/api/v2.0/orders/1

{
    "code": "ApiVersionUnspecified",
    "traceId": "776cf174-79ab-4386-a856-2f385f5f51ca",
    "type": "https://docs.api-versioning.org/problems#unspecified",
    "title": "Unspecified API version",
    "status": 400,
    "detail": "An API version is required, but was not specified."
}

Upvotes: 0

Views: 1387

Answers (1)

Chris Martinez
Chris Martinez

Reputation: 4368

There are several problems here.

Issue 1

You're using Route twice instead of RoutePrefix and Route. The correct controller implementation should be:

[ApiVersion(1.0)]
[RoutePrefix("api/v{version:apiVersion}/[controller]")]
public class AgreementsController: ApiController
{
    [Route("{accountId}")]
    public IHttpActionResult Get(string accountId) =>
        Ok(new Agreement(GetType().FullName, accountId, "1"));
}

Issue 2

Controllers are collated by name. The default naming convention assumes suffix Controller. This means that AgreementsController will be collated as Agreements and AgreementsControllerv2 will remain AgreementsControllerv2. You expect these to be group together so that the Agreements API has 1.0 and 2.0. This means one of the following needs to happen:

  • AgreementsControllerv2 needs to be named AgreementsV2Controller
    • The Controller suffix is dropped
    • API Versioning will assume a default suffix of [vV]#
      • This can be changed via IControllerNameConvention if you really want to
  • Apply [ControllerName("Agreements")]
    • This can also be done for all controllers if you want to keep it consistent

Issue 3

You are mixing Convention Routing with Direct Routing (aka Attribute Routing). This is a partially supported scenario. I strongly advise against mixing and matching them because it's easy to make a mistake and it's hard to troubleshoot the issue. This will also only work if all controllers for an API use the same type of routing.

Attribute Routing is typically the easiest to rationalize about. It will also align better if/when you ever decide to transition to ASP.NET Core.

Issue 4

Since you are versioning by URL segment, neither

options.AssumeDefaultVersionWhenUnspecified = true

nor

defaults: new { id = RouteParameter.Optional, version = RouteParameter.Optional });

are going to do what you think they will. You cannot have an optional route parameter in the middle of a template. This is no different than if you had order/{id}/items. {id} must always be specified. Any and all optional parameters must be specified at the of the template. You can have optional parameters with default values starting from left to right at the first optional parameter, assuming there are no gaps.

AssumeDefaultVersionWhenUnspecified is commonly misused. This feature is intended for backward compatibility, where a client wouldn't know to specify the version and they would otherwise break. If you truly need that, then you need to use Double Route Registration. This is yet another consequence of versioning by URL segment, which isn't RESTful (but I understand you may not be able to change it).

This would require you to have:

[ApiVersion(1.0)]
[RoutePrefix("api/[controller]")] // equivalent to /api/v1/agreements
[RoutePrefix("api/v{version:apiVersion}/[controller]")]
public class AgreementsController: ApiController
{
    [Route("{accountId}")]
    public IHttpActionResult Get(string accountId) =>
        Ok(new Agreement(GetType().FullName, accountId, "1"));
}

for every controller versioned this way. Other methods of versioning do not have this issue; they simply omit the header, query parameter, or media type.

Conclusion

If you apply all of these changes, it should work as you expect.

Upvotes: 0

Related Questions