6

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

tris
  • 1,780
  • 3
  • 18
  • 28
  • 1. It will be a good idea to specify the version of Web API that you are using. 2. "~/" in the routes is not necessary, "api/test" works fine – Yishai Galatzer Jun 02 '15 at 03:30
  • Why not place all http verbs related to a controller... in one controller? – JTW Jun 02 '15 at 03:30
  • @YishaiGalatzer: 1. updated to specify version. 2. Regardless of ~ or not, the same issue occurs. – tris Jun 02 '15 at 03:33
  • @woogy -- I do for most scenarios -- but the goal of this post is to display an issue found when not grouping them by controller, but rather action they are doing. – tris Jun 02 '15 at 03:35
  • yes - the ~/ thing is just stylistic – Yishai Galatzer Jun 02 '15 at 03:46
  • looks like it is by design, I actually get a 500 when I tried your repro with a clear explanation. I do have a workaround I will post as an answer though. – Yishai Galatzer Jun 02 '15 at 04:26

2 Answers2

7

What is going on

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).

What can I do about it

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.

Let us get started

Define a verb constraint

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;
    }
}

Define an abstract base class for a new attribute

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]

Now let us define implementations

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.

Finally let us apply the new attributes (and remove the method attribute)

[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");
    }
}
Yishai Galatzer
  • 8,791
  • 2
  • 32
  • 41
  • Thanks, @Yishai for the extremely detailed explanation. This does work - but will happily wait for MVC 6 :) – tris Jun 03 '15 at 11:39
0

[HttpDelete("DeleteEmployee/{id}")] public async Task DeleteEmployee(Guid id)

Dipen Lama
  • 147
  • 1
  • 5