Question

Is it possible to separate GETs and POSTs into separate API Controller types and accessing them using the same Route Prefix?

Here are my controllers:

[RoutePrefix("api/Books")]
public class BooksWriteController : EventStoreApiController
{
    [Route("")]
    public void Post([FromBody] CommandWrapper commandWrapper){...}
}

[RoutePrefix("api/Books")]
public class BooksReadController : MongoDbApiController
{
    [Route("")]
    public Book[] Get() {...}

    [Route("{id:int}")]
    public Book Get(int id) {...}
}
Was it helpful?

Solution

Web API (1.x-2.x) does not support multiple attribute routes with the same path on different controllers. The result is a 404, because all the route matches more than one controller and at that point Web API will consider the result ambiguous.

Note that MVC Core does support this scenario note: MVC Core serves as both MVC & Web API.

If you choose to use Web API 2.11 (or newer) you can create a route constraint for the http method per controller and use it instead of the built in Route Attribute. The sample below shows that you can use RoutePrefix or directly Routes (like kmacdonald's answer).

using System.Collections.Generic;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Routing;

public class BooksWriteController : ApiController
{
    [PostRoute("api/Books")]
    public void Post() { }
}

[RoutePrefix("api/books")]
public class BooksReadController : ApiController
{
    [GetRoute]
    public void Get() { }

    [GetRoute("{id:int}")]
    public void Get(int id) { }
}

These two classes simplify the use of the constraint route attribute

class GetRouteAttribute : MethodConstraintedRouteAttribute
{
    public GetRouteAttribute(string template) : base(template ?? "", HttpMethod.Get) { }
}

class PostRouteAttribute : MethodConstraintedRouteAttribute
{
    public PostRouteAttribute(string template) : base(template ?? "", HttpMethod.Post) { }
}

This class allows adding constraints to the route generated

class MethodConstraintedRouteAttribute : RouteFactoryAttribute
{
    public MethodConstraintedRouteAttribute(string template, HttpMethod method)
        : base(template)
    {
        Method = method;
    }

    public HttpMethod Method
    {
        get;
        private set;
    }

    public override IDictionary<string, object> Constraints
    {
        get
        {
            var constraints = new HttpRouteValueDictionary();
            constraints.Add("method", new MethodConstraint(Method));
            return constraints;
        }
    }
}

This is just a standard route constraint, nit: you may want to cache the constraints object to reduce allocations.

class MethodConstraint : IHttpRouteConstraint
{
    public HttpMethod Method { get; private set; }

    public MethodConstraint(HttpMethod method)
    {
        Method = method;
    }

    public bool Match(HttpRequestMessage request,
                      IHttpRoute route,
                      string parameterName,
                      IDictionary<string, object> values,
                      HttpRouteDirection routeDirection)
    {
        return request.Method == Method;
    }
}

OTHER TIPS

You don't always need to specify a RoutePrefix on your controller. you could just put the route directly on the web methods:

public class BooksWriteController : EventStoreApiController
{
    [Route("api/Books")]
    public void Post([FromBody] CommandWrapper commandWrapper){...}
}

public class BooksReadController : MongoDbApiController
{
    [Route("api/Books")]
    public TaskTypeInfo[] Get() {...}


    [Route("api/Books/{id:int}")]
    public TaskTypeInfo Get(int id) {...}
}

However, I would imagine that your RoutePrefix would work fine on both controllers. I think the attribute RoutePrefix is used in conjunction with the Route attribute which actually defines the route. This means that as long as you don't have any conflicting routes (this is a biggie) you should be fine.

Take advantage of partial classes. Partial Classes and Methods - C# MSDN

Create two files: BooksController.Write.cs and BooksController.Read.cs Only RoutePrefix one file, since they are the same class it will give you an error saying you are prefixing the same class two times.

Both files will compile as a single class (because it is a single class, but split in different files).

// File BooksController.Write.cs
[RoutePrefix("api/Books")]
public partial class BooksController : EventStoreApiController
{
    [Route("")]
    public void Post([FromBody] CommandWrapper commandWrapper){...}
}

// File BooksController.Read.cs
public partial class BooksController : MongoDbApiController
{
    [Route("")]
    public Book[] Get() {...}


    [Route("{id:int}")]
    public Book Get(int id) {...}
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top