Mehrdeutige Controller-Namen mit Routing-Attributen:Controller mit demselben Namen und unterschiedlichem Namespace für die Versionierung

Mehrdeutige Controller-Namen mit Routing-Attributen:Controller mit demselben Namen und unterschiedlichem Namespace für die Versionierung

Erstens funktionieren das Web-API-Routing und das MVC-Routing nicht genau auf die gleiche Weise.

Ihr erster Link verweist auf das MVC-Routing mit Bereichen. Bereiche werden für die Web-API nicht offiziell unterstützt, obwohl Sie versuchen können, etwas Ähnliches zu erstellen. Aber selbst wenn Sie versuchen, so etwas zu tun, erhalten Sie denselben Fehler, da die Art und Weise, wie die Web-API nach einem Controller sucht, den Namespace des Controllers nicht berücksichtigt.

Out of the Box wird es also nie funktionieren.

Sie können jedoch die meisten Web-API-Verhaltensweisen ändern, und dies ist keine Ausnahme.

Die Web-API verwendet einen Controller Selector, um den gewünschten Controller abzurufen. Das oben erläuterte Verhalten ist das Verhalten des DefaultHttpControllerSelector, der mit der Web-API geliefert wird, aber Sie können Ihren eigenen Selektor implementieren, um den Standardselektor zu ersetzen und neue Verhaltensweisen zu unterstützen.

Wenn Sie nach "Custom Web Api Controller Selector" googeln, finden Sie viele Beispiele, aber ich finde das für genau Ihr Problem am interessantesten:

  • ASP.NET-Web-API:Verwendung von Namespaces zur Versionierung von Web-APIs

Diese Implementierung ist auch interessant:

  • https://github.com/WebApiContrib/WebAPIContrib/pull/111/files (Danke an Robin van der Knaap für die Aktualisierung dieses defekten Links)

Wie Sie dort sehen, müssen Sie im Wesentlichen:

  • implementieren Sie Ihren eigenen IHttpControllerSelector , das Namespaces berücksichtigt, um die Controller zu finden, und die Namespaces-Routing-Variable, um einen davon auszuwählen.
  • ersetzen Sie den ursprünglichen Selektor durch diesen über die Web-API-Konfiguration.

Ich weiß, dass dies vor einiger Zeit beantwortet wurde und vom ursprünglichen Poster bereits akzeptiert wurde. Wenn Sie jedoch wie ich sind und die Verwendung von Attribut-Routing benötigen und die vorgeschlagene Antwort ausprobiert haben, werden Sie wissen, dass es nicht ganz funktionieren wird.

Als ich dies versuchte, stellte ich fest, dass tatsächlich die Routing-Informationen fehlten, die durch Aufrufen der Erweiterungsmethode MapHttpAttributeRoutes generiert werden sollten derHttpConfiguration Klasse:

config.MapHttpAttributeRoutes();

Das bedeutete, dass die Methode SelectController des Ersatzes IHttpControllerSelector -Implementierung wird nie wirklich aufgerufen und deshalb erzeugt die Anfrage eine http 404-Antwort.

Das Problem wird durch eine interne Klasse namens HttpControllerTypeCache verursacht das ist eine interne Klasse im System.Web.Http Montage unter System.Web.Http.Dispatcher Namensraum. Der fragliche Code ist der folgende:

    private Dictionary<string, ILookup<string, Type>> InitializeCache()
    {
      return this._configuration.Services.GetHttpControllerTypeResolver().GetControllerTypes(this._configuration.Services.GetAssembliesResolver()).GroupBy<Type, string>((Func<Type, string>) (t => t.Name.Substring(0, t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length)), (IEqualityComparer<string>) StringComparer.OrdinalIgnoreCase).ToDictionary<IGrouping<string, Type>, string, ILookup<string, Type>>((Func<IGrouping<string, Type>, string>) (g => g.Key), (Func<IGrouping<string, Type>, ILookup<string, Type>>) (g => g.ToLookup<Type, string>((Func<Type, string>) (t => t.Namespace ?? string.Empty), (IEqualityComparer<string>) StringComparer.OrdinalIgnoreCase)), (IEqualityComparer<string>) StringComparer.OrdinalIgnoreCase);
    }

Sie werden in diesem Code sehen, dass nach dem Typnamen ohne den Namespace gruppiert wird. Die DefaultHttpControllerSelector -Klasse verwendet diese Funktionalität, wenn sie einen internen Cache von HttpControllerDescriptor aufbaut für jeden Controller. Bei Verwendung des MapHttpAttributeRoutes Methode verwendet es eine andere interne Klasse namens AttributeRoutingMapper die Teil des System.Web.Http.Routing ist Namensraum. Diese Klasse verwendet die Methode GetControllerMapping des IHttpControllerSelector um die Routen zu konfigurieren.

Wenn Sie also einen benutzerdefinierten IHttpControllerSelector schreiben dann müssen Sie GetControllerMapping überladen Methode, damit es funktioniert. Der Grund, warum ich dies erwähne, ist, dass keine der Implementierungen, die ich im Internet gesehen habe, dies tut.


Basierend auf der Antwort von @JotaBe habe ich meinen eigenen IHttpControllerSelector entwickelt was Controller erlaubt (in meinem Fall solche, die mit [RoutePrefix] getaggt sind Attribut) mit ihrem vollständigen Namen (Namespace UND Name) zugeordnet werden.

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;

/// <summary>
/// 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.
/// </summary>
public class NamespaceHttpControllerSelector : DefaultHttpControllerSelector
{
    private HttpConfiguration _configuration;
    private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers;

    public NamespaceHttpControllerSelector(HttpConfiguration httpConfiguration) : base(httpConfiguration)
    {
        _configuration = httpConfiguration;
        _controllers = new Lazy<Dictionary<string, HttpControllerDescriptor>>(InitializeControllerDictionary);
    }

    public override IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
    {
        return _controllers.Value; // just cache the list of controllers, so we load only once at first use
    }

    /// <summary>
    /// 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
    /// </summary>
    /// <returns></returns>
    private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
    {
        IAssembliesResolver assembliesResolver = _configuration.Services.GetAssembliesResolver();
        IHttpControllerTypeResolver controllersResolver = _configuration.Services.GetHttpControllerTypeResolver(); 
        ICollection<Type> 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<string, HttpControllerDescriptor>(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;
    }

    /// <summary>
    /// 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
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
    {
        HttpControllerDescriptor controller;
        IDictionary<string, HttpControllerDescriptor> controllers = GetControllerMapping();
        IDictionary<string, HttpControllerDescriptor> 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);
    }

}