Multiple Controller Types with same Route prefix ASP.NET Web Api
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;
}
}
Attribute routing in different controllers results in Multiple controller types were found that match the URL
Routing is what determines what controller to use. But there is nothing built into routing (in Web API 2, anyway) that can tell the difference between a Get
and a Post
. By the time the request is handed off to the action invoker, it is already too late to go back and change the controller.
So, to fix this the best option is to use IHttpRouteConstraint
to put further criteria on the route whether to match HttpGet
or HttpPost
and then configure it accordingly.
See Multiple Controller Types with same Route prefix ASP.NET Web Api for an example.
Multiple controllers with same URL routes but different HTTP methods
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) {
// ...
}
}
asp.net web api route for controllers with same name
Unfortunately, that is not very simple because default controller selector (DefaultHttpControllerSelector) does not look for namespace in the full controller name when it selects controller to process request.
So, there are at least two possible solutions to your problem:
- Write your own
IHttpControllerSelector
which takes controller type namespace into account. Sample can be found here. - Rename one of controller types to make then unique.
TL;DR Default controller selector uses the cache of controller types (HttpControllerTypeCache) which holds something like:
{
"Customers" : [
typeof(Foo.CustomersController),
typeof(Bar.CustomersController)
],
"Orders" : [
typeof(Foo.OrdersController)
]
}
So it uses controller name as dictionary key, but it can contain several types which have same controller name part. When request is received, controller selector gets controller name from route data. E.g. if you have default route specified "api/{controller}/{id}"
then request api/customers/5
will map controller value to "customers"
. Controller selector then gets controller types which are registered for this name:
- if there is 1 type, then we can instantiate it and process request
- if there is 0 types, it throws
NotFound
exception - if there is 2 or more types, it throws
AmbiguousControllerException
exception (your case)
So.. definition of another named route will not help you, because you can only provide controller name there. You cannot specify namespace.
Even if you'll use Attribute Routing and specify another route for one of the controller actions
[RoutePrefix("api/customers2")]
public class CustomersController : ApiController
{
[Route("")]
public IHttpActionResult Get()
//...
}
you will still face the controller selector problem because attribute routes are applied to actions - there will be no controller value in route data for those requests. Attributed routes are treated differently - they are processed as sub-routes of the "MS_attributerouteWebApi"
route.
Multiple controller types were found that match the URL in mvc app
Add RoutePrefix for HomeController and move Route from controller to methods/actions.
Empty string in Route and RoutePrefix attributes means that this controller or action is default.
http://dev.local/ => HomeController and Index action
http://dev.local/search?searchTerm=123 => SearchController and Index action
Please keep in mind that only one controller can have empty RoutePrefix and only one action in controller can have empty Route
[RoutePrefix("")]
public class HomeController : Controller
{
[Route("")]
public ActionResult Index()
{
return View();
}
}
[RoutePrefix("search")]
public class SearchController : Controller
{
[Route("")]
public ActionResult Index(string searchTerm)
{
return View();
}
}
Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL
In case of Attribute routing, Web API tries to find all the controllers which match a request. If it sees that multiple controllers are able to handle this, then it throws an exception as it considers this to be possibly an user error. This route probing is different from regular routing where the first match wins.
As a workaround, if you have these two actions within the same controller, then Web API honors the route precedence and you should see your scenario working.
Related Topics
Where Are Clr-Defined Methods Like [Delegate].Begininvoke Documented
Are .Net Switch Statements Hashed or Indexed
How to Add My New User Control to the Toolbox or a New Winform
Datagrid Column Width Doesn't Auto-Update
Reading Fromuri and Frombody at the Same Time
Resharper Formatting: Align Equal Operands
Extension Method on Enumeration, Not Instance of Enumeration
No Itemchecked Event in a Checkedlistbox
C#: How to Add Subitems in Listview
Programmatically Getting the Last Filled Excel Row Using C#
Expression of Type 'System.Int32' Cannot Be Used for Return Type 'System.Object'
Why Would Try/Finally Rather Than a "Using" Statement Help Avoid a Race Condition