Routage des attributs dans API Web ASP.NET 2

Le routage est la façon dont l’API web fait correspondre un URI à une action. L’API web 2 prend en charge un nouveau type de routage, appelé routage d’attributs. Comme son nom l’indique, le routage d’attributs utilise des attributs pour définir des itinéraires. Le routage d’attributs vous donne plus de contrôle sur les URI dans votre API web. Par exemple, vous pouvez facilement créer des URI qui décrivent des hiérarchies de ressources.

Le style de routage antérieur, appelé routage basé sur une convention, est toujours entièrement pris en charge. En fait, vous pouvez combiner les deux techniques dans le même projet.

Cette rubrique montre comment activer le routage des attributs et décrit les différentes options pour le routage d’attributs. Pour obtenir un tutoriel de bout en bout qui utilise le routage d’attributs, consultez Créer une API REST avec le routage d’attributs dans l’API web 2.

Prérequis

Visual Studio 2017 Édition Communauté, Professionnel ou Entreprise

Vous pouvez également utiliser le Gestionnaire de package NuGet pour installer les packages nécessaires. Dans le menu Outils de Visual Studio, sélectionnez Gestionnaire de package NuGet, puis console du gestionnaire de package. Entrez la commande suivante dans la fenêtre Console du Gestionnaire de package :

Install-Package Microsoft.AspNet.WebApi.WebHost

Pourquoi le routage d’attributs ?

La première version de l’API web utilisait le routage basé sur une convention . Dans ce type de routage, vous définissez un ou plusieurs modèles de routage, qui sont essentiellement des chaînes paramétrables. Lorsque l’infrastructure reçoit une requête, elle correspond à l’URI par rapport au modèle de routage. Pour plus d’informations sur le routage basé sur les conventions, consultez Routage dans API Web ASP.NET.

L’un des avantages du routage basé sur des conventions est que les modèles sont définis dans un emplacement unique et que les règles de routage sont appliquées de manière cohérente sur tous les contrôleurs. Malheureusement, le routage basé sur une convention rend difficile la prise en charge de certains modèles d’URI qui sont courants dans les API RESTful. Par exemple, les ressources contiennent souvent des ressources enfants : les clients ont des commandes, les films ont des acteurs, les livres ont des auteurs, etc. Il est naturel de créer des URI qui reflètent ces relations :

/customers/1/orders

Ce type d’URI est difficile à créer à l’aide du routage basé sur une convention. Bien que cela puisse être fait, les résultats ne sont pas bien mis à l’échelle si vous avez de nombreux contrôleurs ou types de ressources.

Avec le routage d’attributs, il est trivial de définir un itinéraire pour cet URI. Il vous suffit d’ajouter un attribut à l’action contrôleur :

[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }

Voici d’autres modèles que le routage d’attributs facilite.

Contrôle de version d’API

Dans cet exemple, « /api/v1/products » est acheminé vers un contrôleur différent de « /api/v2/products ».

/api/v1/products /api/v2/products

Segments d’URI surchargés

Dans cet exemple, « 1 » est un numéro de commande, mais « en attente » correspond à une collection.

/orders/1 /orders/pending

Plusieurs types de paramètres

Dans cet exemple, « 1 » est un numéro de commande, mais « 2013/06/16 » spécifie une date.

/orders/1 /orders/2013/06/16

Activation du routage des attributs

Pour activer le routage d’attributs, appelez MapHttpAttributeRoutes pendant la configuration. Cette méthode d’extension est définie dans la classe System.Web.Http.HttpConfigurationExtensions .

using System.Web.Http;

namespace WebApplication
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API routes
            config.MapHttpAttributeRoutes();

            // Other Web API configuration not shown.
        }
    }
}

Le routage d’attributs peut être combiné avec le routage basé sur une convention . Pour définir des itinéraires basés sur des conventions, appelez la méthode MapHttpRoute .

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Attribute routing.
        config.MapHttpAttributeRoutes();

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

Pour plus d’informations sur la configuration de l’API web, consultez Configuration de API Web ASP.NET 2.

Remarque : Migration à partir de l’API web 1

Avant l’API web 2, les modèles de projet d’API web généraient du code comme suit :

protected void Application_Start()
{
    // WARNING - Not compatible with attribute routing.
    WebApiConfig.Register(GlobalConfiguration.Configuration);
}

Si le routage d’attribut est activé, ce code lève une exception. Si vous mettez à niveau un projet d’API web existant pour utiliser le routage d’attributs, veillez à mettre à jour ce code de configuration comme suit :

protected void Application_Start()
{
    // Pass a delegate to the Configure method.
    GlobalConfiguration.Configure(WebApiConfig.Register);
}

Notes

Pour plus d’informations, consultez Configuration de l’API web avec ASP.NET Hosting.

Ajout d’attributs de route

Voici un exemple d’itinéraire défini à l’aide d’un attribut :

public class OrdersController : ApiController
{
    [Route("customers/{customerId}/orders")]
    [HttpGet]
    public IEnumerable<Order> FindOrdersByCustomer(int customerId) { ... }
}

La chaîne « customers/{customerId}/orders » est le modèle d’URI de l’itinéraire. L’API web tente de faire correspondre l’URI de demande au modèle. Dans cet exemple, « customers » et « orders » sont des segments littérals, et « {customerId} » est un paramètre variable. Les URI suivants correspondent à ce modèle :

  • http://localhost/customers/1/orders
  • http://localhost/customers/bob/orders
  • http://localhost/customers/1234-5678/orders

Vous pouvez restreindre la correspondance à l’aide de contraintes, décrites plus loin dans cette rubrique.

Notez que le paramètre « {customerId} » dans le modèle d’itinéraire correspond au nom du paramètre customerId dans la méthode. Lorsque l’API web appelle l’action du contrôleur, elle tente de lier les paramètres d’itinéraire. Par exemple, si l’URI est http://example.com/customers/1/orders, l’API web tente de lier la valeur « 1 » au paramètre customerId dans l’action.

Un modèle d’URI peut avoir plusieurs paramètres :

[Route("customers/{customerId}/orders/{orderId}")]
public Order GetOrderByCustomer(int customerId, int orderId) { ... }

Toutes les méthodes de contrôleur qui n’ont pas d’attribut de routage utilisent le routage basé sur une convention. De cette façon, vous pouvez combiner les deux types de routage dans le même projet.

HTTP Methods

L’API web sélectionne également des actions en fonction de la méthode HTTP de la requête (GET, POST, etc.). Par défaut, l’API web recherche une correspondance qui ne respecte pas la casse avec le début du nom de la méthode du contrôleur. Par exemple, une méthode de contrôleur nommée PutCustomers correspond à une requête HTTP PUT.

Vous pouvez remplacer cette convention en décorant la méthode avec l’un des attributs suivants :

  • [HttpDelete]
  • [HttpGet]
  • [HttpHead]
  • [HttpOptions]
  • [HttpPatch]
  • [HttpPost]
  • [HttpPut]

Dans l’exemple suivant, l’API web mappe la méthode CreateBook aux requêtes HTTP POST.

[Route("api/books")]
[HttpPost]
public HttpResponseMessage CreateBook(Book book) { ... }

Pour toutes les autres méthodes HTTP, y compris les méthodes non standard, utilisez l’attribut AcceptVerbs , qui prend une liste de méthodes HTTP.

// WebDAV method
[Route("api/books")]
[AcceptVerbs("MKCOL")]
public void MakeCollection() { }

Préfixes de routage

Souvent, les itinéraires d’un contrôleur commencent tous par le même préfixe. Par exemple :

public class BooksController : ApiController
{
    [Route("api/books")]
    public IEnumerable<Book> GetBooks() { ... }

    [Route("api/books/{id:int}")]
    public Book GetBook(int id) { ... }

    [Route("api/books")]
    [HttpPost]
    public HttpResponseMessage CreateBook(Book book) { ... }
}

Vous pouvez définir un préfixe commun pour un contrôleur entier à l’aide de l’attribut [RoutePrefix] :

[RoutePrefix("api/books")]
public class BooksController : ApiController
{
    // GET api/books
    [Route("")]
    public IEnumerable<Book> Get() { ... }

    // GET api/books/5
    [Route("{id:int}")]
    public Book Get(int id) { ... }

    // POST api/books
    [Route("")]
    public HttpResponseMessage Post(Book book) { ... }
}

Utilisez un tilde (~) sur l’attribut de méthode pour remplacer le préfixe d’itinéraire :

[RoutePrefix("api/books")]
public class BooksController : ApiController
{
    // GET /api/authors/1/books
    [Route("~/api/authors/{authorId:int}/books")]
    public IEnumerable<Book> GetByAuthor(int authorId) { ... }

    // ...
}

Le préfixe d’itinéraire peut inclure des paramètres :

[RoutePrefix("customers/{customerId}")]
public class OrdersController : ApiController
{
    // GET customers/1/orders
    [Route("orders")]
    public IEnumerable<Order> Get(int customerId) { ... }
}

Contraintes de routage

Les contraintes de routage vous permettent de restreindre la façon dont les paramètres du modèle d’itinéraire sont mis en correspondance. La syntaxe générale est « {parameter:constraint} ». Par exemple :

[Route("users/{id:int}")]
public User GetUserById(int id) { ... }

[Route("users/{name}")]
public User GetUserByName(string name) { ... }

Ici, la première route n’est sélectionnée que si le segment « id » de l’URI est un entier. Dans le cas contraire, la deuxième route sera choisie.

Le tableau suivant répertorie les contraintes prises en charge.

Contrainte Description Exemple
alpha Correspond aux caractères majuscules ou minuscules de l’alphabet latin (a-z, A-Z) {x:alpha}
bool Correspond à une valeur booléenne. {x:bool}
DATETIME Correspond à une valeur DateTime . {x:datetime}
Décimal Correspond à une valeur décimale. {x:decimal}
double Correspond à une valeur à virgule flottante 64 bits. {x:double}
float Correspond à une valeur à virgule flottante 32 bits. {x:float}
guid Correspond à une valeur GUID. {x:guid}
int Correspond à une valeur entière 32 bits. {x:int}
length Correspond à une chaîne avec la longueur spécifiée ou dans une plage de longueurs spécifiée. {x:length(6)} {x:length(1,20)}
long Correspond à une valeur entière 64 bits. {x:long}
max Correspond à un entier avec une valeur maximale. {x:max(10)}
Maxlength Correspond à une chaîne d’une longueur maximale. {x:maxlength(10)}
minute(s) Correspond à un entier avec une valeur minimale. {x:min(10)}
minlength Correspond à une chaîne d’une longueur minimale. {x:minlength(10)}
range Correspond à un entier dans une plage de valeurs. {x:range(10,50)}
regex Correspond à une expression régulière. {x:regex(^\d{3}-\d-\d{4}{3}$)}

Notez que certaines contraintes, telles que « min », prennent des arguments entre parenthèses. Vous pouvez appliquer plusieurs contraintes à un paramètre, séparé par un signe deux-points.

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { ... }

Contraintes d’itinéraire personnalisé

Vous pouvez créer des contraintes de routage personnalisées en implémentant l’interface IHttpRouteConstraint . Par exemple, la contrainte suivante limite un paramètre à une valeur entière autre que zéro.

public class NonZeroConstraint : IHttpRouteConstraint
{
    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, 
        IDictionary<string, object> values, HttpRouteDirection routeDirection)
    {
        object value;
        if (values.TryGetValue(parameterName, out value) && value != null)
        {
            long longValue;
            if (value is long)
            {
                longValue = (long)value;
                return longValue != 0;
            }

            string valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
            if (Int64.TryParse(valueString, NumberStyles.Integer, 
                CultureInfo.InvariantCulture, out longValue))
            {
                return longValue != 0;
            }
        }
        return false;
    }
}

Le code suivant montre comment inscrire la contrainte :

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var constraintResolver = new DefaultInlineConstraintResolver();
        constraintResolver.ConstraintMap.Add("nonzero", typeof(NonZeroConstraint));

        config.MapHttpAttributeRoutes(constraintResolver);
    }
}

Vous pouvez maintenant appliquer la contrainte dans vos itinéraires :

[Route("{id:nonzero}")]
public HttpResponseMessage GetNonZero(int id) { ... }

Vous pouvez également remplacer l’ensemble de la classe DefaultInlineConstraintResolver en implémentant l’interface IInlineConstraintResolver . Cela remplacera toutes les contraintes intégrées, sauf si votre implémentation d’IInlineConstraintResolver les ajoute spécifiquement.

Paramètres d’URI facultatifs et valeurs par défaut

Vous pouvez rendre un paramètre URI facultatif en ajoutant un point d’interrogation au paramètre d’itinéraire. Si un paramètre de route est facultatif, vous devez définir une valeur par défaut pour le paramètre de méthode.

public class BooksController : ApiController
{
    [Route("api/books/locale/{lcid:int?}")]
    public IEnumerable<Book> GetBooksByLocale(int lcid = 1033) { ... }
}

Dans cet exemple, /api/books/locale/1033 et /api/books/locale retournez la même ressource.

Vous pouvez également spécifier une valeur par défaut à l’intérieur du modèle d’itinéraire, comme suit :

public class BooksController : ApiController
{
    [Route("api/books/locale/{lcid:int=1033}")]
    public IEnumerable<Book> GetBooksByLocale(int lcid) { ... }
}

Cela est presque identique à l’exemple précédent, mais il existe une légère différence de comportement lorsque la valeur par défaut est appliquée.

  • Dans le premier exemple (« {lcid:int?} »), la valeur par défaut 1033 est affectée directement au paramètre de méthode, de sorte que le paramètre aura cette valeur exacte.
  • Dans le deuxième exemple (« {lcid:int=1033} »), la valeur par défaut « 1033 » passe par le processus de liaison de modèle. Le classeur de modèles par défaut convertit « 1033 » en valeur numérique 1033. Toutefois, vous pouvez brancher un classeur de modèles personnalisé, ce qui peut faire quelque chose de différent.

(Dans la plupart des cas, sauf si vous avez des classeurs de modèles personnalisés dans votre pipeline, les deux formulaires seront équivalents.)

Noms de routes

Dans l’API web, chaque itinéraire a un nom. Les noms de routes sont utiles pour générer des liens, de sorte que vous pouvez inclure un lien dans une réponse HTTP.

Pour spécifier le nom de l’itinéraire, définissez la propriété Name sur l’attribut . L’exemple suivant montre comment définir le nom de l’itinéraire et comment utiliser le nom de la route lors de la génération d’un lien.

public class BooksController : ApiController
{
    [Route("api/books/{id}", Name="GetBookById")]
    public BookDto GetBook(int id) 
    {
        // Implementation not shown...
    }

    [Route("api/books")]
    public HttpResponseMessage Post(Book book)
    {
        // Validate and add book to database (not shown)

        var response = Request.CreateResponse(HttpStatusCode.Created);

        // Generate a link to the new book and set the Location header in the response.
        string uri = Url.Link("GetBookById", new { id = book.BookId });
        response.Headers.Location = new Uri(uri);
        return response;
    }
}

Ordre de routage

Lorsque le framework tente de faire correspondre un URI à un itinéraire, il évalue les itinéraires dans un ordre particulier. Pour spécifier l’ordre, définissez la propriété Order sur l’attribut route. Les valeurs inférieures sont évaluées en premier. La valeur de l’ordre par défaut est zéro.

Voici comment le classement total est déterminé :

  1. Comparez la propriété Order de l’attribut route.

  2. Examinez chaque segment d’URI dans le modèle d’itinéraire. Pour chaque segment, triez comme suit :

    1. Segments littérals.
    2. Router les paramètres avec des contraintes.
    3. Paramètres de routage sans contraintes.
    4. Segments de paramètres génériques avec des contraintes.
    5. Segments de paramètres génériques sans contraintes.
  3. Dans le cas d’une liaison, les itinéraires sont classés par une comparaison de chaîne ordinale sans respect de la casse (OrdinalIgnoreCase) du modèle d’itinéraire.

Voici un exemple. Supposons que vous définissez le contrôleur suivant :

[RoutePrefix("orders")]
public class OrdersController : ApiController
{
    [Route("{id:int}")] // constrained parameter
    public HttpResponseMessage Get(int id) { ... }

    [Route("details")]  // literal
    public HttpResponseMessage GetDetails() { ... }

    [Route("pending", RouteOrder = 1)]
    public HttpResponseMessage GetPending() { ... }

    [Route("{customerName}")]  // unconstrained parameter
    public HttpResponseMessage GetByCustomer(string customerName) { ... }

    [Route("{*date:datetime}")]  // wildcard
    public HttpResponseMessage Get(DateTime date) { ... }
}

Ces itinéraires sont classés comme suit.

  1. commandes/détails
  2. orders/{id}
  3. orders/{customerName}
  4. commandes/{*date}
  5. commandes/en attente

Notez que « details » est un segment littéral qui apparaît avant « {id} », mais que « en attente » apparaît en dernier, car la propriété Order est 1. (Cet exemple suppose qu’il n’existe aucun client nommé « détails » ou « en attente ». En général, essayez d’éviter les itinéraires ambigus. Dans cet exemple, un meilleur modèle d’itinéraire pour GetByCustomer est « customers/{customerName} » )