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