MVAESTECH
Guide .NET · 05 · Applications

Publié le · par Mathieu Vaes

Architecture applicative

L'architecture sert à rendre les changements bon marché. Le bon style dépend de la taille de l'équipe, du domaine et du besoin de déploiement indépendant, pas de la mode.

Chapitres du guide
En clair

L'architecture, c'est le rangement du code : quoi va où, et qui a le droit de dépendre de qui. Bien rangé, on peut changer de base de données ou ajouter une fonctionnalité sans tout casser. Les règles métier sont au centre, les détails techniques (base, HTTP, e-mails) autour.

Choisir un style Senior

StylePrincipeAdapté àCoût
N-tiers classiqueUI → BLL → DALCRUD simple, applis internesLe métier fuit vers la DAL et l'UI
Clean / Onion / HexagonaleLe domaine au centre, l'infrastructure en périphérieMétier riche, longue durée de viePlus de projets et d'abstractions
Vertical SliceUn dossier par cas d'usage, de l'endpoint à la requête SQLAPI orientées fonctionnalitésDuplication tolérée, discipline requise
Monolithe modulaireUn déploiement, des modules isolés communiquant par contratsDéfaut recommandé pour la plupart des équipesFrontières à faire respecter par tests
MicroservicesUn service = un déploiement = une basePlusieurs équipes autonomes, besoins de scalabilité distinctsRéseau, cohérence éventuelle, observabilité, exploitation

La règle de dépendance Confirmé

Domain Application Infrastructure Présentation / Hôte Les dépendances pointent vers le centre Domain : entités, Value Objects, règles aucune dépendance, pas même EF Core Application : cas d'usage, interfaces IOrderRepository, IClock, handlers CQRS Infrastructure : implémentations EF Core, HttpClient, Pub/Sub, SMTP Présentation : points d'entrée API, worker, WPF ; compose la DI L'Application déclare une interface, l'Infrastructure l'implémente : inversion des dépendances.
Le domaine ne connaît ni la base, ni HTTP, ni le framework. On peut le tester en mémoire en quelques millisecondes.

Arborescence type d'une solution

src/
  Shop.Domain/            # Entités, Value Objects, événements de domaine — zéro package NuGet
  Shop.Application/       # Commandes, requêtes, handlers, validateurs, interfaces (ports)
  Shop.Infrastructure/    # DbContext EF Core, repositories, clients HTTP, messaging (adaptateurs)
  Shop.Api/               # Endpoints, middleware, composition DI, Program.cs
tests/
  Shop.Domain.Tests/
  Shop.Application.Tests/
  Shop.Api.IntegrationTests/   # WebApplicationFactory + Testcontainers
  Shop.ArchitectureTests/      # vérifie la règle de dépendance
Directory.Build.props
Directory.Packages.props       # versions NuGet centralisées
global.json

DDD : le vocabulaire utile Senior

Stratégique

Bounded Context : frontière où un terme a un sens unique (« Client » en facturation ≠ « Client » au support). Langage ubiquitaire : le code reprend les mots des experts métier. Context Map : relations entre contextes (anti-corruption layer, shared kernel).

Tactique

Entité : identité stable. Value Object : défini par ses valeurs, immuable (record). Agrégat : grappe cohérente modifiée via sa racine, une transaction = un agrégat. Événement de domaine : fait métier passé.

public sealed record Money(decimal Amount, string Currency)        // Value Object
{
    public static Money operator +(Money a, Money b) =>
        a.Currency == b.Currency ? a with { Amount = a.Amount + b.Amount }
                                 : throw new InvalidOperationException("Devises différentes");
}

public sealed class Order : AggregateRoot<OrderId>                  // Racine d'agrégat
{
    private readonly List<OrderLine> _lines = [];
    public IReadOnlyList<OrderLine> Lines => _lines;
    public OrderStatus Status { get; private set; } = OrderStatus.Draft;

    public void AddLine(ProductId product, int qty, Money unitPrice)
    {
        if (Status != OrderStatus.Draft) throw new DomainException("Commande déjà validée");
        if (qty <= 0) throw new DomainException("Quantité invalide");
        _lines.Add(new OrderLine(product, qty, unitPrice));
    }

    public void Submit()
    {
        if (_lines.Count == 0) throw new DomainException("Commande vide");
        Status = OrderStatus.Submitted;
        Raise(new OrderSubmitted(Id));                                  // événement de domaine
    }
}

CQRS et pipeline de handlers Confirmé

Séparer les commandes (modifient l'état, passent par le domaine) des requêtes (lisent, peuvent projeter directement en DTO avec AsNoTracking ou Dapper). Un médiateur ajoute des comportements transverses autour de chaque handler : validation, journalisation, transaction.

public sealed record SubmitOrder(Guid OrderId) : IRequest<Result>;

public sealed class SubmitOrderHandler(IOrderRepository orders, IUnitOfWork uow)
    : IRequestHandler<SubmitOrder, Result>
{
    public async Task<Result> Handle(SubmitOrder cmd, CancellationToken ct)
    {
        var order = await orders.GetAsync(new OrderId(cmd.OrderId), ct);
        if (order is null) return Result.NotFound();
        order.Submit();
        await uow.SaveChangesAsync(ct);   // publie aussi les événements via l'outbox
        return Result.Success();
    }
}

// Comportement transverse : validation FluentValidation avant chaque handler
public sealed class ValidationBehavior<TReq, TRes>(IEnumerable<IValidator<TReq>> validators)
    : IPipelineBehavior<TReq, TRes> where TReq : notnull
{
    public async Task<TRes> Handle(TReq req, RequestHandlerDelegate<TRes> next, CancellationToken ct)
    {
        foreach (var v in validators) await v.ValidateAndThrowAsync(req, ct);
        return await next();
    }
}
Licences

MediatR et AutoMapper sont passés sous licence commerciale en 2025 pour leurs versions récentes (avec une édition gratuite sous conditions). Vérifiez la licence avant une montée de version. Alternatives : le générateur de sources Mediator (martinothamar), Wolverine, ou de simples handlers injectés — souvent suffisants. Pour le mapping, un mapping manuel ou Mapperly (source generator) évite la réflexion.

Garder une trace des décisions

Un ADR (Architecture Decision Record) est un fichier Markdown court dans le dépôt : contexte, décision, alternatives écartées, conséquences. Dans deux ans, il répond à « pourquoi c'est fait comme ça ? » sans archéologie.

Piège
  • Microservices prématurés : un monolithe distribué, avec les inconvénients des deux.
  • Repository générique au-dessus d'EF Core : DbContext est déjà un Unit of Work et DbSet un repository. Un repository par agrégat, avec des méthodes métier, apporte de la valeur ; un IRepository<T> générique non.
  • Modèle anémique : des entités à propriétés publiques et toute la logique dans des services. On perd les invariants.
  • Base partagée entre services : couplage par le schéma, aucune indépendance de déploiement.

À retenir

  • Monolithe modulaire par défaut, découpage en services quand une contrainte réelle l'exige.
  • Le domaine ne dépend de rien ; les tests d'architecture le garantissent.
  • Une transaction modifie un agrégat ; la cohérence entre agrégats passe par des événements.

Un projet .NET à cadrer ou à reprendre ?

API REST documentées et testées, applications ASP.NET MVC (y compris VB.NET), migration de services anciens vers une architecture claire : parlons de votre code.