具有路由属性的模糊控制器名称:具有相同名称和不同名称空间的控制器用于版本控制
Ambiguous Controller Names with Routing attributes: controllers with same name and different namespace for versioning
我正在尝试添加 API 版本控制,我的计划是为不同命名空间中的每个版本创建一个控制器。我的项目结构是这样的(注意:每个版本没有单独的区域)
Controllers
|
|---Version0
| |
| |----- ProjectController.cs
| |----- HomeController.cs
|
|---Version1
|
|----- ProjectController.cs
|----- HomeController.cs
我正在为路由使用 RoutingAttribute。
因此,Version0 中的 ProjectController 具有路由功能
namespace MyProject.Controllers.Version0
{
class ProjectController : BaseController
{
...
[Route(api/users/project/getProjects/{projectId})]
public async GetProjects(string projectId)
{
...
}
}
}
并且 Version1 中的 ProjectController 具有路由功能
namespace MyProject.Controllers.Version1
{
class ProjectController : BaseController
{
...
[Route(api/v1/users/project/getProjects/{projectId})]
public async GetProjects(string projectId)
{
...
}
}
}
但是,当我尝试访问该服务时收到 404-NotFound。
如果我将控制器重命名为具有唯一名称(Project1Controller 和 Project2Controller),则路由有效。但是,为了简单起见,我试图避免重命名。
我按照这个 link 解决了这个问题,但它没有帮助。我确实创建了区域,但仍然没有成功。在 global.aspx 文件中添加路由逻辑没有帮助。命名空间也不起作用。
http://haacked.com/archive/2010/01/12/ambiguous-controller-names.aspx/
上面link建议创建区域,但是属性路由不支持按link的区域:
http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2
还有其他解决办法吗? RoutingAttributes 的错误?
谢谢!
首先,Web API 路由和 MVC 路由的工作方式并不完全相同。
您的第一个 link 指向带有区域的 MVC 路由。 Web API 不正式支持区域,尽管您可以尝试制作类似的东西。然而,即使您尝试做类似的事情,您也会得到同样的错误,因为 Web API 中查找控制器的方式没有考虑控制器的命名空间。
所以,开箱即用,它永远不会工作。
但是,您可以修改大多数 Web API 行为,这也不例外。
Web API 使用控制器选择器来获取所需的控制器。上面解释的行为是 DefaultHttpControllerSelector 的行为,它是 Web API 自带的,但是你可以实现自己的选择器来替换默认选择器,并支持新的行为。
如果你 google "custom web api controller selector" 你会发现很多样本,但我发现这对你的问题最有趣:
这个实现也很有趣:
- https://github.com/WebApiContrib/WebAPIContrib/pull/111/files(感谢 Robin van der Knaap 更新了这个损坏的 link)
如您所见,基本上您需要:
- 实现您自己的
IHttpControllerSelector
,它考虑了用于查找控制器的名称空间和名称空间路由变量,以选择其中之一。
- 通过 Web API 配置用这个替换原来的选择器。
我知道这已经回答了一段时间,并且已经被原始发布者接受了。但是,如果您像我一样需要使用属性路由并尝试了建议的答案,您就会知道它不会很好地工作。
当我尝试这个时,我发现它实际上丢失了应该通过调用HttpConfiguration
的扩展方法MapHttpAttributeRoutes
生成的路由信息 class:
config.MapHttpAttributeRoutes();
这意味着替换 IHttpControllerSelector
实现的方法 SelectController
从未真正被调用,这就是请求产生 http 404 响应的原因。
此问题是由名为 HttpControllerTypeCache
的内部 class 引起的,它是 System.Web.Http.Dispatcher
命名空间下的 System.Web.Http
程序集中的内部 class。有问题的代码如下:
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);
}
您将在这段代码中看到它按类型名称分组,没有名称空间。 DefaultHttpControllerSelector
class 在为每个控制器建立 HttpControllerDescriptor
的内部缓存时使用此功能。当使用 MapHttpAttributeRoutes
方法时,它使用另一个名为 AttributeRoutingMapper
的内部 class,它是 System.Web.Http.Routing
命名空间的一部分。此 class 使用 IHttpControllerSelector
的方法 GetControllerMapping
来配置路由。
因此,如果您要编写自定义 IHttpControllerSelector
,则需要重载 GetControllerMapping
方法才能使其正常工作。我提到这一点的原因是 none 我在互联网上看到的实现是这样做的。
基于@JotaBe 的回答,我开发了 my own IHttpControllerSelector
,它允许控制器(在我的例子中是那些标有 [RoutePrefix]
属性的控制器)用它们的全名映射(命名空间和名称)。
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);
}
}
我正在尝试添加 API 版本控制,我的计划是为不同命名空间中的每个版本创建一个控制器。我的项目结构是这样的(注意:每个版本没有单独的区域)
Controllers
|
|---Version0
| |
| |----- ProjectController.cs
| |----- HomeController.cs
|
|---Version1
|
|----- ProjectController.cs
|----- HomeController.cs
我正在为路由使用 RoutingAttribute。 因此,Version0 中的 ProjectController 具有路由功能
namespace MyProject.Controllers.Version0
{
class ProjectController : BaseController
{
...
[Route(api/users/project/getProjects/{projectId})]
public async GetProjects(string projectId)
{
...
}
}
}
并且 Version1 中的 ProjectController 具有路由功能
namespace MyProject.Controllers.Version1
{
class ProjectController : BaseController
{
...
[Route(api/v1/users/project/getProjects/{projectId})]
public async GetProjects(string projectId)
{
...
}
}
}
但是,当我尝试访问该服务时收到 404-NotFound。
如果我将控制器重命名为具有唯一名称(Project1Controller 和 Project2Controller),则路由有效。但是,为了简单起见,我试图避免重命名。
我按照这个 link 解决了这个问题,但它没有帮助。我确实创建了区域,但仍然没有成功。在 global.aspx 文件中添加路由逻辑没有帮助。命名空间也不起作用。 http://haacked.com/archive/2010/01/12/ambiguous-controller-names.aspx/
上面link建议创建区域,但是属性路由不支持按link的区域: http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2
还有其他解决办法吗? RoutingAttributes 的错误?
谢谢!
首先,Web API 路由和 MVC 路由的工作方式并不完全相同。
您的第一个 link 指向带有区域的 MVC 路由。 Web API 不正式支持区域,尽管您可以尝试制作类似的东西。然而,即使您尝试做类似的事情,您也会得到同样的错误,因为 Web API 中查找控制器的方式没有考虑控制器的命名空间。
所以,开箱即用,它永远不会工作。
但是,您可以修改大多数 Web API 行为,这也不例外。
Web API 使用控制器选择器来获取所需的控制器。上面解释的行为是 DefaultHttpControllerSelector 的行为,它是 Web API 自带的,但是你可以实现自己的选择器来替换默认选择器,并支持新的行为。
如果你 google "custom web api controller selector" 你会发现很多样本,但我发现这对你的问题最有趣:
这个实现也很有趣:
- https://github.com/WebApiContrib/WebAPIContrib/pull/111/files(感谢 Robin van der Knaap 更新了这个损坏的 link)
如您所见,基本上您需要:
- 实现您自己的
IHttpControllerSelector
,它考虑了用于查找控制器的名称空间和名称空间路由变量,以选择其中之一。 - 通过 Web API 配置用这个替换原来的选择器。
我知道这已经回答了一段时间,并且已经被原始发布者接受了。但是,如果您像我一样需要使用属性路由并尝试了建议的答案,您就会知道它不会很好地工作。
当我尝试这个时,我发现它实际上丢失了应该通过调用HttpConfiguration
的扩展方法MapHttpAttributeRoutes
生成的路由信息 class:
config.MapHttpAttributeRoutes();
这意味着替换 IHttpControllerSelector
实现的方法 SelectController
从未真正被调用,这就是请求产生 http 404 响应的原因。
此问题是由名为 HttpControllerTypeCache
的内部 class 引起的,它是 System.Web.Http.Dispatcher
命名空间下的 System.Web.Http
程序集中的内部 class。有问题的代码如下:
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);
}
您将在这段代码中看到它按类型名称分组,没有名称空间。 DefaultHttpControllerSelector
class 在为每个控制器建立 HttpControllerDescriptor
的内部缓存时使用此功能。当使用 MapHttpAttributeRoutes
方法时,它使用另一个名为 AttributeRoutingMapper
的内部 class,它是 System.Web.Http.Routing
命名空间的一部分。此 class 使用 IHttpControllerSelector
的方法 GetControllerMapping
来配置路由。
因此,如果您要编写自定义 IHttpControllerSelector
,则需要重载 GetControllerMapping
方法才能使其正常工作。我提到这一点的原因是 none 我在互联网上看到的实现是这样做的。
基于@JotaBe 的回答,我开发了 my own IHttpControllerSelector
,它允许控制器(在我的例子中是那些标有 [RoutePrefix]
属性的控制器)用它们的全名映射(命名空间和名称)。
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);
}
}