Reputation: 39
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
Reputation: 4368
There are several problems here.
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"));
}
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
Controller
suffix is dropped[vV]#
IControllerNameConvention
if you really want to[ControllerName("Agreements")]
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.
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.
If you apply all of these changes, it should work as you expect.
Upvotes: 0