9

私は現在、次の設計上の決定に準拠する必要があるASP.NET MVC4Webアプリケーションプロジェクトに取り組んでいます。

  • メインのMVCアプリケーションは、ソリューションのルートにあります。
  • すべての管理者機能は別の領域にあります。
  • 各外部関係者(サプライヤーなど)には独自の領域があります。
  • ルートを含む各領域は、十分に分離された機能ブロックを構成します。ある領域の機能が別の領域に公開されない場合があります。これは、データへの不正アクセスを防ぐためです。
  • ルートを含む各領域には、独自のRESTfull API(Web API)があります。

ルートを含むすべての領域のすべての通常のコントローラーは、期待どおりに機能します。ただし、一部のWebAPIコントローラーは予期しない動作を示します。たとえば、同じ名前で異なる領域に2つのWeb APIコントローラーがあると、次の例外が発生します。

'clients'という名前のコントローラーに一致する複数のタイプが見つかりました。これは、このリクエストを処理するルート('api / {controller} / {id}')が、サポートされていない同じ名前で異なる名前空間で定義された複数のコントローラーを検出した場合に発生する可能性があります。

'clients'のリクエストにより、一致する次のコントローラーが見つかりました:MvcApplication.Areas.Administration.Controllers.Api.ClientsController MvcApplication.Controllers.Api.ClientsController

両方を分離する必要がある別個のルートがあるので、これは奇妙に思えます。管理セクションのAreaRegistrationは次のとおりです。

public class AdministrationAreaRegistration : AreaRegistration
{
    public override string AreaName
    {
        get
        {
            return "Administration";
        }
    }

    public override void RegisterArea(AreaRegistrationContext context)
    {
        context.Routes.MapHttpRoute(
            name: "Administration_DefaultApi",
            routeTemplate: "Administration/api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        context.MapRoute(
            "Administration_default",
            "Administration/{controller}/{action}/{id}",
            new { action = "Index", id = UrlParameter.Optional }
        );
    }
}

さらに、呼び出しからエリアの名前を省略しながら、エリア固有のWebAPIにアクセスできることに気付きました。

ここで何が起こっているのですか?WebAPIコントローラーを通常のASP.NETMVCコントローラーと同じように動作させるにはどうすればよいですか?

4

1 に答える 1

19

ASP.NET MVC 4は、エリア間でのWebAPIコントローラーのパーティション化をサポートしていません。

WebApiコントローラーを異なるエリアの異なるApiフォルダーに配置することもできますが、ASP.NETMVCはそれらがすべて同じ場所にあるかのように扱います。

幸い、ASP.NET MVCインフラストラクチャの一部をオーバーライドすることで、この制限を克服できます。制限と解決策の詳細については、私のブログ投稿「ASP.NET MVC 4 RC:WebApiと領域を適切に再生する」をお読みください。ソリューションのみに関心がある場合は、以下をお読みください。

ステップ1.ルートエリアを認識させる

次の拡張メソッドをASP.NETMVCアプリケーションに追加し、AreaRegistrationクラスからアクセスできることを確認します。

public static class AreaRegistrationContextExtensions
{
    public static Route MapHttpRoute(this AreaRegistrationContext context, string name, string routeTemplate)
    {
        return context.MapHttpRoute(name, routeTemplate, null, null);
    }

    public static Route MapHttpRoute(this AreaRegistrationContext context, string name, string routeTemplate, object defaults)
    {
        return context.MapHttpRoute(name, routeTemplate, defaults, null);
    }

    public static Route MapHttpRoute(this AreaRegistrationContext context, string name, string routeTemplate, object defaults, object constraints)
    {
        var route = context.Routes.MapHttpRoute(name, routeTemplate, defaults, constraints);
        if (route.DataTokens == null)
        {
            route.DataTokens = new RouteValueDictionary();
        }
        route.DataTokens.Add("area", context.AreaName);
        return route;
    }
}

新しい拡張メソッドを使用するにはRoutes、呼び出しチェーンからプロパティを削除します。

context.MapHttpRoute( /* <-- .Routes removed */
    name: "Administration_DefaultApi",
    routeTemplate: "Administration/api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

手順2.WebAPIコントローラーセレクターの領域を認識させる

次のクラスをASP.NETMVCアプリケーションに追加し、Global.asaxからアクセスできることを確認します

namespace MvcApplication.Infrastructure.Dispatcher
{
    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using System.Net.Http;
    using System.Web.Http;
    using System.Web.Http.Controllers;
    using System.Web.Http.Dispatcher;

    public class AreaHttpControllerSelector : DefaultHttpControllerSelector
    {
        private const string AreaRouteVariableName = "area";

        private readonly HttpConfiguration _configuration;
        private readonly Lazy<ConcurrentDictionary<string, Type>> _apiControllerTypes;

        public AreaHttpControllerSelector(HttpConfiguration configuration)
            : base(configuration)
        {
            _configuration = configuration;
            _apiControllerTypes = new Lazy<ConcurrentDictionary<string, Type>>(GetControllerTypes);
        }

        public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
        {
            return this.GetApiController(request);
        }

        private static string GetAreaName(HttpRequestMessage request)
        {
            var data = request.GetRouteData();
            if (data.Route.DataTokens == null)
            {
                return null;
            } 
            else 
            {
                object areaName;
                return data.Route.DataTokens.TryGetValue(AreaRouteVariableName, out areaName) ? areaName.ToString() : null;
            }
        }

        private static ConcurrentDictionary<string, Type> GetControllerTypes()
        {
            var assemblies = AppDomain.CurrentDomain.GetAssemblies();

            var types = assemblies
                .SelectMany(a => a
                    .GetTypes().Where(t =>
                        !t.IsAbstract &&
                        t.Name.EndsWith(ControllerSuffix, StringComparison.OrdinalIgnoreCase) &&
                        typeof(IHttpController).IsAssignableFrom(t)))
                .ToDictionary(t => t.FullName, t => t);

            return new ConcurrentDictionary<string, Type>(types);
        }

        private HttpControllerDescriptor GetApiController(HttpRequestMessage request)
        {
            var areaName = GetAreaName(request);
            var controllerName = GetControllerName(request);
            var type = GetControllerType(areaName, controllerName);

            return new HttpControllerDescriptor(_configuration, controllerName, type);
        }

        private Type GetControllerType(string areaName, string controllerName)
        {
            var query = _apiControllerTypes.Value.AsEnumerable();

            if (string.IsNullOrEmpty(areaName))
            {
                query = query.WithoutAreaName();
            }
            else
            {
                query = query.ByAreaName(areaName);
            }

            return query
                .ByControllerName(controllerName)
                .Select(x => x.Value)
                .Single();
        }
    }

    public static class ControllerTypeSpecifications
    {
        public static IEnumerable<KeyValuePair<string, Type>> ByAreaName(this IEnumerable<KeyValuePair<string, Type>> query, string areaName)
        {
            var areaNameToFind = string.Format(CultureInfo.InvariantCulture, ".{0}.", areaName);

            return query.Where(x => x.Key.IndexOf(areaNameToFind, StringComparison.OrdinalIgnoreCase) != -1);
        }

        public static IEnumerable<KeyValuePair<string, Type>> WithoutAreaName(this IEnumerable<KeyValuePair<string, Type>> query)
        {
            return query.Where(x => x.Key.IndexOf(".areas.", StringComparison.OrdinalIgnoreCase) == -1);
        }

        public static IEnumerable<KeyValuePair<string, Type>> ByControllerName(this IEnumerable<KeyValuePair<string, Type>> query, string controllerName)
        {
            var controllerNameToFind = string.Format(CultureInfo.InvariantCulture, ".{0}{1}", controllerName, AreaHttpControllerSelector.ControllerSuffix);

            return query.Where(x => x.Key.EndsWith(controllerNameToFind, StringComparison.OrdinalIgnoreCase));
        }
    }
}

Global.asaxDefaultHttpControllerSelectorのメソッドに次の行を追加して、をオーバーライドします。Application_Start

GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), new AreaHttpControllerSelector(GlobalConfiguration.Configuration));

おめでとうございます。これで、Web APIコントローラーは、通常のMVCコントローラーと同じようにエリアのルールを尊重するようになります。

更新:2012年9月6日

DataTokensルート変数のプロパティがである場合に遭遇したシナリオについて、何人かの開発者から連絡がありましたnullDataTokens私の実装では、プロパティは常に初期化されており、このプロパティが。の場合は正しく機能しないと想定していますnull。この動作は、ASP.NET MVCフレームワークの最近の変更が原因である可能性が高く、実際にはフレームワークのバグである可能性があります。このシナリオを処理するためにコードを更新しました。

于 2012-08-19T08:52:22.530 に答える