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!
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:
This implementation is also interesting:
As you see there, basically you need to:
IHttpControllerSelector
, which takes into account namespaces to find the controllers, and the namespaces route variable, to choose one of them.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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With