Ambiguous Controller Names with Routing attributes: controllers with same name and different namespace for versioning
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.
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.
Based on @JotaBe answer I've developed my own IHttpControllerSelector
which allows controllers (in my case those which are tagged with [RoutePrefix]
attribute) to be mapped with their full name (Namespace AND name).
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Dispatcher;
using System.Web.Http.Routing;
/// <summary>
/// Allows the use of multiple controllers with same name (obviously in different namespaces)
/// by prepending controller identifier with their namespaces (if they have [RoutePrefix] attribute).
/// Allows attribute-based controllers to be mixed with explicit-routes controllers without conflicts.
/// </summary>
public class NamespaceHttpControllerSelector : DefaultHttpControllerSelector
{
private HttpConfiguration _configuration;
private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers;
public NamespaceHttpControllerSelector(HttpConfiguration httpConfiguration) : base(httpConfiguration)
{
_configuration = httpConfiguration;
_controllers = new Lazy<Dictionary<string, HttpControllerDescriptor>>(InitializeControllerDictionary);
}
public override IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
{
return _controllers.Value; // just cache the list of controllers, so we load only once at first use
}
/// <summary>
/// The regular DefaultHttpControllerSelector.InitializeControllerDictionary() does not
/// allow 2 controller types to have same name even if they are in different namespaces (they are ignored!)
///
/// This method will map ALL controllers, even if they have same name,
/// by prepending controller names with their namespaces if they have [RoutePrefix] attribute
/// </summary>
/// <returns></returns>
private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
{
IAssembliesResolver assembliesResolver = _configuration.Services.GetAssembliesResolver();
IHttpControllerTypeResolver controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();
ICollection<Type> controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);
// simple alternative? in case you want to map maybe "UserAPI" instead of "UserController"
// var controllerTypes = System.Reflection.Assembly.GetExecutingAssembly().GetTypes()
// .Where(t => t.IsClass && t.IsVisible && !t.IsAbstract && typeof(IHttpController).IsAssignableFrom(t));
var controllers = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
foreach (Type t in controllerTypes)
{
var controllerName = t.Name;
// ASP.NET by default removes "Controller" suffix, let's keep that convention
if (controllerName.EndsWith(ControllerSuffix))
controllerName = controllerName.Remove(controllerName.Length - ControllerSuffix.Length);
// For controllers with [RoutePrefix] we'll register full name (namespace+name).
// Those routes when matched they provide the full type name, so we can match exact controller type.
// For other controllers we'll register as usual
bool hasroutePrefixAttribute = t.GetCustomAttributes(typeof(RoutePrefixAttribute), false).Any();
if (hasroutePrefixAttribute)
controllerName = t.Namespace + "." + controllerName;
if (!controllers.Keys.Contains(controllerName))
controllers[controllerName] = new HttpControllerDescriptor(_configuration, controllerName, t);
}
return controllers;
}
/// <summary>
/// For "regular" MVC routes we will receive the "{controller}" value in route, and we lookup for the controller as usual.
/// For attribute-based routes we receive the ControllerDescriptor which gives us
/// the full name of the controller as registered (with namespace), so we can version our APIs
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
HttpControllerDescriptor controller;
IDictionary<string, HttpControllerDescriptor> controllers = GetControllerMapping();
IDictionary<string, HttpControllerDescriptor> controllersWithoutAttributeBasedRouting =
GetControllerMapping().Where(kv => !kv.Value.ControllerType
.GetCustomAttributes(typeof(RoutePrefixAttribute), false).Any())
.ToDictionary(kv => kv.Key, kv => kv.Value);
var route = request.GetRouteData();
// regular routes are registered explicitly using {controller} route - and in case we'll match by the controller name,
// as usual ("CourseController" is looked up in dictionary as "Course").
if (route.Values != null && route.Values.ContainsKey("controller"))
{
string controllerName = (string)route.Values["controller"];
if (controllersWithoutAttributeBasedRouting.TryGetValue(controllerName, out controller))
return controller;
}
// For attribute-based routes, the matched route has subroutes,
// and we can get the ControllerDescriptor (with the exact name that we defined - with namespace) associated, to return correct controller
if (route.GetSubRoutes() != null)
{
route = route.GetSubRoutes().First(); // any sample route, we're just looking for the controller
// Attribute Routing registers a single route with many subroutes, and we need to inspect any action of the route to get the controller
if (route.Route != null && route.Route.DataTokens != null && route.Route.DataTokens["actions"] != null)
{
// if it wasn't for attribute-based routes which give us the ControllerDescriptor for each route,
// we could pick the correct controller version by inspecting version in accepted mime types in request.Headers.Accept
string controllerTypeFullName = ((HttpActionDescriptor[])route.Route.DataTokens["actions"])[0].ControllerDescriptor.ControllerName;
if (controllers.TryGetValue(controllerTypeFullName, out controller))
return controller;
}
}
throw new HttpResponseException(HttpStatusCode.NotFound);
}
}