I have two actions on the same controller, with identical routes, but separate HttpMethod
requirements (POST
vs DELETE
).
[AllowAnonymous]
public class TestController : ApiController
{
[Route("~/api/test")]
[HttpDelete]
public IHttpActionResult Endpoint1()
{
return this.Ok("endpoint1");
}
[Route("~/api/test")]
[HttpPost]
public IHttpActionResult Endpoint2()
{
return this.Ok("endpoint2");
}
}
This is all fine -- both endpoints work when switching from DELETE
to POST
.
E.g.
DELETE /api/test = endpoint1
POST /api/test = endpoint2
If I separate the actions into separate controllers, it does not work anymore:
[AllowAnonymous]
public class TestController : ApiController
{
[Route("~/api/test")]
[HttpDelete]
public IHttpActionResult Endpoint1()
{
return this.Ok("endpoint1");
}
}
[AllowAnonymous]
public class TestController2 : ApiController
{
[Route("~/api/test")]
[HttpPost]
public IHttpActionResult Endpoint2()
{
return this.Ok("endpoint2");
}
}
E.g.
DELETE /api/test = endpoint1
POST /api/test = { "Message": "The requested resource does not support http method 'POST'." }
Is this expected from the framework?
EDIT: The exact WebAPI package version is: 5.2.3
Web API 2.0 does not a allow a route to match on two different controllers. This is solved in MVC 6 (which is Web API combined framework).
First like @woogy and you say, it is not a very common pattern, so most users should just not go here (or move to MVC 6 when it goes RTM).
The root cause is that the route actually matches, the verb defined an an IActionHttpMethodProvider
does not constraint the route from matching, and it matches on multiple controllers thus failing.
You can however define a constraint on the route, and as a side effect get a more succinct API.
This will constraint the route to only match the predefined verb, so it wouldn't match the other controller.
public class VerbConstraint : IHttpRouteConstraint
{
private HttpMethod _method;
public VerbConstraint(HttpMethod method)
{
_method = method;
}
public bool Match(HttpRequestMessage request,
IHttpRoute route,
string parameterName,
IDictionary<string, object> values,
HttpRouteDirection routeDirection)
{
// Note - we only want to constraint on the outgoing path
if (routeDirection == HttpRouteDirection.UriGeneration ||
request.Method == _method)
{
return true;
}
return false;
}
}
public abstract class VerbRouteAttribute : RouteFactoryAttribute, IActionHttpMethodProvider
{
private string _template;
private HttpMethod _method;
public VerbRouteAttribute(string template, string verb)
: base(template)
{
_method = new HttpMethod(verb);
}
public Collection<HttpMethod> HttpMethods
{
get
{
var methods = new Collection<HttpMethod>();
methods.Add(_method);
return methods;
}
}
public override IDictionary<string, object> Constraints
{
get
{
var constraints = new HttpRouteValueDictionary();
constraints.Add("verb", new VerbConstraint(_method));
return constraints;
}
}
}
This class merges 3 things
1. The route attribute with the route template
2. Applies a verb route constraint to the route
3. Specifies the action method selector, so the rest of the system (like help page) recognizes it just like the [HttpPost]
/ [HttpDelete]
public class PostRouteAttribute : VerbRouteAttribute
{
public PostRouteAttribute(string template) : base(template, "POST")
{
}
}
public class DeleteRouteAttribute : VerbRouteAttribute
{
public DeleteRouteAttribute(string template) : base(template, "DELETE")
{
}
}
These as you can tell are pretty trivial, and just make the use of these attributes in your code a lot smoother.
[AllowAnonymous]
public class TestController : ApiController
{
[DeleteRoute("api/test")]
public IHttpActionResult Endpoint1()
{
return this.Ok("endpoint1");
}
}
[AllowAnonymous]
public class TestController2 : ApiController
{
[PostRoute("api/test")]
public IHttpActionResult Endpoint2()
{
return this.Ok("endpoint2");
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With