Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ambiguous Controller Names with Routing attributes: controllers with same name and different namespace for versioning

I am trying to add API versioning and my plan is to create a controller for each version in different namespace. My project structure looks like this (note: no separate area for each version)

Controllers
 |
 |---Version0
 |      |
 |      |----- ProjectController.cs
 |      |----- HomeController.cs
 |
 |---Version1
       |
       |----- ProjectController.cs
       |----- HomeController.cs

I am using RoutingAttribute for the routes. So, ProjectController in Version0 has function with route as

namespace MyProject.Controllers.Version0
{
   class ProjectController : BaseController
   {
     ...

     [Route(api/users/project/getProjects/{projectId})]
     public async GetProjects(string projectId) 
     {
       ...
     }
  }
}

and ProjectController in Version1 has function with route as

namespace MyProject.Controllers.Version1
{
   class ProjectController : BaseController
   {
     ...

     [Route(api/v1/users/project/getProjects/{projectId})]
     public async GetProjects(string projectId) 
     {
      ...
     }
  }
}

But, I get 404-NotFound when I am trying to hit the service.

If I rename the controllers to have unique name (Project1Controller and Project2Controller) the routing works. But, I am trying to avoid renaming for simplicity.

I followed this link to resolve the issue, but it didn't help. I did create areas but still no success. Adding routing logic in global.aspx file do not help. The namespace do not work either. http://haacked.com/archive/2010/01/12/ambiguous-controller-names.aspx/

The above link suggest to create areas, but the attribute routing do not support areas as per link: http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2

Is there another solution? A bug with RoutingAttributes?

Thank you!

like image 474
rkd Avatar asked Jan 14 '15 00:01

rkd


2 Answers

First, Web API routing, and MVC routing doesn't work exactly in the same way.

Your first link points to MVC routing, with areas. Areas are not officially supported for Web API, although you can try to make something similar to them. However, even if you try to do something like that, you'll get the same error, because the way in wich Web API looks for a controller doesn't takes into account the controller's namespace.

So, out of the box, it will never work.

However, you can modify most Web API behaviors, and this is not an exception.

Web API uses a Controller Selector to get the desired controller. The behavior explained above is the behavior of the DefaultHttpControllerSelector, which comes with Web API, but you can implement your own selector to replace the default one, and support new behaviors.

If you google for "custom web api controller selector" you'll find many samples, but I find this the most interesting for exactly your problem:

  • ASP.NET Web API: Using Namespaces to Version Web APIs

This implementation is also interesting:

  • https://github.com/WebApiContrib/WebAPIContrib/pull/111/files (thank you to Robin van der Knaap for the update of this broken link)

As you see there, basically you need to:

  • implement your own IHttpControllerSelector, which takes into account namespaces to find the controllers, and the namespaces route variable, to choose one of them.
  • replace the original selector with this via Web API configuration.
like image 50
JotaBe Avatar answered Sep 17 '22 14:09

JotaBe


I know this was answered a while a go and has already been accepted by the original poster. However if you are like me and require the use of attribute routing and have tried the suggested answer you will know that it wont quite work.

When I tried this I found out that it was actually missing the routing information that should have been generated by calling the extension method MapHttpAttributeRoutes of theHttpConfiguration class:

config.MapHttpAttributeRoutes();

This meant that the method SelectController of the replacement IHttpControllerSelector implementation never actually gets called and is why the request produces a http 404 response.

The issue is caused by an internal class called HttpControllerTypeCache which is an internal class in the System.Web.Http assembly under the System.Web.Http.Dispatcher namespace. The code in question is the following:

    private Dictionary<string, ILookup<string, Type>> InitializeCache()
    {
      return this._configuration.Services.GetHttpControllerTypeResolver().GetControllerTypes(this._configuration.Services.GetAssembliesResolver()).GroupBy<Type, string>((Func<Type, string>) (t => t.Name.Substring(0, t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length)), (IEqualityComparer<string>) StringComparer.OrdinalIgnoreCase).ToDictionary<IGrouping<string, Type>, string, ILookup<string, Type>>((Func<IGrouping<string, Type>, string>) (g => g.Key), (Func<IGrouping<string, Type>, ILookup<string, Type>>) (g => g.ToLookup<Type, string>((Func<Type, string>) (t => t.Namespace ?? string.Empty), (IEqualityComparer<string>) StringComparer.OrdinalIgnoreCase)), (IEqualityComparer<string>) StringComparer.OrdinalIgnoreCase);
    }

You will see in this code that it is grouping by the type name without the namespace. The DefaultHttpControllerSelector class uses this functionality when it builds up an internal cache of HttpControllerDescriptor for each controller. When using the MapHttpAttributeRoutes method it use another internal class called AttributeRoutingMapper which is part of the System.Web.Http.Routing namespace. This class uses the method GetControllerMapping of the IHttpControllerSelector in order to configure the routes.

So if you are going to write a custom IHttpControllerSelector then you need to overload the GetControllerMapping method for it to work. The reason I mention this is that none of the implementations I have seen on the internet does this.

like image 42
Tom Maher Avatar answered Sep 19 '22 14:09

Tom Maher