Ambiguous Controller Names with Routing attributes: controllers with same name and different namespace for versioning

前端 未结 3 1860
迷失自我
迷失自我 2020-12-15 06:39

I am trying to add API versioning and my plan is to create a controller for each version in different namespace. My project structure looks like this (note: no separate area

3条回答
  •  忘掉有多难
    2020-12-15 07:04

    Based on @JotaBe answer I've developed my own IHttpControllerSelector which allows controllers (in my case those which are tagged with [RoutePrefix] attribute) to be mapped with their full name (Namespace AND name).

    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;
    
    /// 
    /// 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.
    /// 
    public class NamespaceHttpControllerSelector : DefaultHttpControllerSelector
    {
        private HttpConfiguration _configuration;
        private readonly Lazy> _controllers;
    
        public NamespaceHttpControllerSelector(HttpConfiguration httpConfiguration) : base(httpConfiguration)
        {
            _configuration = httpConfiguration;
            _controllers = new Lazy>(InitializeControllerDictionary);
        }
    
        public override IDictionary GetControllerMapping()
        {
            return _controllers.Value; // just cache the list of controllers, so we load only once at first use
        }
    
        /// 
        /// 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
        /// 
        /// 
        private Dictionary InitializeControllerDictionary()
        {
            IAssembliesResolver assembliesResolver = _configuration.Services.GetAssembliesResolver();
            IHttpControllerTypeResolver controllersResolver = _configuration.Services.GetHttpControllerTypeResolver(); 
            ICollection 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(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;
        }
    
        /// 
        /// 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
        /// 
        /// 
        /// 
        public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
        {
            HttpControllerDescriptor controller;
            IDictionary controllers = GetControllerMapping();
            IDictionary 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);
        }
    
    }
    

提交回复
热议问题