Multiple Controller Types with same Route prefix ASP.NET Web Api
Asked Answered
A

3

27

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) {...}
}
Abreaction answered 15/4, 2014 at 21:9 Comment(1)
Although it would still be the same controller one thing you could do is make your controller a partial class with your GETs in one file and your POSTS in another.Sunderland
A
36

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;
    }
}
Arch answered 16/4, 2014 at 1:34 Comment(3)
@YishaiGalatzer: any specific reason you need the custom MethodConstraint here instead of reusing System.Web.Http.Routing.HttpMethodConstraint?Gripe
The point is constraining the route itself. The built-in property only acts in action selection which runs too late.Arch
In case anyone is looking at this answer and is having issues naming the route and then building a route using Url.Link(...). There is a similar answer here: #40893137 The main differences I saw were the additional interfaces and possible the use of HttpMethodConstraint with an array of HttpMethods. Whichever the case, the other answer enables route naming to still work FYI.Christlike
M
4

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.

Marsipobranch answered 15/4, 2014 at 21:26 Comment(1)
Thanks, that's not the problem. The problem is selecting the correct controller type based on GET or POSTAbreaction
G
1

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) {...}
}
Grethel answered 17/4, 2020 at 9:48 Comment(1)
getting error --Partial declarations must not specify different base classesReceptor

© 2022 - 2024 — McMap. All rights reserved.