Aller au contenu

Les bases fondamentales pour la création d'un service Web API

Introduction

Une API Web (Application Programming Interface exposée sur le Web) permet à des applications de communiquer entre elles au travers du protocole HTTP. Elle expose un ensemble de points d'entrée, appelés endpoints, auxquels des clients Web peuvent envoyer des requêtes via le protocole HTTP, afin d'obtenir ou agir sur les données.

Dans l'écosystème .NET, ASP.NET Core fournit l'infrastructure nécessaire à la création de services Web API. Il prend en charge la réception et le traitement des requêtes HTTP, le routage vers les endpoints appropriés, l'injection de dépendances, l'exécution d'une pipeline de middlewares ainsi que la construction des réponses HTTP retournées aux clients. La création d'une API ne consiste donc pas uniquement à implémenter une méthode répondant à une requête HTTP. Plusieurs mécanismes collaborent pour permettre le traitement d'une requête, depuis sa réception par l'application jusqu'à l'envoi de la réponse au client.

Dans cet article, nous allons créer progressivement un service Web API avec ASP.NET Core afin d'en découvrir les principaux mécanismes. Nous verrons notamment comment :

  • Le cycle de traitement d'une requête HTTP

  • Créer et configurer une application ASP.NET Core

  • Implémenter un endpoint

  • Comprendre le fonctionnement du routage

  • Comprendre le rôle des middlewares HTTP

Nous nous appuierons sur un exemple volontairement simple, afin de nous concentrer sur les mécanismes fondamentaux d'ASP.NET Core et comprendre comment ils collaborent entre eux.

Cycle de vie d'une requête HTTP

Les échanges entre le client et l'application côté serveur reposent sur le protocole HTTP. Côté serveur plusieurs étapes sont exécutées :

Cycle de vie d'une requête HTTP

Dans les prochains chapitres de cet article, nous aborderons chacune de ces étapes.

Création d'un projet Web API ASP.NET Core

Avant de présenter chacune des étapes du cycle de vie, il est nécessaire de créer un projet ASP.NET Core Web API. Celui-ci constitue l'application qui va héberger nos services Web API avec leurs endpoints, recevoir les requêtes HTTP envoyées par les clients et retourner les réponses correspondantes. Un projet peut être créé depuis Visual Studio ou directement avec une commande de la CLI .NET :

dotnet new webapi -n ProductApi

Cette commande crée une application ASP.NET Core contenant la structure et les dépendances nécessaires à la mise en place d'une API Web.

Point d'entrée de l'application

Comme dans toute application .NET, une application ASP.NET Core possède un point d'entrée. Celui-ci est implémenté dans le fichier Program.cs.

La première phase consiste à préparer l'application et enregistrer les services dont elle aura besoin. La seconde consiste à construire et configurer l'application qui traitera les requêtes HTTP, avant de la démarrer :

Cycle de vie d'une requête HTTP

Grâce au concept du top-level statements, il n'est plus nécessaire de déclarer explicitement une classe Program et une méthode Main. Dans notre application, nous pouvons partir de la configuration minimale suivante :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.MapControllers();

app.Run();

Cette implémentation contient les principales étapes nécessaires au démarrage de notre service Web API.

Création de l'application

La première instruction crée un objet de type WebApplicationBuilder :

var builder = WebApplication.CreateBuilder(args);

Cet objet permet de préparer et de configurer l'application avant son démarrage. Il donne notamment accès à la configuration de l'application, au système de journalisation ainsi qu'au conteneur d'injection de dépendances. Nous utiliserons notamment sa propriété Services pour enregistrer les différents services nécessaires à notre application :

builder.Services.AddControllers();

Cette méthode enregistre dans le conteneur d'injection de dépendances les services nécessaires au fonctionnement des contrôleurs ASP.NET Core.

Construction de l'application

Une fois les différents services configurés, l'application peut être construite :

var app = builder.Build();

L'objet de type WebApplication obtenu représente l'application ASP.NET Core qui va traiter les requêtes HTTP. À partir de cet objet, nous pouvons configurer la pipeline de traitement des requêtes et déclarer les endpoints exposés par notre Web API. Dans notre exemple, nous indiquons que les actions définies dans nos contrôleurs doivent être exposées en tant que endpoints :

app.MapControllers();

Nous reviendrons plus en détail sur le routage et le rôle de la méthode MapControllers lorsque nous créerons notre premier contrôleur.

Démarrage de l'application

Enfin, l'instruction suivante démarre l'application :

app.Run();

L'application peut alors recevoir et traiter les requêtes HTTP qui lui sont adressées.

Configuration de l'application et conteneur d'injection de dépendances

Lors de la création d'une application ASP.NET Core, il est nécessaire de configurer les services dont l'application aura besoin pour traiter les requêtes qui lui seront envoyées. Ces services sont enregistrés dans le conteneur d'injection de dépendances fourni nativement par ASP.NET Core. Cette configuration est réalisée avant la construction de l'application, principalement à partir de la collection builder.Services, propriété de type IServiceCollection qui représente la collection des services qui seront disponibles dans le conteneur d'injection de dépendances.

Enregistrement des services nécessaires aux contrôleurs

Dans notre configuration minimale, nous avons déjà enregistré des services :

builder.Services.AddControllers();

La méthode AddControllers() ajoute dans la collection les services nécessaires au fonctionnement des contrôleurs ASP.NET Core. Nous pouvons également utiliser cette collection pour enregistrer les services propres à notre application. Par exemple, si notre API utilise un service chargé de gérer des produits, nous codons l'interface suivante :

public interface IProductService
{
    ProductDto? GetById(long id);
}

et son implémentation associée :

public class ProductService : IProductService
{
    public ProductDto? GetById(long id)
    {
        // Logique applicative / métier
    }
}

Le service peut alors être enregistré dans le conteneur :

builder.Services.AddScoped<IProductService, ProductService>();

Cet enregistrement doit être implémenté avant la construction de l'application. Il indique au conteneur que lorsqu'un composant de l'application demande une instance de l'interface IProductService, une instance de ProductService doit lui être fournie. La méthode AddScoped indique qu'une même instance du service sera utilisée pendant toute la durée d'un scope. Dans une application ASP.NET Core, une requête HTTP correspond généralement à un scope.

Injection des dépendances

L'intérêt du conteneur apparaît lorsque les endpoints de notre service Web API auront besoin d'obtenir un produit :

[ApiController]
[Route("api/product")]
public class ProductController(IProductService productService)
    : ControllerBase
{
    // ...
}

Lorsqu'ASP.NET Core crée une instance du contrôleur ProductController, le conteneur examine les dépendances demandées par son constructeur. Il constate qu'une instance de IProductService est nécessaire et la lui fournit. Cette approche permet notamment de réduire le couplage entre les classes et facilite le remplacement de l'implémentation par une autre, ainsi que l'implémentation de tests unitaires.

Pour en apprendre davantage sur les injections de dépendances, je vous invite à lire cet article.

Implémentation d'un endpoint

Une fois notre application ASP.NET Core configurée, nous pouvons exposer des fonctionnalités accessibles aux applications clientes. Pour ce faire, une Web API expose des endpoints. Un endpoint représente un point d'entrée de l'application pouvant être appelé au travers d'une requête HTTP. Il est notamment caractérisé par :

  • Une route

  • Un verbe HTTP (GET, POST, PUT, PATCH, DELETE, etc.)

  • Une méthode chargée de traiter la requête

  • Une réponse HTTP

Dans notre Web API, un client doit pouvoir récupérer un produit à partir de son identifiant en envoyant la requête suivante :

GET /api/product/42

Implémentation du contrôleur

Un contrôleur regroupe un ensemble d'actions permettant de traiter les requêtes HTTP associées à une ressource ou à une fonctionnalité de notre API. Voici son implémentation :

[ApiController]
[Route("api/product")]
public class ProductController(IProductService productService) : ControllerBase
{

}

L'attribut [ApiController] indique que le contrôleur est destiné à être utilisé dans le cadre d'un service Web API. Il active également plusieurs comportements facilitant son implémentation, notamment concernant la liaison et la validation des données reçues.

L'attribut [Route] définit la route associée au contrôleur.

Le contrôleur hérite de la classe ControllerBase, qui fournit les fonctionnalités nécessaires à l'implémentation d'un contrôleur Web API, notamment des méthodes permettant de construire des réponses HTTP telles que Ok(), NotFound(), BadRequest() ou encore Problem().

Implémentation d'une action

Ajoutons une méthode permettant d'obtenir un produit :

[HttpGet("{id:long}")]
public IActionResult Get(long id)
{
    var product = productService.GetById(id);

    return Ok(product);
}

L'attribut :

[HttpGet("{id:long}")]

indique que cette action répond aux requêtes utilisant le verbe HTTP GET.

Un endpoint ne devrait pas porter toute la logique métier

Une action d'un contrôleur doit prendre en charge le traitement de la requête. Une fois que le routage a défini l'action à exécuter, il n'est pas de la responsabilité de cette action d'exécuter directement les traitements nécessaires. Ceux-ci sont pris en charge par un ou plusieurs services applicatifs, accessibles grâce aux injections de dépendance. Cependant, il est de sa responsabilité de construire la réponse HTTP.

La pipeline HTTP et les middlewares

Lorsqu'une requête HTTP est envoyée à une application ASP.NET Core, elle n'est pas directement transmise au contrôleur chargé de la traiter. Dans son cycle de vie, avant cette étape, elle traverse une pipeline HTTP composée d'une succession de middlewares.

Un middleware est un composant capable d'intervenir lors du traitement d'une requête et / ou réponse HTTP :

pipeline HTTP et les middlewares

Les middlewares permettent d'exécuter des traitements qui ne sont pas spécifiques à un endpoint particulier. Par exemple, ils peuvent intervenir dans la gestion des erreurs, les redirections HTTPS, l'authentification, les autorisations, la journalisation, l'accès aux fichiers statiques et la vérification des CORS.

Configuration de la pipeline HTTP

La pipeline est configurée dans le fichier Program.cs, après la construction de l'application :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

// Configuration de la pipeline HTTP

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

L'ordre dans lequel ces middlewares sont ajoutés est important, car il détermine l'ordre dans lequel ils interviennent, à la fois lors du traitement de la requête et de la réponse HTTP.

Fonctionnement d'un middleware

Pour comprendre le fonctionnement de la pipeline, nous pouvons implémenter un middleware in-line dans le fichier Program.cs :

app.Use(async (context, next) =>
{
    Console.WriteLine("Avant le middleware suivant");

    await next();

    Console.WriteLine("Après le middleware suivant");
});

Les deux paramètres jouent un rôle important :

  • Le premier est context qui représente le contexte de la requête HTTP en cours d'exécution. Il permet notamment d'accéder à la requête via sa propriété Request et à la réponse via sa propriété Response

  • Le second est next qui permet d'appeler le composant suivant dans la pipeline

Le middleware peut donc exécuter un traitement avant l'appel du middleware suivant, puis reprendre son exécution une fois que celui-ci a terminé son traitement.

Routage de la requête

Lorsque l'application reçoit une requête HTTP, le service de routage d'ASP.NET Core recherche l'endpoint correspondant au verbe HTTP et à la route communiqués :

Routage et requête HTTP

La contrainte :long présente dans la route :

"{id:long}"

indique que cette partie de la route doit pouvoir être interprétée comme une valeur de type long. Le nom id correspond également au paramètre de notre méthode :

public IActionResult Get(long id)

ASP.NET Core récupère donc la valeur présente dans l'URL et la fournit automatiquement à la méthode lors de son exécution.

Construction de la réponse HTTP

Une action ne se contente pas d'exécuter un traitement. Elle indique au client le résultat de ce traitement au travers d'une réponse HTTP :

return Ok(product);

La méthode Ok() construit une réponse HTTP avec le code de statut :

200 OK

et le produit constitue le corps de la réponse. ASP.NET Core se charge ensuite de sérialiser cet objet, généralement en JSON (il est possible de choisir XML). Dans notre exemple, le client recevra :

{
  "id": 44,
  "name": "Clavier",
  "price": 69.79
}

Un endpoint peut naturellement retourner différents codes de statut HTTP selon le résultat du traitement. Si aucun produit ne correspond à l'identifiant demandé, nous pourrions par exemple retourner une réponse avec l'instruction NotFound() :

[HttpGet("{id:long}")]
public IActionResult Get(long id)
{
    var product = productService.GetById(id);

    return product is null
         ? NotFound()
         : Ok(product);
}

La réponse envoyée au client est :

404 Not Found

Rôle de la méthode MapControllers()

Pour que les actions des contrôleurs puissent être considérées comme endpoints, nous avons ajouté dans le fichier Program.cs :

app.MapControllers();

Cette instruction ajoute au système de routage les endpoints correspondant aux actions des contrôleurs utilisant le routage par attributs.