Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WebAPI 2.0 Post and Delete Routes

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

like image 494
tris Avatar asked Jun 02 '15 03:06

tris


1 Answers

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");
    }
}
like image 192
Yishai Galatzer Avatar answered Sep 21 '22 16:09

Yishai Galatzer