Why are my Area specific Web API’s accessible from all other Areas?

陌路散爱 提交于 2019-11-30 07:08:09

ASP.NET MVC 4 does not support the partitioning of Web API controllers across Areas.

You may place WebApi controllers in different Api folders in different Areas, but ASP.NET MVC will treat as if they are all in the same place.

Fortunately, you can overcome this limitation by overriding a part of the ASP.NET MVC infrastructure. For more information about the limitation and the solution, please read my blog post 'ASP.NET MVC 4 RC: Getting WebApi and Areas to play nicely'. If you are only interested in the solution, read on:

Step 1. Make your routes Area aware

Add the following extension methods to your ASP.NET MVC application and make sure they are accessible from your AreaRegistration classes:

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

To use the new extension method, remove the Routes property from the call chain:

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

Step 2. Make Web API controller selector Area aware

Add the following class to your ASP.NET MVC application and make sure it is accessible from the 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));
        }
    }
}

Override the DefaultHttpControllerSelector by adding the following line to the Application_Start method in the Global.asax.

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

Congratulations, your Web API controllers will now respect the rules of your areas just like your normal MVC controllers do!

UPDATE: 6 september 2012

Several developers have contacted me about a scenario they encountered where the DataTokens property of the route variable is null. My implementation assumes that the DataTokens property is always initialized and will not function properly if this property is null. This behavior is most likely caused by recent changes in the ASP.NET MVC framework and may be actually be a bug in the framework. I’ve updated my code to handle this scenario.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!