Que fait LocalTax - Architecture en tranches verticales .NET ?
Comment construire et maintenir une architecture en tranches verticales dans .NET — anatomie des tranches (variantes fichier unique et dossier), distribution directe des handlers sans médiateur, préoccupations transversales, ce que les tranches peuvent partager, comment les tranches interagissent avec un modèle de domaine ou les couches Clean, et tests au niveau de la tranche. Utilisez ce guide lors de l'ajout ou de la modification d'un cas d'utilisation/point de terminaison/fonctionnalité dans tout module organisé par tranches verticales, ou lorsque la duplication entre tranches est discutée.
Anatomie d'une tranche verticale
Chaque tranche verticale encapsule un cas d'utilisation complet. Il existe deux variantes :
- Variante fichier unique : Tout le code de la tranche (commande, handler, requête réponse, etc.) réside dans un seul fichier (ex.
CreateOrderSlice.cs). - Variante dossier : Chaque composant est dans un dossier dédié (ex.
CreateOrder/Command.cs,CreateOrder/Handler.cs,CreateOrder/Response.cs).
Structure recommandée (variante fichier unique)
// Slice.cs
public class CreateOrderSlice
{
public record Command(int CustomerId, List<OrderItem> Items);
public record Result(int OrderId);
public class Handler
{
public async Task<Result> HandleAsync(Command command, CancellationToken ct)
{
// Logique métier directe
var order = new Order(command.CustomerId, command.Items);
await _repository.SaveAsync(order, ct);
return new Result(order.Id);
}
}
}
Distribution directe sans médiateur
Au lieu d'utiliser un médiateur comme MediatR, appelez directement le handler :
// Dans un endpoint minimal API
app.MapPost("/create-order", async (CreateOrderSlice.Command command, CreateOrderSlice.Handler handler) =>
{
var result = await handler.HandleAsync(command, CancellationToken.None);
return Results.Ok(result);
});
Préoccupations transversales
Les préoccupations transversales (logging, validation, sécurité) peuvent être gérées de deux manières :
- Dans le handler lui-même : Simple mais dupliqué si plusieurs handlers ont besoin de la même logique.
- Via un pipeline personnalisé : Utilisez des décorateurs ou un middleware ASP.NET Core pour interceptrer l'appel.
// Exemple de décorateur pour la validation
public class ValidationHandler<TResponse> : IHandler<TResponse> where TResponse : class
{
private readonly IHandler<TResponse> _innerHandler;
public ValidationHandler(IHandler<TResponse> innerHandler) => _innerHandler = innerHandler;
public async Task<TResponse> HandleAsync(object command, CancellationToken ct)
{
// Validation ici
return await _innerHandler.HandleAsync(command, ct);
}
}
Partage entre tranches
Les tranches peuvent partager :
- Modèles de domaine : Entités partagées (ex.
Order,Customer). - Services partagés : Repositories, services externes.
- Infrastructure commune : Connexion à la base de données, logging.
Règle d'or : Ne partagez pas le code spécifique à un cas d'utilisation (ex. logique de validation propre à un use case). Si deux slices utilisent la même logique métier, extrayez-la dans un service partagé ou un domaine commun.
Interaction avec le domaine et les couches Clean
- Domaine : Les tranches interagissent avec le domaine via des repositories et des entités. Le domaine contient les règles métier fondamentales.
- Couches Clean : Chaque tranche peut être considérée comme un petit composant horizontal à l'intérieur d'une architecture Clean. Par exemple, le handler est le cas d'utilisation (use case interactor), la commande est le request, et le résultat est le response.
// Exemple d'interaction avec le domaine
public class CreateOrderSlice
{
public class Handler
{
private readonly IOrderRepository _orderRepo;
private readonly ICustomerRepository _customerRepo;
public Handler(IOrderRepository orderRepo, ICustomerRepository customerRepo)
{
_orderRepo = orderRepo;
_customerRepo = customerRepo;
}
public async Task<Result> HandleAsync(Command command, CancellationToken ct)
{
var customer = await _customerRepo.GetByIdAsync(command.CustomerId, ct);
if (customer == null) return new Result(null, "Customer not found");
var order = new Order
{
CustomerId = customer.Id,
Items = command.Items.Select(i => new OrderItem(i.ProductId, i.Quantity)).ToList()
};
await _orderRepo.SaveAsync(order, ct);
return new Result(order.Id, null);
}
}
}
Tests au niveau de la tranche
Chaque tranche doit être testable de manière isolée :
- Tests unitaires : Testez le handler avec des mocks des repositories.
- Tests d'intégration : Testez la tranche entière avec une base de données réelle ou en mémoire.
[Fact]
public async Task CreateOrder_WithValidCommand_ShouldReturnOrderId()
{
// Arrange
var repo = new Mock<IOrderRepository>();
var handler = new CreateOrderSlice.Handler(repo.Object, Mock.Of<ICustomerRepository>());
var command = new CreateOrderSlice.Command(1, new List<OrderItem> { new(1, 2) });
// Act
var result = await handler.HandleAsync(command, CancellationToken.None);
// Assert
Assert.NotNull(result.OrderId);
}
Comment éviter la duplication
Lorsque vous détectez une duplication entre tranches :
- Identifiez le code dupliqué : Est-ce une règle métier, une validation, une logique de mapping ?
- Extrayez dans un service partagé : Par exemple, une classe
OrderServicequi contient la logique de création d'une commande. - Référencez ce service dans les handlers : Ne dupliquez pas le code, injectez le service.
// Service partagé
public class OrderService
{
public async Task CreateOrder(Command command, CancellationToken ct)
{
// Logique partagée
}
}
// Dans le handler
public class CreateOrderSlice
{
public class Handler
{
private readonly OrderService _orderService;
public Handler(OrderService orderService) => _orderService = orderService;
public async Task<Result> HandleAsync(Command command, CancellationToken ct)
{
await _orderService.CreateOrder(command, ct);
return new Result(command.OrderId);
}
}
}
Conclusion
L'architecture en tranches verticales permet de garder chaque cas d'utilisation indépendant, testable et maintenable, tout en réduisant la complexité due aux médiateurs. Suivez ces principes pour construire des systèmes .NET modulaires et évolutifs.