简体   繁体   中英

Select ASP.NET Web API Controller depending on route value (namespace)

I am trying to add various Web API plugins to my MVC website. Copying and pasting DLLs of API files allow me to call appropriate service methods using the default route - hostname/api/{controller}/{id}, but it throws an error, when I have controllers with the same name but in different locations (DLLs, namespaces). The error message is something like this (which is normal):

Multiple types were found that match the controller named 'Names'. This can happen if the route that services this request ('api/{controller}/{id}') found multiple controllers defined with the same name but differing namespaces, which is not supported. The request for 'Names' has found the following matching controllers: ApiExt.NamesController ApiExt1.NamesController

I have the same "Names" controller in different DLLs (namespaces) ApiExt, ApiExt1.

I already found a similar topic to select controller depending of API version - http://shazwazza.com/post/multiple-webapi-controllers-with-the-same-name-but-different-namespaces/ , but this is not quite what I need. I need to select the controller (namespace) depending on route value, something like this:

hostname/api/{namespace}/{controller}/{id}

I believe this is absolutely possible, but I'm not familiar with overriding MVC controller selector (implementing IHttpControllerSelector).

Any suggestions?

Thank you.

You can definitely achieve this. You need to write your own Controller Selector by implementing IHttpControllerSelector . Please refer to this link for detailed, step-by-step explanation.

The blog post describing a solution, https://blogs.msdn.microsoft.com/webdev/2013/03/07/asp-net-web-api-using-namespaces-to-version-web-apis/ , doesnt contain the complete code and has a bad link to what used to provide it.

Here's a blob providing the class, original from Umbraco

https://github.com/WebApiContrib/WebAPIContrib/blob/master/src/WebApiContrib/Selectors/NamespaceHttpControllerSelector.cs ,

Full listing in case it's removed:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Dispatcher;

namespace WebApiContrib.Selectors
{
    //originally created for Umbraco https://github.com/umbraco/Umbraco-CMS/blob/7.2.0/src/Umbraco.Web/WebApi/NamespaceHttpControllerSelector.cs
    //adapted from there, does not recreate HttpControllerDescriptors, instead caches them
    public class NamespaceHttpControllerSelector : DefaultHttpControllerSelector
    {
        private const string ControllerKey = "controller";
        private readonly HttpConfiguration _configuration;
        private readonly Lazy<HashSet<NamespacedHttpControllerMetadata>> _duplicateControllerTypes;

        public NamespaceHttpControllerSelector(HttpConfiguration configuration)
            : base(configuration)
        {
            _configuration = configuration;
            _duplicateControllerTypes = new Lazy<HashSet<NamespacedHttpControllerMetadata>>(InitializeNamespacedHttpControllerMetadata);
        }

        public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
        {
            var routeData = request.GetRouteData();
            if (routeData == null || routeData.Route == null || routeData.Route.DataTokens["Namespaces"] == null)
                return base.SelectController(request);

            // Look up controller in route data
            object controllerName;
            routeData.Values.TryGetValue(ControllerKey, out controllerName);
            var controllerNameAsString = controllerName as string;
            if (controllerNameAsString == null)
                return base.SelectController(request);

            //get the currently cached default controllers - this will not contain duplicate controllers found so if
            // this controller is found in the underlying cache we don't need to do anything
            var map = base.GetControllerMapping();
            if (map.ContainsKey(controllerNameAsString))
                return base.SelectController(request);

            //the cache does not contain this controller because it's most likely a duplicate, 
            // so we need to sort this out ourselves and we can only do that if the namespace token
            // is formatted correctly.
            var namespaces = routeData.Route.DataTokens["Namespaces"] as IEnumerable<string>;
            if (namespaces == null)
                return base.SelectController(request);

            //see if this is in our cache
            var found = _duplicateControllerTypes.Value.FirstOrDefault(x => string.Equals(x.ControllerName, controllerNameAsString, StringComparison.OrdinalIgnoreCase) && namespaces.Contains(x.ControllerNamespace));
            if (found == null)
                return base.SelectController(request);

            return found.Descriptor;
        }

        private HashSet<NamespacedHttpControllerMetadata> InitializeNamespacedHttpControllerMetadata()
        {
            var assembliesResolver = _configuration.Services.GetAssembliesResolver();
            var controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();
            var controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);

            var groupedByName = controllerTypes.GroupBy(
                t => t.Name.Substring(0, t.Name.Length - ControllerSuffix.Length),
                StringComparer.OrdinalIgnoreCase).Where(x => x.Count() > 1);

            var duplicateControllers = groupedByName.ToDictionary(
                g => g.Key,
                g => g.ToLookup(t => t.Namespace ?? String.Empty, StringComparer.OrdinalIgnoreCase),
                StringComparer.OrdinalIgnoreCase);

            var result = new HashSet<NamespacedHttpControllerMetadata>();

            foreach (var controllerTypeGroup in duplicateControllers)
            {
                foreach (var controllerType in controllerTypeGroup.Value.SelectMany(controllerTypesGrouping => controllerTypesGrouping))
                {
                    result.Add(new NamespacedHttpControllerMetadata(controllerTypeGroup.Key, controllerType.Namespace,
                        new HttpControllerDescriptor(_configuration, controllerTypeGroup.Key, controllerType)));
                }
            }

            return result;
        }

        private class NamespacedHttpControllerMetadata
        {
            private readonly string _controllerName;
            private readonly string _controllerNamespace;
            private readonly HttpControllerDescriptor _descriptor;

            public NamespacedHttpControllerMetadata(string controllerName, string controllerNamespace, HttpControllerDescriptor descriptor)
            {
                _controllerName = controllerName;
                _controllerNamespace = controllerNamespace;
                _descriptor = descriptor;
            }

            public string ControllerName
            {
                get { return _controllerName; }
            }

            public string ControllerNamespace
            {
                get { return _controllerNamespace; }
            }

            public HttpControllerDescriptor Descriptor
            {
                get { return _descriptor; }
            }
        }
    }
}

Then just add the Namespaces token to the route

route.DataTokens["Namespaces"] = new string[] {"Foo.Controllers"};

It's also more production ready with the caching.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM