带有路由属性的不明确的控制器名称:具有相同名称和不同命名空间的控制器,用于版本控制

时间:2015-01-14 00:00:57

标签: c# asp.net asp.net-web-api asp.net-web-api2 asp.net-web-api-routing

我正在尝试添加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),则路由可以正常工作。但是,我试图避免重命名以简化。

我按照此链接解决了问题,但它没有帮助。我确实创造了一些领域但仍然没有成功。在global.aspx文件中添加路由逻辑没有帮助。命名空间也不起作用。 http://haacked.com/archive/2010/01/12/ambiguous-controller-names.aspx/

以上链接建议创建区域,但属性路由不支持按链接区域: http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2

还有其他解决方案吗? RoutingAttributes的错误?

谢谢!

3 个答案:

答案 0 :(得分:19)

首先,Web API路由和MVC路由不能以完全相同的方式工作。

您的第一个链接指向带有区域的MVC路由。虽然您可以尝试制作与它们类似的内容,但Web API并未正式支持这些区域。但是,即使你尝试做类似的事情,你也会得到同样的错误,因为Web API寻找控制器的方式没有考虑控制器的命名空间。

所以,开箱即用,它永远不会奏效。

但是,您可以修改大多数Web API行为,这也不例外。

Web API使用Controller Selector获取所需的控制器。上面解释的行为是Web API附带的DefaultHttpControllerSelector的行为,但您可以实现自己的选择器来替换默认选择器,并支持新行为。

如果你谷歌搜索“自定义web api控制器选择器”你会发现很多样本,但我觉得这对你的问题最有意思:

这个实现也很有趣:

如你所见,基本上你需要:

  • 实现您自己的IHttpControllerSelector,它会考虑名称空间来查找控制器,以及命名空间路由变量,以选择其中一个。
  • 通过Web API配置替换原始选择器。

答案 1 :(得分:6)

我知道这回答了一段时间,并且已经被原始海报接受了。但是,如果你像我一样需要使用属性路由并尝试了建议的答案,你就会知道它不会很有效。

当我尝试这个时,我发现它实际上缺少应该通过调用MapHttpAttributeRoutes类的扩展方法HttpConfiguration生成的路由信息​​:

config.MapHttpAttributeRoutes();

这意味着替换SelectController实现的方法IHttpControllerSelector实际上从未被调用,这就是请求产生http 404响应的原因。

该问题是由名为HttpControllerTypeCache的内部类引起的,该类是System.Web.Http命名空间下System.Web.Http.Dispatcher程序集中的内部类。有问题的代码如下:

    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类为每个控制器构建HttpControllerDescriptor的内部缓存时使用此功能。使用MapHttpAttributeRoutes方法时,它使用另一个名为AttributeRoutingMapper的内部类,它是System.Web.Http.Routing命名空间的一部分。此类使用GetControllerMapping的方法IHttpControllerSelector来配置路由。

因此,如果您要编写自定义IHttpControllerSelector,则需要重载GetControllerMapping方法才能使其正常工作。我提到这个的原因是我在互联网上看到的所有实现都没有这样做。

答案 2 :(得分:1)

基于@JotaBe答案,我开发了my own IHttpControllerSelector,它允许控制器(在我的情况下是带有[RoutePrefix]属性的控制器)以其全名(命名空间)进行映射AND名称)。

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);
    }

}