Conception de microservices : approche architecturale et pratique

Les microservices offrent flexibilité, évolutivité et indépendance lors de la gestion de systèmes nécessitant des ressources décentralisées. Mais comment ces qualités se traduisent-elles dans la pratique ? Explorons les microservices d’un point de vue architectural et créons un microservice de commentaires à partir de zéro dans ASP.NET Core.
Dans un monde axé sur la technologie et fortement orienté vers le consommateur, utiliser des approches classiques comme les monolithes n’est pas toujours le meilleur choix. Dans ces cas, les microservices se présentent comme une option flexible et viable pour créer et déployer des services indépendants.
Dans cet article, nous explorerons la façon dont les microservices s’articulent architecturalement, leurs avantages et inconvénients, et comment créer un microservice backend dans ASP.NET Core pour gérer les commentaires des clients.
🏛️ Microservices en architecture
Si vous avez travaillé ou même étudié la programmation ces dernières années, vous avez probablement vu le mot « microservice » quelque part. En effet, les microservices n’ont jamais été aussi populaires. Les grandes entreprises technologiques ont fait de ce terme presque synonyme d’évolutivité, de résilience et d’innovation.
Cependant, il est important de comprendre que les microservices ne constituent pas une formule toute faite pour un projet. Il s’agit avant tout d’un choix architectural et, comme toute décision architecturale, il doit être basé sur les besoins du système et non pas simplement sur la volonté d’adopter l’approche la plus populaire.
En pratique, les microservices se présentent comme une alternative au modèle monolithique. Dans un monolithe, toute la logique métier coexiste au sein d’une seule application. Cependant, dans les microservices, l’application est divisée en services plus petits et indépendants, chacun responsable d’un contexte spécifique.
Le choix des microservices a un impact direct sur l’architecture et s’est avéré être un excellent choix dans de nombreux scénarios. Considérons un scénario dans lequel un microservice serait une approche recommandée.
Imaginez un scénario dans lequel nous disposons de trois services principaux : commande, paiement et commentaires. Dans une entreprise de commerce électronique, par exemple, la commande et le paiement sont fondamentaux pour le fonctionnement de l’entreprise, tandis que les commentaires, bien qu’importants, jouent un rôle plus secondaire. Considérons maintenant la situation suivante : un bug se produit dans le service Feedback. Quel impact cela aurait-il sur l’application si elle était structurée comme un monolithe plutôt que comme un ensemble de microservices ?
Dans le modèle monolithique, tous les services sont regroupés dans un seul système, partageant la même base de code et s’exécutant dans le même processus. Cela signifie que, même si le problème se limite à la seule fonctionnalité Feedback, il existe un risque de compromettre la stabilité de l’ensemble du système.
De plus, réparer la Fonction nécessite un redéploiement complet, y compris la Commande et le Paiement, même si ces services n’ont pas été affectés. En d’autres termes, un petit détail dans une zone secondaire peut devenir un goulot d’étranglement pour l’ensemble de l’entreprise, comme le montre l’image ci-dessous :

Dans le modèle des microservices, chaque partie du système est isolée dans son propre processus, avec un déploiement, une évolutivité et une base de données indépendants. Dans ce scénario, un bug dans Feedback n’affecte que ce service spécifique, et comme il n’est pas essentiel au processus d’achat, il peut être hors ligne pendant un certain temps sans complications majeures.
Pendant ce temps, les services Commande et Paiement continuent de fonctionner normalement, sans compromettre le flux d’achats ni le traitement des paiements, qui sont le cœur de l’activité. Les correctifs sont également plus rapides, car seul le service problématique doit être mis à jour.

Il est important de souligner que, dans ce scénario hypothétique, nous envisageons le pire des cas sans entrer dans trop de détails. En ce sens, alors que dans un monolithe, un bug apparemment inoffensif peut provoquer une instabilité dans l’ensemble du système, l’impact sur les microservices est beaucoup plus localisé et contrôlé. C’est pourquoi, dans les scénarios où certains services sont critiques pour l’entreprise, comme la Commande et le Paiement, l’architecture des microservices offre des avantages significatifs en termes de résilience et de continuité opérationnelle.
📉 Inconvénients des microservices
S’ils offrent des avantages tels que l’évolutivité, la résilience et la séparation des responsabilités, les microservices présentent également des inconvénients dont il faut tenir compte.
L’un des principaux est la complexité architecturale. Diviser un système en dizaines ou centaines de services indépendants nécessite beaucoup plus d’orchestration, de surveillance, de déploiement et de communication entre les composants. En outre, le trafic réseau augmente considérablement, car les appels qui étaient auparavant internes à un processus monolithique sont désormais répartis entre différents services, ce qui peut devenir un risque à long terme.
Un autre défi rencontré lors du choix d’utiliser des microservices est lié à la gestion des données et à la cohérence des transactions. Chaque microservice a tendance à disposer de sa propre base de données, ce qui rend difficile le maintien d’une cohérence entre eux et nécessite souvent l’adoption de stratégies plus sophistiquées, telles que les sagas ou le sourcing d’événements. Cette complexité augmente la courbe d’apprentissage de l’équipe, exige une plus grande maturité dans les pratiques DevOps et peut augmenter les coûts d’infrastructure et d’exploitation.
Par conséquent, pour les organisations qui ne disposent pas de bases solides dans ces domaines, l’adoption de microservices peut aboutir à un système plus fragile et plus coûteux qu’un monolithe bien structuré.
🛠️ Construire un microservice
Maintenant que nous comprenons les aspects de l’architecture logicielle des microservices, créons un microservice avec ASP.NET Core qui utilise SQL Server comme base de données et l’exécute dans un conteneur Docker.
Pensons d’abord à la conception de domaine. Nous devons définir clairement les responsabilités du microservice. Il doit résoudre un problème spécifique dans le contexte plus large du système, sans accumuler des fonctions qui n’y appartiennent pas. Cette frontière claire est ce qui empêche un microservice de devenir un « mini-monolithe ».
En ce sens, le microservice que nous allons créer sera chargé de gérer les retours clients. Il doit recevoir des données client de base telles que le nom du client, le produit ou le service auquel il fait référence, sa note (sur une échelle de 1 à 5, où 1 signifie très mauvais et 5 très bon) et une description facultative. Il convient ensuite d’insérer ces données dans une base de données. En outre, il devrait rendre les données saisies disponibles pour une évaluation future.
Du point de vue de la conception, ce microservice fait référence à un simple CRUD basé sur les données, où la modélisation et la mise en œuvre sont guidées par les données et la base de données. CRUD (Create, Read, Update, Delete) fait référence au fait que le microservice expose des points de terminaison simples qui permettent de créer, d’interroger, de mettre à jour et de supprimer des enregistrements, comme le montre l’image ci-dessous.

Création du projet et téléchargement des dépendances
Vous pouvez accéder au code complet de l’application dans ce référentiel GitHub : Code source des informations sur les clients.
Pour créer le projet et la solution, vous pouvez utiliser les commandes .NET ci-dessous :
dotnet new sln -n CustomerInsights
dotnet new webapi -n CustomerInsights
dotnet sln CustomerInsights.sln add CustomerInsights/CustomerInsights.csproj
Exécutez ensuite les commandes suivantes pour ajouter les packages NuGet au projet :
cd CustomerInsights
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.EntityFrameworkCore.Design
Création de la classe d’entité
Définissons maintenant l’entité qui représentera le microservice chargé de recevoir, stocker et fournir les commentaires des clients. Dans ce cas, nous aurons une classe appelée Feedback.
Ainsi, au sein du projet, créez un nouveau dossier appelé « Entités » et ajoutez-y la classe suivante :
namespace CustomerInsights.Entities;
public class Feedback
{
public int Id { get; set; }
public string CustomerName { get; private set; }
public string Product { get; private set; }
public int Rating { get; private set; }
public string? Description { get; private set; }
public Feedback() { }
public Feedback(string customerName, string product, int rating, string? description = null)
{
if (string.IsNullOrWhiteSpace(customerName))
throw new ArgumentException("Customer name is required.", nameof(customerName));
if (string.IsNullOrWhiteSpace(product))
throw new ArgumentException("Product or service is required.", nameof(product));
if (rating < 1 || rating > 5)
throw new ArgumentOutOfRangeException(nameof(rating), "Rating must be between 1 and 5.");
CustomerName = customerName;
Product = product;
Rating = rating;
Description = description;
}
}
Nous avons ici une entité simple qui représente les commentaires des clients. Malgré sa simplicité, il suit les meilleures pratiques en gardant ses propriétés privées. De plus, la création d’un nouvel enregistrement subit des validations au sein de la classe elle-même, afin que le domaine soit expressif et révèle son intention à travers le comportement qu’il contient.
Création de la couche de données
La couche de données contiendra la classe qui communiquera avec la base de données. Puisque nous utilisons Entity Framework Core comme ORM (Object Relational Mapping), nous devons définir le DbContext classe.
Alors, créez un nouveau dossier appelé « Data » et ajoutez-y la classe suivante :
using CustomerInsights.Entities;
using Microsoft.EntityFrameworkCore;
namespace CustomerInsights.Data;
public class AppDbContext : DbContext
{
public DbSet<Feedback> Feedbacks { get; set; } = null!;
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Feedback>(entity =>
{
entity.ToTable("Feedbacks");
entity.HasKey(f => f.Id);
entity.Property(f => f.CustomerName)
.IsRequired()
.HasMaxLength(100);
entity.Property(f => f.Product)
.IsRequired()
.HasMaxLength(100);
entity.Property(f => f.Rating)
.IsRequired();
entity.Property(f => f.Description)
.HasMaxLength(500);
});
}
}
Le AppDbContext classe mappera les entités de la base de données, dans ce cas, Feedback, en plus de créer certaines propriétés, telles que CustomerNamesont obligatoires lors de la génération de migrations de bases de données.
Création des DTO
Pour insérer et récupérer des données, nous allons créer un DTO (Data Transfer Object) qui sera reçu dans la requête. Alors, créez un nouveau dossier appelé « Dtos » et à l’intérieur, créez l’enregistrement et la classe suivants :
namespace CustomerInsights.Dtos;
public record CreateCustomerFeedbackDto(string CustomerName, string Product, int Rating, string? Description);
namespace CustomerInsights.Dtos;
public class FeedbackResponseDto
{
public int Id { get; set; }
public string CustomerName { get; set; } = string.Empty;
public string Product { get; set; } = string.Empty;
public int Rating { get; set; }
public string? Description { get; set; }
}
Création des mappages
Le mappage des classes est nécessaire pour éviter d’exposer les classes d’entités en dehors de l’API, ni d’introduire des données de l’extérieur sans traitement approprié. Alors, créez un nouveau dossier appelé « Mappings » et, à l’intérieur, ajoutez la classe suivante :
using CustomerInsights.Dtos;
using CustomerInsights.Entities;
namespace CustomerInsights.Mappings;
public static class FeedbackMappings
{
public static FeedbackResponseDto ToDto(this Feedback feedback)
{
return new FeedbackResponseDto
{
Id = feedback.Id,
CustomerName = feedback.CustomerName,
Product = feedback.Product,
Rating = feedback.Rating,
Description = feedback.Description
};
}
public static IEnumerable<FeedbackResponseDto> ToDtoList(this IEnumerable<Feedback> feedbacks) =>
feedbacks.Select(f => f.ToDto());
public static Feedback ToEntity(this CreateCustomerFeedbackDto dto)
{
return new Feedback(
dto.CustomerName,
dto.Product,
dto.Rating,
dto.Description
);
}
}
Création de la classe de contrôleur
L’étape suivante consiste à créer une classe de contrôleur pour recevoir les appels API et effectuer des actions sur la base de données. Créez un nouveau dossier appelé « Contrôleurs » et ajoutez-y la classe de contrôleur suivante :
using CustomerInsights.Data;
using CustomerInsights.Dtos;
using CustomerInsights.Entities;
using CustomerInsights.Mappings;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace CustomerInsights.Controllers;
[ApiController]
[Route("api/v1/[controller]")]
public class FeedbackController : ControllerBase
{
private readonly AppDbContext _dbContext;
public FeedbackController(AppDbContext dbContext)
{
_dbContext = dbContext;
}
[HttpPost]
[ProducesResponseType(typeof(Feedback), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Create([FromBody] CreateCustomerFeedbackDto feedbackDto)
{
try
{
var feedback = feedbackDto.ToEntity();
_dbContext.Feedbacks.Add(feedback);
await _dbContext.SaveChangesAsync();
return CreatedAtAction(nameof(GetById), new { id = feedback.Id }, feedback);
}
catch (ArgumentOutOfRangeException ex)
{
return BadRequest(new { error = ex.Message });
}
catch (ArgumentException ex)
{
return BadRequest(new { error = ex.Message });
}
}
[HttpGet]
public async Task<IActionResult> GetAll([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10)
{
var feedbacks = await _dbContext.Feedbacks
.AsNoTracking()
.OrderBy(f => f.Id)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return Ok(feedbacks.ToDtoList());
}
[HttpGet("product/{product}")]
public async Task<IActionResult> GetByProduct(string product, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10)
{
var feedbacks = await _dbContext.Feedbacks
.AsNoTracking()
.Where(f => f.Product == product)
.OrderBy(f => f.Id)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return Ok(feedbacks.ToDtoList());
}
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var feedback = await _dbContext.Feedbacks
.AsNoTracking()
.FirstOrDefaultAsync(f => f.Id == id);
if (feedback is null)
return NotFound();
return Ok(feedback.ToDto());
}
}
Configuration de la classe de programme
Maintenant, configurons la classe Program, en ajoutant les injections de dépendances et l’initialisation de la base de données (DatabaseInit.MigrationInitialization). Ajoutez le code suivant à la classe Program :
using CustomerInsights.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddOpenApi();
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
DatabaseInit.MigrationInitialization(app);
app.Run();
Puis, dans le appsettings.json fichier, ajoutez la chaîne de connexion SQL Server :
"ConnectionStrings": {
"DefaultConnection": "Server=mssql-server,1433;Initial Catalog=FeedbackDb;User ID=SA;Password=8/geTo'7l0f4;TrustServerCertificate=true"
}
Application des migrations
Vous pouvez utiliser les commandes ci-dessous pour appliquer les migrations de bases de données EF Core.
dotnet ef migrations add InitialCreate
dotnet tool install --global dotnet-ef
Exécuter le microservice et la base de données dans un conteneur Docker
Notre microservice est prêt à fonctionner. Pour cela, nous utiliserons Docker. Si vous n’êtes pas familier avec Docker, je suggère ces deux articles qui montrent comment déployer une application ASP.NET Core à partir de zéro :
- Déploiement d’applications ASP.NET Core avec Docker – Partie 1.
- Déploiement d’applications ASP.NET Core avec Docker – Partie 2.
À des fins pédagogiques, nous exécuterons SQL Server dans un conteneur Docker, mais dans les environnements de production, il est recommandé d’utiliser les ressources cloud pour maintenir la base de données.
Ajoutez donc le fichier Docker Compose et le Dockerfile suivants à la racine de l’application :
version: "3.9"
services:
customer-insights:
build:
context: .
dockerfile: Dockerfile
container_name: customer-insights
ports:
- "5050:8080"
depends_on:
- mssql-server
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__DefaultConnection=Server=mssql-server,1433;Initial Catalog=FeedbackDb;User ID=SA;Password=8/geTo'7l0f4;TrustServerCertificate=true
mssql-server:
image: mcr.microsoft.com/mssql/server:2022-latest
container_name: mssql-server
ports:
- "1433:1433"
environment:
- ACCEPT_EULA=Y
- MSSQL_SA_PASSWORD=8/geTo'7l0f4
volumes:
- mssqldata:/var/opt/mssql
volumes:
mssqldata:
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY *.csproj ./
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
WORKDIR /app
COPY --from=build /app ./
EXPOSE 8080
ENTRYPOINT ["dotnet", "CustomerInsights.dll"]
Exécutez ensuite les commandes Docker dans un terminal :
docker-compose up -d
Après l’exécution, vous devriez disposer de deux conteneurs Docker, un pour le microservice et un pour SQL Server :

Maintenant, si vous effectuez une requête POST à l’API en utilisant la route : http://localhost:5050/api/v1/feedback/1 et transmettez le JSON suivant dans le corps :
{
"customerName": "John Smith",
"product": "helloPhone 10 Pro",
"rating": 5,
"description": "Excellent phone! Super fast, great camera, and battery lasts all day. Worth the upgrade."
}
Un nouvel enregistrement sera inséré dans la base de données.

Et si vous faites une requête GET, l’enregistrement sera renvoyé :

Nous disposons donc d’un microservice complet basé sur les données et exécuté dans un conteneur Docker.
🌱 Conclusion et prochaines étapes
Les microservices sont apparus comme une alternative aux systèmes monolithiques, réduisant le couplage étroit entre les fonctionnalités. Leurs principaux avantages sont :
- Déploiement indépendant, qui permet de mettre à jour chaque service sans impacter le reste du système
- Évolutivité sélective, où seuls les services les plus gourmands en ressources peuvent être étendus
- Résilience, puisque les pannes d’un service ne compromettent pas l’ensemble de l’application
Dans cet article, nous avons créé un microservice CRUD basé sur les données pour enregistrer et interroger les commentaires des clients et l’avons déployé dans un conteneur Docker.
J’espère que les exemples présentés vous aideront à comprendre comment les microservices sont représentés dans une architecture moderne. Plusieurs fonctionnalités importantes peuvent être explorées, telles que l’authentification, la surveillance, les tests automatisés, la gestion des versions et l’observabilité, qui rendent l’écosystème des microservices encore plus robuste.
Source link
