Nomi di controller ambigui con attributi di routing:controller con lo stesso nome e spazio dei nomi diverso per il controllo delle versioni

Nomi di controller ambigui con attributi di routing:controller con lo stesso nome e spazio dei nomi diverso per il controllo delle versioni

Innanzitutto, il routing dell'API Web e il routing MVC non funzionano esattamente allo stesso modo.

Il tuo primo collegamento punta al routing MVC, con aree. Le aree non sono ufficialmente supportate per l'API Web, anche se puoi provare a creare qualcosa di simile ad esse. Tuttavia, anche se provi a fare qualcosa del genere, riceverai lo stesso errore, perché il modo in cui l'API Web cerca un controller non tiene conto dello spazio dei nomi del controller.

Quindi, fuori dagli schemi, non funzionerà mai.

Tuttavia, puoi modificare la maggior parte dei comportamenti dell'API Web e questa non è un'eccezione.

L'API Web utilizza un Controller Selector per ottenere il controller desiderato. Il comportamento spiegato sopra è il comportamento di DefaultHttpControllerSelector, fornito con l'API Web, ma puoi implementare il tuo selettore per sostituire quello predefinito e supportare nuovi comportamenti.

Se cerchi su Google "selettore di controller API web personalizzato" troverai molti esempi, ma trovo questo il più interessante esattamente per il tuo problema:

  • API Web ASP.NET:utilizzo degli spazi dei nomi per la versione delle API Web

Anche questa implementazione è interessante:

  • https://github.com/WebApiContrib/WebAPIContrib/pull/111/files (grazie a Robin van der Knaap per l'aggiornamento di questo collegamento interrotto)

Come vedi, in pratica devi:

  • implementa il tuo IHttpControllerSelector , che tiene conto degli spazi dei nomi per trovare i controller e della variabile di instradamento degli spazi dei nomi per sceglierne uno.
  • sostituisci il selettore originale con questo tramite la configurazione dell'API Web.

So che questo ha ricevuto risposta un po' di tempo ed è già stato accettato dal poster originale. Tuttavia, se sei come me e richiedi l'uso del routing degli attributi e hai provato la risposta suggerita, saprai che non funzionerà del tutto.

Quando l'ho provato, ho scoperto che in realtà mancavano le informazioni di routing che avrebbero dovuto essere generate chiamando il metodo di estensione MapHttpAttributeRoutes del HttpConfiguration classe:

config.MapHttpAttributeRoutes();

Ciò significava che il metodo SelectController del sostituto IHttpControllerSelector l'implementazione non viene mai effettivamente chiamata ed è per questo che la richiesta produce una risposta http 404.

Il problema è causato da una classe interna chiamata HttpControllerTypeCache che è una classe interna nel System.Web.Http assemblaggio sotto il System.Web.Http.Dispatcher spazio dei nomi. Il codice in questione è il seguente:

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

Vedrai in questo codice che sta raggruppando in base al nome del tipo senza lo spazio dei nomi. Il DefaultHttpControllerSelector class usa questa funzionalità quando crea una cache interna di HttpControllerDescriptor per ogni controllore. Quando si utilizza il MapHttpAttributeRoutes metodo utilizza un'altra classe interna chiamata AttributeRoutingMapper che fa parte del System.Web.Http.Routing spazio dei nomi. Questa classe usa il metodo GetControllerMapping del IHttpControllerSelector per configurare i percorsi.

Quindi, se hai intenzione di scrivere un IHttpControllerSelector personalizzato quindi devi sovraccaricare il GetControllerMapping metodo per farlo funzionare. Il motivo per cui lo menziono è che nessuna delle implementazioni che ho visto su Internet lo fa.


Sulla base della risposta di @JotaBe ho sviluppato il mio IHttpControllerSelector che consente ai controllori (nel mio caso quelli contrassegnati da [RoutePrefix] attributo) da mappare con il loro nome completo (Spazio dei nomi E nome).

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

}