WebAPI2: Personnaliser le routing

Par défaut

WebApi, le pendant de ASP.net MVC pour la création d’API Rest, est un superbe outil que j’utilise au quotidien. Microsoft a simplifié à l’extrême l’écriture et masqué beaucoup de la machinerie sous-jacente.

Malheureusement, comme à chaque fois que l’on simplifie on perd forcément quelque chose.

Le context

Afin de pouvoir gérer simplement le versioning de mon API Rest proprement, c’est à dire sans mettre V1, V2… dans le nom de mes contrôleurs, mais plutôt dans les namespaces, j’ai cherché une solution facile à mettre en oeuvre et surtout qui soit reproductible et maintenable facilement.

Résolution du contrôleur dans ASP.net MVC

Pour comprendre la problématique que je rencontre, il faut connaître la façon de fonctionner d’ASP.net MVC.

Par défaut, au démarrage de l’application, le moteur récupère toutes les classes de l’AppDomain qui se terminent par Controller. Ensuite à chaque appel, la résolution du contrôleur se fait uniquement par le nom, ainsi si vous avez 2 contrôleurs dont les noms sont identiques mais dont le namespace est différent, il va y avoir conflit.
Par exemple :

  • MonApplication.MonApi.V1.ClientsController
  • MonApplication.MonApi.V2.ClientsController

Malgré les attributs du genre [RoutePrefix("v1/clients")]  de chaque contrôleur ça ne fonctionnera pas, vous obtiendrez systématiquement une erreur 404.

Il va donc falloir modifier le comportement par défaut d’ASP.net MVC et comme le framework est bien fait, c’est relativement simple !

IHttpControllerSelector

IHttpControllerSelector est l’interface qui va nous permettre de nous introduire dans le système de résolution des contrôleurs. Grâce a cette interface, on va pouvoir écrire la logique que nous recherchons : inclure le namespace dans le nom du contrôleur.

Commençons par regarder l’interface :

namespace System.Web.Http.Dispatcher
{
    //
    // Summary:
    //     Defines the methods that are required for an
    //     System.Web.Http.Controllers.IHttpController factory.
    public interface IHttpControllerSelector
    {
        //
        // Summary:
        //     Returns a map, keyed by controller string, of all
        //     System.Web.Http.Controllers.HttpControllerDescriptor
        //     that the selector can select. This is primarily called by
        //     System.Web.Http.Description.IApiExplorer
        //     to discover all the possible controllers in the system.
        //
        // Returns:
        //     A map of all System.Web.Http.Controllers.HttpControllerDescriptor
        //     that the selector
        //     can select, or null if the selector does not have a well-defined
        //     mapping of System.Web.Http.Controllers.HttpControllerDescriptor.
        IDictionary<string, HttpControllerDescriptor> GetControllerMapping();

        //
        // Summary:
        //     Selects a System.Web.Http.Controllers.HttpControllerDescriptor
        //     for the given
        //     System.Net.Http.HttpRequestMessage.
        //
        // Parameters:
        //   request:
        //     The request message.
        //
        // Returns:
        //     An System.Web.Http.Controllers.HttpControllerDescriptor instance.
        HttpControllerDescriptor SelectController(HttpRequestMessage request);
    }
}

Les 2 méthodes de cette interface permettent l’une de construire un dictionnaire de contrôleurs et l’autre de retourner un contrôleur en fonction d’une requête.

Commençons par recenser tous les contrôleurs et leur namespace :

private Dictionary<string, HttpControllerDescriptor>
        InitializeControllerDictionary()
{
    var dictionary =
        new Dictionary<string, HttpControllerDescriptor>(
            StringComparer.OrdinalIgnoreCase);

    var assembliesResolver = configuration.Services.GetAssembliesResolver();
    var controllersResolver =
        configuration.Services.GetHttpControllerTypeResolver();

    var controllerTypes =
        controllersResolver.GetControllerTypes(assembliesResolver);

    foreach (var t in controllerTypes)
    {
        // pour chaque contrôleur, on ajoute le descriptor correspondant
        // dans le dictionnaire
        var key = string.Format(CultureInfo.InvariantCulture,
                                "{0}.{1}", t.Namespace, t.Name);
        dictionary[key] = new HttpControllerDescriptor(
                              configuration, t.Name, t);
    }

    return dictionary;
}

Dans cet extrait, on récupère l’AssemblyResolver qui va nous permettre d’avoir accès à toutes les assemblies de l’application et le ControlerTypeResolver nous retournera que les types « Controller ».

Ainsi, il suffit d’enregistrer dans un dictionnaire les ControllerDescriptor avec pour clé le nom du type préfixé avec le namespace.

Ce dictionnaire sera retourné en résultat de la méthode GetControllerMapping() :

private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> controllers =
   new Lazy<Dictionary<string, HttpControllerDescriptor>>
           (InitializeControllerDictionary);

public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
{
    return controllers.Value;
}

Puis à l’autre bout de la chaîne d’appel, nous allons implémenter la méthode SelectController de l’interface afin de retourner le controller correspondant à l’appel de la requête :

public HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
    // on récupère la liste des sous routes
    var subRoutes = request.GetRouteData().GetSubRoutes();
    if (subRoutes == null || !subRoutes.Any())
    {
        // si aucune route trouvée, c'est que la requête n'est pas
        // gérée par ASP.net MVC
        return null;
        //throw new HttpResponseException(HttpStatusCode.NotFound);
    }

    // on récupère la première route trouvée
    var route = subRoutes.First().Route;

    if (route == null)
    {
        // si aucune route trouvée... c'est que ce n'est toujours
        // pas géré par ASP.net MVC
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }

    // on récupère le nom du contrôleur à partir de l'ActionDescriptor
    // de la route courante
    var actionDescriptors =
        GetRouteVariables<HttpActionDescriptor[]>(route, "actions");
    if(actionDescriptors == null || !actionDescriptors.Any())
    {
       throw new HttpResponseException(HttpStatusCode.NotFound);
    }

    var controllerName = actionDescriptors.First()
            .ControllerDescriptor.ControllerType.FullName;

    HttpControllerDescriptor controllerDescriptor;
    if (controllers.Value.TryGetValue(controllerName, out controllerDescriptor))
    {
        return controllerDescriptor;
    }
    throw new HttpResponseException(HttpStatusCode.NotFound);
}

// Récupère les valeurs stockées dans les DataTokens
private static TResult GetRouteVariables<TResult>(IHttpRoute route, string name)
{
    object result = null;
    if (route.DataTokens.TryGetValue(name, out result))
    {
        return (TResult) result;
    }
    return default(TResult);
}

Un dernier effort pour déclarer notre selector au démarrage de l’application :

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services

        // Web API routes
        config.MapHttpAttributeRoutes();
        config.Services.Replace(typeof(IHttpControllerSelector),
                 new NamespaceHttpControllerSelector(config));

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

Voilà, c’est terminé, votre application peut maintenant gérer des contrôleurs dont les noms divergent uniquement par le namespace.

Vous trouverez ci-joint le code source qui a servi de support à cet article.

Une réflexion sur “WebAPI2: Personnaliser le routing

Votre commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l’aide de votre compte WordPress.com. Déconnexion /  Changer )

Photo Facebook

Vous commentez à l’aide de votre compte Facebook. Déconnexion /  Changer )

Connexion à %s