ルーティング属性を持つあいまいなコントローラー名:バージョン管理のために同じ名前で異なる名前空間を持つコントローラー

まず、Web API ルーティングと MVC ルーティングはまったく同じようには機能しません。

最初のリンクは、領域を含む MVC ルーティングを指しています。エリアは Web API で公式にサポートされていませんが、似たようなものを作成することはできます。ただし、そのようなことをしようとしても、同じエラーが発生します。これは、Web API がコントローラーを検索する方法で、コントローラーの名前空間が考慮されていないためです。

したがって、そのままでは機能しません。

ただし、ほとんどの Web API の動作は変更でき、これも例外ではありません。

Web API は、コントローラー セレクターを使用して目的のコントローラーを取得します。上記で説明した動作は、Web API に付属する DefaultHttpControllerSelector の動作ですが、独自のセレクターを実装してデフォルトのセレクターを置き換え、新しい動作をサポートすることができます。

「カスタム Web API コントローラー セレクター」をグーグルで検索すると、多くのサンプルが見つかりますが、これがまさにあなたの問題について最も興味深いと思います:

  • ASP.NET Web API:名前空間を使用して Web API をバージョン管理する

この実装も興味深いです:

  • https://github.com/WebApiContrib/WebAPIContrib/pull/111/files (この壊れたリンクを更新してくれた Robin van der Knaap に感謝します)

ご覧のとおり、基本的に次のことを行う必要があります:

  • 独自の IHttpControllerSelector を実装する 、これは名前空間を考慮してコントローラーを見つけ、名前空間ルート変数を使用してそれらの 1 つを選択します。
  • Web API 設定を介して元のセレクターをこれに置き換えます。

これはしばらく前に回答され、元の投稿者によってすでに受け入れられていることを知っています。ただし、私のように属性ルーティングを使用する必要があり、提案された回答を試した場合、それがうまく機能しないことがわかります。

これを試してみたところ、拡張メソッド 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 をオーバーロードする必要があります それが機能するための方法。私がこれに言及する理由は、私がインターネットで見た実装のどれもこれを行っていないからです.


@JotaBe の回答に基づいて、独自の 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);
    }

}