Multiple controllers with same URL routes but different HTTP methods

13,933

UPDATE

Based on your comments, updated question and the answer provided here

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

Desired result can be achieved via custom route constraints for the HTTP method applied to controller actions.

On inspection of the default Http{Verb} attributes ie [HttpGet], [HttpPost] and the RouteAttribute, which by the way are sealed, I realized that their functionality can be combine into one class similar to how they are implemented in Asp.Net-Core.

The following is for GET and POST, but it shouldn't be difficult to create constraints for the other HTTP methods PUT, DELETE...etc to be applied to the controllers.

class HttpGetAttribute : MethodConstraintedRouteAttribute {
    public HttpGetAttribute(string template) : base(template, HttpMethod.Get) { }
}

class HttpPostAttribute : MethodConstraintedRouteAttribute {
    public HttpPostAttribute(string template) : base(template, HttpMethod.Post) { }
}

The important class is the route factory and the constraint itself. The framework already has base classes that take care of most of the route factory work and also a HttpMethodConstraint so it is just a matter of applying the desired routing functionality.

class MethodConstraintedRouteAttribute 
    : RouteFactoryAttribute, IActionHttpMethodProvider, IHttpRouteInfoProvider {
    public MethodConstraintedRouteAttribute(string template, HttpMethod method)
        : base(template) {
        HttpMethods = new Collection<HttpMethod>(){
            method
        };
    }

    public Collection<HttpMethod> HttpMethods { get; private set; }

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

So given the following controller with the custom route constraints applied...

[RoutePrefix("api/some-resources")]
public class CreationController : ApiController {
    [HttpPost("")]
    public IHttpActionResult CreateResource(CreateData input) {
        return Ok();
    }
}

[RoutePrefix("api/some-resources")]
public class DisplayController : ApiController {
    [HttpGet("")]
    public IHttpActionResult ListAllResources() {
        return Ok();
    }

    [HttpGet("{publicKey:guid}")]
    public IHttpActionResult ShowSingleResource(Guid publicKey) {
        return Ok();
    }
}

Did an in-memory unit test to confirm functionality and it worked.

[TestClass]
public class WebApiRouteTests {
    [TestMethod]
    public async Task Multiple_controllers_with_same_URL_routes_but_different_HTTP_methods() {
        var config = new HttpConfiguration();
        config.MapHttpAttributeRoutes();
        var errorHandler = config.Services.GetExceptionHandler();

        var handlerMock = new Mock<IExceptionHandler>();
        handlerMock
            .Setup(m => m.HandleAsync(It.IsAny<ExceptionHandlerContext>(), It.IsAny<System.Threading.CancellationToken>()))
            .Callback<ExceptionHandlerContext, CancellationToken>((context, token) => {
                var innerException = context.ExceptionContext.Exception;

                Assert.Fail(innerException.Message);
            });
        config.Services.Replace(typeof(IExceptionHandler), handlerMock.Object);


        using (var server = new HttpTestServer(config)) {
            string url = "http://localhost/api/some-resources/";

            var client = server.CreateClient();
            client.BaseAddress = new Uri(url);

            using (var response = await client.GetAsync("")) {
                Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
            }

            using (var response = await client.GetAsync("3D6BDC0A-B539-4EBF-83AD-2FF5E958AFC3")) {
                Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
            }

            using (var response = await client.PostAsJsonAsync("", new CreateData())) {
                Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
            }
        }
    }

    public class CreateData { }
}

ORIGINAL ANSWER

Referencing : Routing and Action Selection in ASP.NET Web API

That's because it uses the routes in the route table to find the controller first and then checks for Http{Verb} to select an action. which is why it works when they are all in the same controller. if it finds the same route to two different controllers it doesn't know when one to select, hence the error.

If the goal is simple code organization then take advantage of partial classes

ResourcesController.cs

[RoutePrefix("/some-resources")]
partial class ResourcesController : ApiController { }

ResourcesController_Creation.cs

partial class ResourcesController {
    [HttpPost, Route]
    public ... CreateResource(CreateData input) {
        // ...
    }
}

ResourcesController_Display.cs

partial class ResourcesController {
    [HttpGet, Route]
    public ... ListAllResources() {
        // ...
    }

    [HttpGet, Route("{publicKey:guid}"]
    public ... ShowSingleResource(Guid publicKey) {
        // ...
    }
}
Share:
13,933
Crozin
Author by

Crozin

Updated on June 13, 2022

Comments

  • Crozin
    Crozin almost 2 years

    I've got a following two controllers:

    [RoutePrefix("/some-resources")
    class CreationController : ApiController
    {
        [HttpPost, Route]
        public ... CreateResource(CreateData input)
        {
            // ...
        }
    }
    
    [RoutePrefix("/some-resources")
    class DisplayController : ApiController
    {
        [HttpGet, Route]
        public ... ListAllResources()
        {
            // ...
        }
    
        [HttpGet, Route("{publicKey:guid}"]
        public ... ShowSingleResource(Guid publicKey)
        {
            // ...
        }
    }
    

    All three actions got in fact three different routes:

    • GET /some-resources
    • POST /some-resources
    • GET /some-resources/aaaaa-bbb-ccc-dddd

    If I put them into single controller everything works just fine, however if I separate them (as shown above) WebApi throws following exception:

    Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL.

    This message is quite obvious. It seems WebApi does not take HTTP method into account when looking for a right candidate for controller/action.

    How could I achieve the expected behavior?


    UPDATE: I've digged a little into Web API internals and I understand that's the way it works by default. My goal is to separate the code and logic - in real world case those controllers have different dependencies and are a bit more complex. For the sake of maintenance, testability, project organization etc. they should be different objects (SOLID and stuff).

    I thought I could override some WebAPI services (IControllerSelector etc) however this seems to be a little bit risky and non-standard approach for this simple and - as I assumed - common case.

  • Crozin
    Crozin over 7 years
    Sorry for the delay. Everything seem to work flawless. Thank you!
  • quetzalcoatl
    quetzalcoatl over 6 years
    I think you forgot to actually use [MethodConstraintedRoute(..)] in the So given the following controller with the custom route constraints applied section
  • Nkosi
    Nkosi over 6 years
    @quetzalcoatl no I have not. If you look at the code snippets above you will see that MethodConstraintedRoute is a base class used by the HttpGetAttribute and HttpPostAttribute
  • quetzalcoatl
    quetzalcoatl over 6 years
    ooh, that's true. I overlooked that part, and thought that HttpGet/HttpPost are std framework code. Thanks!