Cédric BRASSEUR
Mis à jour le 19/03/2025
Microservices
Comprendre les différences entre microservices et monolithe
Savoir quand choisir l'architecture microservices
Réaliser des microservices simples et les faire interagir avec RabbitMQ
Mise en place pratique d'un exemple simple sous forme de tutoriel à suivre pas à pas
Le terme "Microservices" fait référence à une architecture logicielle qui est souvent mise en opposition au monolithique.
Les principales informations à comprendre :
Application à grande échelle
Les raisons principales du choix de l'architecture microservices sont les suivantes :
Equipe de développement hétérogène
Déploiement fréquents
Scalabilité et résilience
Projet évolutif à maintenir sur le long terme
Mais pourquoi ?
Nous allons aborder quelques concepts plus techniques liés aux microservices et nous allons les détailler plus tard, au fur et à mesure dans la suite de la formation.
Voici quelques définitions techniques que nous allons éclaircir plus tard. Ces notions sont primordiales pour la mise en place d'une architecture microservices
Petit schéma pour avoir un visuel de tout ça ensemble...
Comme il y a de nombreuses étapes de mise en place pour réaliser une architecture microservices, nous allons procéder par étape et via des workshop très guidé que je vous ai mis en place :
L'API (Application Programming Interface) est un élément de votre projet extrêmement utilisé dans de nombreux contextes.
Elle permet (en général) de faire le lien entre deux éléments, par exemple :
Un exemple simple d'API, qui retourne juste une blague Chuck Norris :
https://api.chucknorris.io/jokes/random
Vous l'avez remarqué si vous avez cliqué sur le lien de la slide précédente, le format de retour d'une API est très généralement du JSON.
{
"categories": [],
"created_at": "2020-01-05 13:42:24.696555",
"icon_url": "https://api.chucknorris.io/img/avatar/chuck-norris.png",
"id": "kR5RQ_S7QwWbcmF18s9upA",
"updated_at": "2020-01-05 13:42:24.696555",
"url": "https://api.chucknorris.io/jokes/kR5RQ_S7QwWbcmF18s9upA",
"value": "Chuck Norris invented airplanes because he was tired of being
the only person that could fly."
}
En plus de ce json, l'appel à une API (donc à une URL, en http / https) retourne un code retour pour connaître le type de reponse de l'API. On le verra un poil plus tard ça.
HEAD
PATCH
DELETE
POST
OPTIONS
Modification d'un contenu (complet)
Comme GET mais pour le header (méta)
Mise à jour de contenu (partiel)
Ajout de contenu
Suppression de contenu
Retourne les ressources disponibles
Il existe plusieurs types de requêtes pour une API, chacune ayant un objectif précis. Dont 5 sont les plus fréquentes (en gras : GET, PUT, UPDATE, POST, DELETE)
PUT
GET
Récupération d'un contenu
Lors de la requête, on va donc avoir également un type de requête associé à la demande
Les statut code de retour (Code status), les plus fréquents, car il en existe beaucoup d'autres....
Code | Nom | Description | Cas d'utilisation courants |
---|---|---|---|
200 | OK | Requête réussie | Récupération d'une ressource (GET) |
201 | Created | Ressource créée avec succès | Création d'une ressource (POST) |
204 | No Content | Requête réussie, mais aucune réponse | Suppression d'une ressource (DELETE) |
400 | Bad Request | Requête mal formulée ou invalide | Données manquantes ou incorrectes dans une requête |
401 | Unauthorized | Authentification requise | Jeton d'authentification manquant ou invalide |
404 | Not Found | Ressource non trouvée | Identifiant de ressource inexistant |
500 | Internal Server Error | Erreur interne du serveur | Exception ou bug inattendu côté serveur |
Merci ChatGPT pour ce joli tableau !
Schéma (simplifié) du fonctionnement d'une API :
Un DTO (Data Transfer Object) est un objet de transfert de données, son objectif est de définir la structure des éléments entrants et sortants d'une API
On peut utiliser une classe classique pour nos DTOs, mais en .Net, les records offrent certains avantages et une simplicité d'écriture.
Les avantages
public record ItemDto(Guid Id, string Name, string Description, decimal Price);
public record CreateItemDto([Required] string Name, [Required] string Description, [Range(0,100)] decimal Price);
public record UpdateItemDto([Required] string Name, [Required] string Description, [Range(0, 100)] decimal Price);
Exemple de record
Un contrôleur est une classe qui va définir les différentes routes de notre API, c'est à dire les requêtes qui peuvent être réalisés par un client.
[ApiController]
[Route("items")]
public class ItemsController : ControllerBase
{
[HttpGet]
public IEnumerable<ItemDto> Get()
{
return items;
}
[HttpGet("{id}")]
public ActionResult<ItemDto> GetById(Guid id)
{
var item = items.Where(item => item.Id == id).FirstOrDefault();
if (item == null)
{
return NotFound();
}
return item;
}
[HttpPost]
public ActionResult<ItemDto> Create(CreateItemDto createItemDto)
{
var item = new ItemDto(Guid.NewGuid(), createItemDto.Name, createItemDto.Description, createItemDto.Price);
items.Add(item);
return CreatedAtAction(nameof(GetById), new { id = item.Id }, item);
}
//...
}
Exemple de controller
On remarquera rapidement qu'il sera nécessaire de convertir un élément (Entity, donc côté base de données) en DTO, pour simplifier celà, on utilise les extensions en .Net.
Ici, ça nous permettra de transformer une entité en DTO
public static class ItemsExtension
{
public static ItemDto AsDto(this Item item)
{
return new ItemDto(item.Id, item.Name, item.Description, item.Price);
}
}
// La méthode AsDto est maintenant appelable sur n'importe quel objet de type Item
Exemple d'extension
Explications sur Swagger, son utilité et son utilisation (live)
Explications sur Postman, son utilité et son utilisation (live)
Utilisation de Postman
Postman est un outil de requêtage REST très pratique.
Conseillé sous Windows
& Mac
Il permet de faire tout type de requêtes, je vous conseille de paramétrer pour chaque requête l'authentification basique avec votre compte CouchDB.
Je vais vous faire suivre un workshop très guidé afin de mettre en place une première API en .Net en utilisant les notions que l'on a vu au préalable.
Step 1
Envoyer Workshop Step 1
Le repository pattern est une façon bien complexe de définir un principe très simple : On va simplement mettre nos classes et éléments d'accès aux données dans un dossier et le séparer des autres éléments. Autrement dit, on va éviter de mélanger nos éléments d'accès aux données à nos controller.
L'avantage est que ça simplifie grandement le changement de base de données si un jour on souhaite en changer.
En général, on couple cette notion à l'injection de dépendances, que l'on verra un peu après dans cette partie.
Schéma dans l'API
MongoDB est une base de données NoSQL orientée Documents.
Chaque document est en json.
On utilise souvent MongoDB lorsque l'on réalise des microservices car c'est scalable et fait pour simplifier la scalabilité (horizontale)
Nous allons démarrer un conteneur MongoDB avec Docker
Les commandes à réaliser sont :
- Récupérer l'image mongo
docker pull mongo
- Start mongo container (seule commande obligatoire pour notre cas) :
docker run -d --name mongodb -p 27017:27017 -v localmongovolume:/data/db mongo
- Arriver dans le terminal de commande du conteneur :
docker exec -it mongodb bash
- Démarrer mongo en invite de commande (dans docker) :
mongosh
**Vous êtes connecté sur mongo sur docker !**
Via Docker
Juste pour vous montrer un peu comment ça marche avant l'exercice, je vais vous montrer comment on peut utiliser MongoDB à travers les différentes méthodes proposées.
De même, nous allons installer une extension dans VSCode pour pouvoir simplement parcourir nos base de données MongoDB
La chaîne de connexion est :
Documentation !!!
Documentation des commandes
Documentation !!!
Documentation !!!
mongodb://{HOST}:{PORT}
mongodb://localhost:27017 (de manière générale en local)
Pour utiliser MongoDB avec .Net, il nous faut ajouter le package Nuget MongoDB.Driver
Ensuite, nous pouvons facilement exploiter les classes MongoDB pour les classes MongoDB pour réaliser notre CRUD avec notre base de données en MongoDB (orienté document)
Je vais vous montrer un exemple de code et vous aurez le workshop guidé en fin de tutoriel pour pratiquer et comprendre comment l'utiliser
Un principe de la POO est à connaître pour réaliser proprement nos liens entre nos controllers et nos repositories
L'injection de dépendances
Vous l'avez sûrement compris dans la slide précédente, mais l'injection de dépendance permet d'éviter de se lier à une implémentation concrète d'une classe en plus de permettre de délier les éléments et éviter les dépendances directes. Ceci rendant le code plus modulaire et évolutif.
En .Net, on peut même paramétrer directement l'injection de dépendance dans nos Settings de départ (fichier Program.cs) afin de pouvoir définir comment l'injection de dépendance doit être gérée.
Nous allons voir un exemple ensemble et vous allez également pouvoir pratiquer avec le workshop guidé
Je vais vous faire suivre un workshop très guidé afin de mettre en place le repository pattern avec MongoDB et appliquer l'injection de dépendances
Step 2
Envoyer Workshop Step 2
Le but de cette étape est de rendre notre code plus générique afin d'éviter de dupliquer du code qui peut être factorisé.
Le principe ici va être de retravailler sur :
Ensuite, on va créer une librairie de classe pour pouvoir utiliser cette librairie directement dans nos services
Voici un petit schéma pour comprendre pourquoi on ne fait pas un service générique, mais un package que l'on va importer avec Nuget.
Ici, c'est simple, on va simplement extraire une interface de notre entité, l'appeler IEntity et forcer l'implémentation d'un Guid Id. C'est tout ce dont on a obligatoirement besoin.
public interface IEntity
{
Guid Id { get; set; }
}
// Notre entité concrète ne change pas, elle implémente juste cette interface
public class Item : IEntity
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
}
Ici, on va utiliser la généricité pour notre Repository, l'objectif est de lui permettre de gérer toute Entity héritant de IEntity.
On commence par créer une interface.
public interface IRepository<T> where T : IEntity
{
Task CreateAsync(T entity);
Task DeleteOneById(Guid id);
Task<IReadOnlyCollection<T>> GetAllAsync();
Task<T> GetByIdAsync(Guid id);
Task UpdateAsync(T entity);
}
Ensuite, on va appliquer la généricité à notre Repository actuel (slide suivante)
Le but étant de rendre les méthodes classiques d'accès aux données génériques en fonction du type générique.
public class MongoRepository<T> : IRepository<T> where T : IEntity
{
private readonly IMongoCollection<T> _dbCollection;
private readonly FilterDefinitionBuilder<T> _filterBuilder = Builders<T>.Filter;
public MongoRepository(IMongoDatabase mongoDatabase, string collectionName)
{
_dbCollection = mongoDatabase.GetCollection<T>(collectionName);
}
public async Task<IReadOnlyCollection<T>> GetAllAsync()
{
return await _dbCollection.Find(_filterBuilder.Empty).ToListAsync();
}
public async Task<T> GetByIdAsync(Guid id)
{
FilterDefinition<T> filter = _filterBuilder.Eq(entity => entity.Id, id);
return await _dbCollection.Find(filter).FirstOrDefaultAsync();
}
public async Task CreateAsync(T entity)
{
ArgumentNullException.ThrowIfNull(entity);
await _dbCollection.InsertOneAsync(entity);
}
public async Task UpdateAsync(T entity)
{
ArgumentNullException.ThrowIfNull(entity);
FilterDefinition<T> filter = _filterBuilder.Eq(existingEntity => existingEntity.Id, entity.Id);
await _dbCollection.ReplaceOneAsync(filter, entity);
}
public async Task DeleteOneById(Guid id)
{
FilterDefinition<T> filter = _filterBuilder.Eq(entity => entity.Id, id);
await _dbCollection.DeleteOneAsync(filter);
}
}
Attention, maintenant on va passer le collectionName via le constructeur et mettre en place l'injection de dépendances pour la base de données
Il ne reste plus qu'à modifier notre utilisation du repository dans notre controller
private readonly IRepository<Item> _itemsRepository;
public ItemsController(IRepository<Item> itemsRepository)
{
_itemsRepository = itemsRepository;
}
On va appliquer le même principe pour ce qui est de notre configuration via notre Program.cs en utilisant les extensions
public static class ServiceExtension
{
public static IServiceCollection AddMongo(this IServiceCollection services)
{
BsonSerializer.RegisterSerializer(new GuidSerializer(BsonType.String));
BsonSerializer.RegisterSerializer(new DecimalSerializer(BsonType.String));
services.AddSingleton(serviceProvider =>
{
var configuration = serviceProvider.GetService<IConfiguration>();
var serviceSettings = configuration.GetSection(nameof(ServiceSettings)).Get<ServiceSettings>();
var mongoDbSettings = configuration.GetSection(nameof(MongoDBSettings)).Get<MongoDBSettings>();
var mongoClient = new MongoClient(mongoDbSettings.ConnectionString);
return mongoClient.GetDatabase(serviceSettings.Name);
});
return services;
}
public static IServiceCollection AddMongoRespository<T>(this IServiceCollection services, string collectionName)
where T : IEntity
{
services.AddSingleton<IRepository<T>>(serviceProvider =>
{
var database = serviceProvider.GetService<IMongoDatabase>();
return new MongoRepository<T>(database, collectionName);
});
return services;
}
}
Utiliser l'extension dans notre Program.cs
using Play.Catalog.Service.Entities;
using Play.Catalog.Service.Extensions;
using Play.Catalog.Service.Settings;
var builder = WebApplication.CreateBuilder(args);
var serviceSettings = builder.Configuration.GetSection(nameof(ServiceSettings)).Get<ServiceSettings>();
// On utilise les extensions que l'on a créer pour simplifier notre appel
builder.Services
.AddMongo()
.AddMongoRespository<Item>("items");
// Fin de la modification, rien d'autre n'a changé !
builder.Services.AddControllers(options =>
{
options.SuppressAsyncSuffixInActionNames = false;
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapControllers();
app.Run();
Pour utiliser cette partie générique dans nos services, on va créer une librairie de classe afin de pouvoir la versionner et l'utiliser dans nos services.
dotnet new classlib -n Play.Common
On déplace les éléments génériques (IEntity, IRepository, ServiceExtensions, MongoDBSettings).
Puis on créer un package (classlib) que l'on pourra importer avec Nuget dans nos services
dotnet pack -o ../../../packages/
Il ne reste plus qu'à importer le package Nuget, supprimer les doublons restant dans notre service et utiliser les éléments du package Common !
Comme on l'a dit, souvent, nos services vont avoir chacun leur propre base de données, on va donc faire en sorte d'utiliser docker-compose afin de pouvoir démarrer nos différentes bases.
services:
mongoDbServiceCatalog:
image: mongo
container_name: mongoDbServiceCatalog
ports:
- "27017:27017"
volumes:
- mongoDbDataCatalog:/data/db
mongoDbServiceInventory:
image: mongo
container_name: mongoDbServiceInventory
ports:
- "27018:27017"
volumes:
- mongoDbDataInventory:/data/db
volumes:
mongoDbDataCatalog:
mongoDbDataInventory:
Avec le package Common que l'on a mis en place, créer un second service est maintenant un jeu d'enfant !
Vous allez pouvoir pratiquer avec la partie suivante, j'ai essayé de la rendre guidée tout de même.
Je vais vous faire suivre un workshop très guidé afin de mettre en place la généricité avant de créer votre second service
Step 2.5
Envoyer Workshop Step 2.5
Je vais vous faire suivre un workshop très guidé afin de créer un second service avec le package Common que l'on a créé
Step 3
Envoyer Workshop Step 3
Il existe deux moyens principaux pour faire communiquer nos services, chacun avec leurs avantages et inconvénients, mais on va en appliquer qu'un seul car c'est le plus fréquent.
Petit schéma synchrone...
Les services communiquent entre eux et ça nécessite de mettre en place différentes stratégies en cas d'échecs & ça augmente la latence de réponse des services
Différents patterns / techniques sont à connaître et implémenter dans le cas de communication synchrone entre les services
On ne va pas faire ça !
Ce n'est pas forcément une mauvaise idée, mais ça n'est pas la façon la plus courante de faire fonctionner nos services ensemble...
On repart sur le schéma de départ pour la communication asynchrone
Les services communiquent entre eux via le message broker qui est là pour faire tampon et s'assurer que les services consomment les messages dès que possible
Je vais vous faire suivre un workshop très guidé afin d'appliquer la communication asynchrone entre nos services. Il y a beaucoup de choses à comprendre et apprendre dans cette partie très technique, mais nous en aurons vu une majeure partie ensemble à l'oral en présentation.
Step 4
Envoyer Workshop Step 4
Vous devez déjà connaître, mais les CORS sont un souci récurrent pour les développeurs, quelques notions :
Petit schéma...
Petit schéma...
Pour l'instant on a mis de côté la gateway, mais elle apporte de nombreux avantages non négligeables :
Petit schéma sans gateway et je vous explique les inconvénients, surtout au niveau des CORS
Petit schéma sans gateway et je vous explique les avantages
Ajoutons un front que je vous envoie et paramétrons les CORS (pour l'instant sans gateway)
Step 5
Envoyer Workshop Step 5
Créez un service complet de recommandation d'items à un utilisateur.
Un peu comme ce qui est fait par notre service inventaire, je vous demande de créer un service de recommandation.
Les recommandations sont faites pour un utilisateur (guid généré par postman). Une recommandation a un id, l'id des items recommandés + le nom et le prix de l'item provenant du service Catalog
Créer un troisième service avec communication asynchrone