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
Verbe | Description |
GET | Permet d'obtenir une liste de ressources |
GET/{id} | Permet d'obtenir une ressource |
POST | Permet de créer une ressource |
PUT | Permet de remplacer une ressource |
PATCH | Permet de remplacer une partie de la ressource |
DELETE | Permet 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
Verbe | Description |
HTTPGet | Récupérer une ressource ou une liste |
HTTPPost | Créer une ressource |
Nous avons décorer décoré notre classe API avec les décorateurs suivants
Décorateur | Description |
[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
Code | Objet | Contenu |
200 | OkObjectResult | Data |
400 | BadRequestObjectResult | Message 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
Verbe | Utilisation |
GET | Récupérer la liste des ressources |
GET("{id}") | Récupérer une seule ressource |
POST | Créer une ressource |
PUT | Modifier une ressource en entier |
PATCH | Modifier une partie de la ressource |
DELETE | Supprimer 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
- Référencez le package NSwag.AspNetCore
- 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
- 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
- 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);
}
}
- 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