Créer une API RestFUL en ASP.NET Core 2

Comment Créer une API en ASP.NET Core 2 - Les différentes approches

Bonjour,

Bienvenue dans un monde fascinant des WEB API en ASP.NET Core 2. Microsoft a beaucoup facilité la création de Web API et aujourd'hui beaucoup d'architecture moderne les implémentent mais savez-vous réellement créer une Web API qui sera RestFUL, documentée et testée ?

L'approche de cet article se veut didacticiel. Ainsi nous partirons de la création d'une simple API de type Rest, nous lui ajouterons les verbes HTTP pour devenir compatible RestFul. Une fois notre API RestFul, nous utiliserons Swagger pour la documenter et NSwag pour générer le client et en fin nous utiliserons XUnit pour créer des projets de test unitaire et d'intégration.

Qu'est-ce qu'une API ?

Une API (Application Programming Interface ou interface de programmation) est un ensemble de services offert par un logiciel à d'autres logiciels. Grâce aux API, les programmes informatiques peuvent interagir entre eux selon des conditions déterminées.

Aujourd'hui pour créer une API, il existe différentes styles d'API Web.

Les différentes démarches de développement

Il existe aujourd'hui deux méthodes pour créer une API Web RestFul en ASP.NET Core 2.1

La méthode proposée par cet article, est la méthode appelée code first où l'écriture du code se fait en premier, puis la documentation est générée automatiquement pour en fin permettre de générer le client SDK.

Il est cependant possible de générer la documentation au préalable permettant de donner un cadre à notre API et ainsi permettre une collaboration plus rapide entre les développeurs qui développent l'API et les développeurs qui consomment l'API. Cette démarche est appelée Spec-first

Les styles d'API Web

Comme je l'ai indiqué dans mon livre Learning ASP.NET Core 2.0 édité par Packt,, les API Web ont différentes styles et nous pouvons cités les styles suivantes

L'API Rest 

REST est un style d'architecture défini dans la thèse de Roy Fielding dans les années 2000, ce n'est donc ni un protocole ni un format. Les implémentations sont donc multiples et différentes. Cependant, on retrouve souvent le principe dans les API HTTP, comme c'est le cas de GitHub et Twitter par exemple.

L'API RestFul

L'API Restful est une API rest qui comporte l'ensemble des verbes HTTP

Les différents verbes

VerbeDescription
GETPermet d'obtenir une liste de ressources
GET/{id}Permet d'obtenir une ressource
POSTPermet de créer une ressource
PUTPermet de remplacer une ressource
PATCHPermet de remplacer une partie de la ressource
DELETEPermet de supprimer une ressource

L'API Hateoas

pour Hypermedia As The Engine Of Application State, est la contrainte (4.4) qui est la plus présente sur le web mais malheureusement pas souvent dans les APIs. Arrivé à ce niveau, on reconnaît évidemment que le web est bien ancré sur le principe de REST, à quelques exceptions près. Lorsque l'on obtient une ressource, ou une page sur le web, il est très important de la lier à d'autres ressources via des liens. C'est aussi bête que ça.

Cet article ne traite pas des API, si vous souhaitez savoir comment créer une API Hateoas, je vous invite à lire mon livre sur Learning ASP.NET Core 2

Créer une API Web Rest Simple

Architecture de l'API

Premier Exemple - Api Simple

Voir le fichier SimpleAPIController.cs 
https://github.com/mbt-versusmind/APIRest/blob/master/SimpleAPI1.cs

Dans ce premier exemple, nous jouons uniquement avec les premiers verbes HTTP

VerbeDescription
HTTPGetRécupérer une ressource ou une liste
HTTPPostCréer une ressource

Nous avons décorer décoré notre classe API avec les décorateurs suivants

DécorateurDescription
[Produces("application/json")]Définit le type de contenu accepté et retournée par notre API ici Json
[Route("api/category")]Définit le path de notre API ici api/category donc les url seront de type http://monserver/api/category
[ValidateModel]Appel une action custom permettant de valider les paramétres voir ci-dessous

Ajouter une action de validation généralisée

Bien qu'il soit possible d'appeler directement le mécanisme de Validation fournit par .NET Core 2, il est recommandé d'utiliser une Action filtre permettant d'abstraire le code et d'éviter d'avoir plusieurs responsabilités dans chaque action. Ce filtre peut être appelé directement depuis la classe API et donc être appliqué à l'ensemble des actions ou alors être appelé depuis une seule action

public class ValidateModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            context.Result = new BadRequestObjectResult(context.ModelState);
        }
    }
}

Ajouter un contrat à notre API

Ici au lieu de retourner un POCO fortement typé, nous avons préféré utiliser un retour Standard pour l'ensemble de nos actions

IActionResult

et d'utiliser des code HTTP différents pour le type de résultat

CodeObjetContenu
200OkObjectResultData
400BadRequestObjectResultMessage descrivant l'erreur

 

Quelque limites à ce code

- Le client SDK va devoir gérer le code Retour pour savoir si le traitement s'est bien passé et lire le contenu

- Nous confondons là, les erreurs HTTP techniques et les erreurs fonctionnelles

En régle générale

Il est fortement recommendé de ne pas jouer avec les codes HTTP lorsque ce sont des erreurs fonctionnelles mais d'utiliser un POCO indiquant clairement si l'opération s'est bien passé fonctionnellement ou non et de retourner un message descriptif. Les code HTTP doivent être uniquement servir pour les status techniques

Transformons notre API Simple en API Fortement typée

Créons une Interface

Voir le fichier https://github.com/mbt-versusmind/APIRest/blob/master/ICategoryApiController.cs

Modifions maintenant notre API pour utiliser l'interface

Voir le fichier https://github.com/mbt-versusmind/APIRest/blob/master/SimpleAPI2.cs

Cool maintenant nous avons une API fortement typée.

Transformons notre API Fortement typée en API RestFull

Voyons maintenant un peu plus loin et essayons d'être compatible avec RestFUL.

Tout d'abord qu'est-ce que RestFUL.

RestFUL est un standard basé sur REST et prenant en compte les régles suivantes

1°) Les Verbes HTTP doivent être utilisés

2°) Gérer Les resultats des opérations de retour correctement

3°) Notre API doit être décrite et documentée

Transformons notre API pour utiliser les verbes HTTP

VerbeUtilisation
GETRécupérer la liste des ressources
GET("{id}")Récupérer une seule ressource
POSTCréer une ressource
PUTModifier une ressource en entier
PATCHModifier une partie de la ressource
DELETESupprimer une ressource

Ici, avec notre exemple d'API simple nous avons déjà implémenté les verbes GET, GET("{id}") et POST. Nous allons ajouter les méthodes de modification et mise à jour partielle et de suppression

Commençons par modifier notre Interface

Voir le fichier https://github.com/mbt-versusmind/APIRest/blob/master/ICategoryApiControllerRestFull.cs

Modifions maintenant notre API pour implémenter les méthodes manquantes

Voir le fichier https://github.com/mbt-versusmind/APIRest/blob/master/CategoryApiControllerFullRest.cs

Cool nous avons maintenant une API qui fait appel à tous les verbes HTTP existants, voyons maintenant la deuxième régle

Gérer Les resultats des opérations de retour correctement

2.1 - Opération de création

Une Opération de création doit avoir pour resultat un statut 201 et disposer dans le header un attribut Location possédant l'URL. Pour ce faire nous pouvons ajouter une classe CreatedActionResultFilterAttribute

public class CreatedActionResultFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext context)
    {
        if (context.Result is ObjectResult)
        {
            var objRes = context.Result as ObjectResult;

            if (objRes.Value is RequestResultWithData)
            {
                RequestResultWithData requestResultWithData = objRes.Value as RequestResultWithData;

                context.HttpContext.Response.Headers["Location"] = requestResultWithData.Location;

                if (requestResultWithData.IsSuccess)
                    context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Created;
                else
                    context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadGateway;
            }
        }

        base.OnActionExecuted(context);
    }
}

et de décorer nos actions d'insertion

/// <inheritdoc />
[HttpPost(""), CreatedActionResultFilter]
public async Task<RequestResultWithData<Category>> AddAsync(Category Category)
{

2.2 - Opération de Mise à jour 

Une Opération de mise à jour doit avoir pour resultat un statut 200. Pour ce faire, nous créons une classe UpdateActionResultFilterAttribute

public class UpdateActionResultFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext context)
    {
        if (context.Result is ObjectResult)
        {
            var objRes = context.Result as ObjectResult;

            if (objRes.Value is RequestResultWithData)
            {
                RequestResultWithData requestResultWithData = objRes.Value as RequestResultWithData;

                context.HttpContext.Response.Headers["Location"] = requestResultWithData.Location;

                context.HttpContext.Response.StatusCode = requestResultWithData.IsSuccess ? (int)HttpStatusCode.OK : (int)HttpStatusCode.BadGateway;
            }
        }

        base.OnActionExecuted(context);
    }

}

et nous pouvons utiliser comme pour les méthodes d'Ajout le décorateur

[HttpPut("{categoryId}/translation/{translationId}"), UpdateActionResultFilter]
        public async Task<RequestResult> UpdateTranslationAsync([FromRoute]Guid categoryId, [FromRoute]Guid translationId, CategoryTranslation translation)
        {

....

}

2.3 Les opérations de suppression

Les opérations de suppression doivent avoir pour retour 202.

Pour cela nous ajoutons une action DeleteActionResultFilter

public class DeleteActionResultFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext context)
    {
        if (context.Result is ObjectResult)
        {
            var objRes = context.Result as ObjectResult;

            if (objRes.Value is RequestResultWithData)
            {
                RequestResultWithData requestResultWithData = objRes.Value as RequestResultWithData;

                context.HttpContext.Response.Headers["Location"] = requestResultWithData.Location;

                context.HttpContext.Response.StatusCode = requestResultWithData.IsSuccess ? (int)HttpStatusCode.Accepted : (int)HttpStatusCode.BadGateway;
            }
        }

        base.OnActionExecuted(context);
    }

}

et nous pouvons ajouter un décorateur à nos méthodes de suppression

/// <inheritdoc />
[HttpDelete("{id}"), DeleteActionResultFilter]
public async Task<RequestResult> RemoveAsync([FromRoute]Guid id)
{

Décrivons notre API 

Pour permettre une meilleur intégration de notre API dans un système existant ou pour permettre facilement la création de nouveau SDK client, il est nécessaire que notre API soit documenté

Il existe deux standards pour décrire une API

Open API

Contrairement à une API privée, une API décrite au format OpenAPI (maintenant on est à la version 3 https://en.wikipedia.org/wiki/Open_API) est accessible au public pour tous les développeurs. Ils permettent aux développeurs, en dehors d'une organisation, d'accéder aux données backend qui peuvent ensuite être utilisées pour améliorer leurs propres applications. Les API ouvertes peuvent augmenter considérablement les revenus sans que l'entreprise n'ait à investir dans l'embauche de nouveaux développeurs, ce qui en fait une application logicielle très rentable.

Cependant, il est important de se rappeler que l'ouverture de l'information au public peut créer toute une gamme de problèmes de sécurité et de gestion. Par exemple, la publication d'API ouvertes peut rendre plus difficile aux organisations de contrôler l'expérience que les utilisateurs finaux ont avec leurs ressources d'informations. Les éditeurs d'API ouverts ne peuvent pas supposer que les applications client construites sur leurs API offriront une bonne expérience utilisateur. De plus, ils ne peuvent pas garantir que les applications client conservent l'apparence de leur image de marque.

Swagger

Swagger est une spécification indépendante du langage pour décrire les API REST. Le projet Swagger a été donné au projet OpenAPI Initiative et s’appelle maintenant Open API. Les deux noms sont utilisés indifféremment, mais Open API est préféré. Il permet aux ordinateurs et aux utilisateurs de comprendre les fonctionnalités d’un service sans aucun accès direct à l’implémentation (code source, accès réseau, documentation). L’un des objectifs est de limiter la quantité de travail nécessaire pour connecter des services dissociés. Un autre objectif est de réduire le temps nécessaire pour documenter un service avec précision

La spécification de Swagger

Pour que Swagger fonctionne et encore plus l'interface de documentation graphique appelée Swagger UI, un fichier swagger.json doit exister (du moins au minimum virtuellement).

Ce fichier, est en général en ASP.NET générée par un composant tierce qui recupére la documentation du code au format XML et en extrait une documentation Json.

Elle décrit les fonctionnalités de votre API et comment y accéder avec HTTP. Elle gère l’IU Swagger et est utilisée par la chaîne d’outils pour activer la découverte et la génération de code client

en ASP.NET Le composant à utiliser était le composant Swashbuckle

Swashbuckle compte trois composants principaux :

  • Swashbuckle.AspNetCore.Swagger : modèle objet Swagger et intergiciel (middleware) pour exposer des objets SwaggerDocument sous forme de points de terminaison JSON.
  • Swashbuckle.AspNetCore.SwaggerGen : générateur Swagger qui crée des objets SwaggerDocument directement à partir de vos routes, contrôleurs et modèles. Il est généralement associé à l’intergiciel de point de terminaison Swagger pour exposer automatiquement Swagger JSON.
  • Swashbuckle.AspNetCore.SwaggerUI: version incorporée de l’outil IU Swagger. Elle interprète Swagger JSON afin de générer une expérience complète et personnalisable pour décrire la fonctionnalité de l’API web. Il inclut des ateliers de test intégrés pour les méthodes publiques

Pour plus d'information https://docs.microsoft.com/fr-FR/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-2.1&tabs=visual-studio%2Cvisual-studio-xml

en ASP.NET core 2 il est plus recommandé d'utiliser le composant NSwag

L’utilisation de NSwag avec un intergiciel (middleware) ASP.NET Core nécessite le package NuGet NSwag.AspNetCore. Le package se compose d’un générateur Swagger, de l’IU Swagger (v2 et v3) et de l’IU ReDoc

Pour utiliser le package

  1. Référencez le package NSwag.AspNetCore
  2. Dans Startup Appelez le middle Swagger
app.UseSwagger(typeof(Startup).Assembly, settings =>
{
    settings.PostProcess = document =>
    {
        document.Info.Version = "v1";
        document.Info.Title = "Manage product's familly API";
        document.Info.Description = "This API allows to create a familly of product, set the translation and SEO values and manage product's category in the same maner";
        document.Info.TermsOfService = "See Startpoint web site";
        document.Info.Contact = new NSwag.SwaggerContact
        {
            Name = "Michel Bruchet",
            Email = "admin@startpoint-inc.com",
            Url = "https://www.startpoint-inc.com"
        };
        document.Info.License = new NSwag.SwaggerLicense
        {
            Name = "Startpoint inc",
            Url = "https://startpoint.io/license"
        };
    };
});

app.UseMvc();
}

Pour installer le client Swagger-ui

Appelez le middleware SwaggerUi3

app.UseSwaggerUi3(typeof(Startup).GetTypeInfo().Assembly, settings =>
{
    settings.GeneratorSettings.DefaultPropertyNameHandling = NJsonSchema.PropertyNameHandling.CamelCase;
});

Pour tester l'application appelez l'URL http://monserver/swagger

 

Vous pouvez utiliser ReDoc pour générer l'interface de documentation. Celle-ci fournit une meilleur interface graphique

Pour cela il suffit juste de remplacer app.UseSwagger par app.UseSwaggerReDoc 

Créons un client C# pour notre API

Pour permettre aux applications clients d'accéder à notre API, nous avons besoin de générer un client c# 

pour ce faire, NSwag nous fournit une application Windows capable de lire le fichier swagger.json et de générer un client en c#

1. Installer le composant

- Téléchargez depuis le repository Github le MSI https://github.com/RSuter/NSwag/wiki/NSwagStudio

- Installez le MSI

2. Générer le client c#

- Lancez le programme

Sélectionnez l'onglet Swagger specification

Cliquez sur Create local copy

Cochez l'option Csharp Client

Cliquez sur Generate Outputs

Vous pouvez générer les fichiers cs nécessaire ou copier le code

3. Utilisez le code c#

- Créez un projet NetStandard

- Créez un dossier Generated

- Ajoutez une classe ApiClient

- Copiez/collez le code depuis l'interface de génération client

4. Préparez le code c# client pour être testé

2. Ajoutez une classe partial qui ajoute un constructeur et charge la méthode PrepareRequest

public partial class CategoryRootApiClient
{
    private HttpClient _httpClient;

    public CategoryRootApiClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public CategoryRootApiClient(HttpClient httpClient, string baseUrl)
    {
        _httpClient = httpClient;
        _baseUrl = baseUrl;
    }

    partial void PrepareRequest(HttpClient client, HttpRequestMessage request, StringBuilder urlBuilder)
    {
        client = _httpClient ?? client ?? new HttpClient();
    }
}

Créeons un projet de test unitaire

Il est possible d'utiliser différentes technologies pour réaliser les tests unitaires et d'intégration de notre API RestFull

Ici nous allons utiliser XUNIT et Moq pour le mocking des repositories

1. Commençons par créer un projet de type Test

2. Ajoutons les packages Moq et FluentAssertions

- Le package Moq permettra d'éviter d'implémenter le code pour le repository et de pouvoir utiliser le Moqing (qui permet de contrôller le retour d'une méthode)

- Le package FluentAssertions permet d'écrire des assertions plus lisibles

3. Ajouter une interface ICategoryRootApiControllerTest qui posséde les méthode que l'interface ICategoryRootApiController mais Permettant de tester les fonctions souhaitées ce qui nous garantira que nous avons bien testé toutes les méthode de l'interface

4 - Implémentons chaque méthode de test en respectant les 3 A

  • Arrange : Permet de paramétrer les objects et le contexte du test
  • Act : Permet d'implémenter la logique même du test
  • Assert : Permet de contrôller que le test s'est bien passé

N'oublions pas d'ajouter également la méthode qui permettra d'appeler le test

Exemple de code pour un test unitaire

[Fact]
public void CheckAddFact()
{
    var categoryRoot = new CategoryRoot { Id = Guid.NewGuid(), Name = "Test" };
    CheckAdd(categoryRoot).ConfigureAwait(false);
}

public async Task CheckAdd(CategoryRoot categoryRoot)
{
    //Arrange
    _moqRepositoryCategory.Setup(m => m.AddAsync(categoryRoot)).Returns(Task.FromResult((true, categoryRoot)));
    var controller = new CategoryRootApiController(_moqRepositoryCategory.Object, _moqRepositoryCategoryTranslation.Object);

    //Act
    var createResult = await controller.AddAsync(categoryRoot).ConfigureAwait(false);

    //Assert
    createResult.Should().NotBeNull();
    createResult.IsSuccess.Should().BeTrue();
    createResult.Data.Should().NotBeNull();
    createResult.Data.Id.Should().Equals(categoryRoot.Id);
}

Créons maintenant un projet de test d'intégration

Contrairement à un test unitaire, le test d'intégration va consister à charger l'API dans un serveur virtuel (car lorsque vous mettez en place un test d'intégration automatique via un CI comme avec VSTS ou Jenkins il ne s'agit pas de charger IIS ou Kestrel pour accéder à l'API). Le processus pour réaliser un test d'intégration va consister

  1. Créer un projet de test en règle général le nom du projet se termine souvent par TestIntegration mais ce n'est pas une obligation
  2. Créer une classe qui hébergera l'API et utiliser le package Microsoft.AspNetCore.Mvc.Testing
public class CustomWebApplicationFactory<TStartup>:WebApplicationFactory<Startup>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            Mock<IRepository<CategoryRoot>> moqRepositoryCategory;
            Mock<IRepository<CategoryRootTranslation>> moqRepositoryCategoryTranslation;

            moqRepositoryCategory = new Moq.Mock<IRepository<CategoryRoot>>();
            moqRepositoryCategoryTranslation = new Moq.Mock<IRepository<CategoryRootTranslation>>();

            services.AddTransient<IRepository<CategoryRoot>>(provider=>moqRepositoryCategory.Object);
            services.AddTransient<IRepository<CategoryRootTranslation>>(provider=>moqRepositoryCategoryTranslation.Object);

        });

        base.ConfigureWebHost(builder);
    }
}
  1. Créer une classe de test similaire à la classe unitaire et appeler la classe Factory pour générer un client HTTP
public class CategoryRootApiControllerTest:IClassFixture<CustomWebApplicationFactory<Startup>>
{
    private readonly CustomWebApplicationFactory<Startup> _factory;
    private readonly HttpClient _client;

    public CategoryRootApiControllerTest(CustomWebApplicationFactory<Startup> factory)
    {
        _factory = factory;

        _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
            HandleCookies = true
        });

    }

    [Fact]
    public async Task Get_ListAllCategories()
    {
        var content = await _client.GetAsync("/api/root").ConfigureAwait(false);

        content.Should().NotBeNull();
        content.EnsureSuccessStatusCode();
    }
}

Conclusion

Dans cet article, nous avons abordé comment créer une API Web en ASP.NET core 2. Documenté via Swagger et Nswag. réaliser automatiquement le client SDK en C# (Swagger permet de réaliser des clients dans beaucoup d'autre languages). Et en fin nous avons mis en place les tests unitaires et d'intégration pour permettre de tester le bon fonctionnement de l'API.

Quoi faire ?

Nous avons vu dans notre article que le document de spécification Swagger a été généré automatiquement après le développement hors si plusieurs développeurs ont besoin de travailler ensemble sur l'API cela devient très difficile car le développeur qui a besoin de consommer son api a besoin d'attendre la réalisation de l'API avant de pouvoir l'utiliser. Pour simplifier la bonne collaboration il est tout à fait possible de réaliser la spécification de l'API avant de l'écrire, pour cela nous utiliserons une méthode appelée Spec first. MAis cela est un autre sujet que nous verrons dans un article prochain

 

 

Michel Bruchet