Onion Architecture
Introduction
Dans une application, les règles métier décrivent les traitements exécutés par les services qu'elle contient. Elles changent généralement moins souvent que les solutions techniques et technologiques utilisées pour les exécuter. Par exemple, on peut remplacer une base de données ou modifier la façon d'exécuter un traitement, sans changer les règles métier. L’architecture en oignon permet de conserver ces règles indépendantes d’ASP.NET Core, d’Entity Framework Core (EF Core) et d'autres fournisseurs externes. Ainsi, un changement technique aura moins d'impacts sur ces dernières.
C'est pourquoi, nous pouvons dire que l'architecture en oignon organise le code autour du métier pour maîtriser ces dépendances. Présentée par Jeffrey Palermo en 2008, son principe central est simple : les dépendances du code pointent vers le centre de l’application.
Qu’est-ce que l’architecture en oignon ?
Cette architecture représente l’application sous forme de couches concentriques, tel qu'est constitué un oignon. Au centre se trouve le modèle de domaine : les concepts et les règles métier. Autour de lui se placent les traitements métier, puis les éléments techniques et les points d’entrée, comme une API Web.
Les couches extérieures peuvent connaître les couches intérieures. Cependant, l’inverse est interdit. Ainsi, une entité métier ne doit pas avoir besoin de référencer un contrôleur ASP.NET Core ou d’un contexte de données (DbContext) proposé par Entity Framework Core (EF Core). Une couche extérieure peut accéder directement à une couche plus intérieure, sans imposer de référencer chaque couche intermédiaire.
Une organisation possible dans une solution .NET est la suivante :
Ce découpage est un exemple, pas une liste de projets obligatoire. L’essentiel est de respecter le sens des dépendances.
Le principe clé : inverser les dépendances
Supposons qu’un cas d’utilisation doive enregistrer une commande. Il a besoin d’une capacité de persistance, mais il n’a pas besoin de connaître EF Core. Un contrat décrit cette capacité implémentée dans la couche applicative :
public interface IOrderRepository
{
Task AddAsync(Order order, CancellationToken cancellationToken);
}
Puis, l’infrastructure fournit une implémentation utilisant le contexte de données (DbContext) proposé par EF Core :
public sealed class OrderRepository(AppDbContext dbContext)
: IOrderRepository
{
public async Task AddAsync(
Order order,
CancellationToken cancellationToken)
{
await dbContext.Orders.AddAsync(order, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
}
}
Enfin, le projet de démarrage (API) enregistrer le contrat et son implémentation :
Le cas d’utilisation décrit le service dont il a besoin au travers d’une interface. La classe qui utilise EF Core implémente cette interface et fournit ces données. Au démarrage, l’injection de dépendances associe l'inteface (contrat) à la classe concrète. Les références entre projets (.csproj) jouent un autre rôle : elles empêchent le projet qui contient les règles métier de dépendre directement du projet qui contient EF Core. Ainsi, le métier peut utiliser la persistance sans connaître les détails d'implémentation d'EF Core au sein de l'application.
Architecture en oignon et déploiement
Les couches de l’oignon décrivent l’organisation du code et ses dépendances. Elles ne désignent pas des serveurs ou des services à déployer et sur quels serveurs.
Une application en oignon peut donc être déployée comme un monolithe. Elle peut aussi servir à organiser le code interne d’un service au sein d’un système distribué. Ces deux décisions répondent à des questions différentes : comment structurer le code, puis comment déployer l'application.
Les avantages
Des règles métier protégées
Le domaine peut évoluer sans dépendre directement de l’API, d’EF Core ou d’un composant externe (nuget).
Des tests ciblés
Les règles métier et les cas d’utilisation peuvent être vérifiés sans démarrer le serveur Web, ni avoir accès à la base de données.
Des responsabilités lisibles
Le domaine décide, l’application coordonne, l’infrastructure réalise les opérations techniques et l'API expose.
Les limites
Davantage de structure à maintenir
Les projets, contrats et conversions ajoutent du code qui peut être disproportionné pour une simple application.
Des frontières pas toujours explicites
Un mauvais partage des responsabilités peut produire des interfaces artificielles ou disperser une règle métier entre plusieurs couches.
Une indépendance technique relative
Changer de serveur de base de données peut aussi modifier les transactions, les performances et certaines décisions applicatives.
Une discipline collective nécessaire
Sans contrôle des références, les détails techniques peuvent progressivement être implémentés dans le domaine et les cas d’utilisation.
Quand cette architecture est-elle pertinente ?
L’architecture en oignon est utile lorsque l’application possède des règles métier importantes, plusieurs intégrations techniques ou des cas d’utilisation appelés, depuis différents points d’entrée.
Pour une petite application qui se limite essentiellement à saisir et afficher des données, le coût des couches et des contrats peut amoindrir leurs bénéfices et les dépasser. Dans ce cas, iI est conseillé de commencer avec une structure plus simple, et d’introduire des frontières là où la complexité apparaît.
Bonnes pratiques avec .NET
Faire respecter le sens des références
Le domaine reste indépendant ; l’application peut utiliser le domaine, tandis que l’infrastructure implémente les contrats du cœur.
Placer les contrats selon leurs "consommateurs"
Définir une interface dans le domaine ou l’application selon la couche qui en a réellement besoin, sans créer autant d’interfaces que de classes.
Conserver EF Core dans l’infrastructure
Y placer une spécialisation du `DbContext`, sa configuration et ses implémentations de persistance, de surveillance du tracking.
Modéliser les règles dans le domaine
Confier aux entités et aux services de domaine les décisions métier, plutôt que les disperser dans les contrôleurs ou les repositories.
Utiliser l’application pour coordonner
Faire porter aux cas d’utilisation l’enchaînement des opérations, les contrats nécessaires et les résultats attendus.
Assembler les dépendances au démarrage
Enregistrer les implémentations dans la configuration ASP.NET Core, sans introduire leurs types concrets dans les "consommateurs" des interfaces.
Adapter la structure à la complexité
Créer les projets et abstractions qui clarifient de vraies frontières, puis enrichir le découpage lorsque les besoins le justifient.
