L'utilisation excessive de types primitifs comme int ou string pour représenter des concepts métier (ID client, adresses email, montants) est une source majeure de bugs et de complexité dans les architectures .NET. Ce phénomène, connu sous le nom d'Obsession Primitive, affaiblit la sémantique de votre code. Cet article explore comment la bibliothèque Vogen utilise les générateurs de source pour transformer ces types fragiles en objets de valeur (Value Objects) robustes et typés.
Le problème : L'Obsession Primitive
L'utilisation de types de base pour des données métier critiques présente plusieurs risques :
- Absence de sécurité de type : Un compilateur ne fera pas la différence entre un
int idUtilisateuret unint idCommande. - Validation dispersée : Les règles de validation sont souvent dupliquées à chaque point d'entrée.
- Manque de clarté : La signature des méthodes ne reflète pas l'intention métier.
// Exemple de code problématique
public void EnregistrerVente(int clientID, string codePostal, double montant)
{
if (clientID <= 0) throw new ArgumentException("ID invalide");
if (string.IsNullOrWhiteSpace(codePostal)) throw new ArgumentException("Code postal requis");
// Logique métier...
}
Introduction à Vogen
Vogen est une bibliothèque .NET qui s'appuie sur les Source Generators pour créer automatiquement des Value Objects. Contrairement à une implémentation manuelle, Vogen génère tout le code répétitif (Boilerplate) nécessaire à l'égalité, à la comparaison et à la validation, tout en empêchant l'instanciation incorrecte via des analyseurs de code intégrés.
Installation
Ajoutez le package via la console NuGet :
dotnet add package Vogen
Implémentation d'un Value Object
Pour créer un type sécurisé, il suffit de définir une structure ou une classe partial avec l'attribut [ValueObject].
using Vogen;
[ValueObject<int>]
public partial struct IdentifiantClient { }
Vogen génère automatiquement une méthode d'usine From. Toute tentative d'instanciation directe via new ou default provoquera une erreur de compilation, garantissant l'intégrité de l'objet.
// Usage correct
var id = IdentifiantClient.From(42);
// Erreurs de compilation générées par Vogen
var erreur1 = new IdentifiantClient();
var erreur2 = default(IdentifiantClient);
Vaildation et Transformation des données
L'un des principaux avantages des Value Objects est l'encapsulation de la logique de validation. Vogen permet d'intercepter la création via une méthode statique Validate.
[ValueObject<double>]
public partial struct NoteExamen
{
private static Validation Validate(double valeur) =>
valeur switch
{
< 0 => Validation.Invalid("La note ne peut pas être négative."),
> 20 => Validation.Invalid("La note ne peut pas dépasser 20."),
_ => Validation.Ok
};
}
Normalisation des entrées
Vous pouvez également transformer la donnée avant son stockage (par exemple, mise en majuscules ou suppression d'espaces) via la méthode NormalizeInput.
[ValueObject<string>]
public partial struct CodePromo
{
private static string NormalizeInput(string entree) => entree.Trim().ToUpperInvariant();
private static Validation Validate(string valeur) =>
valeur.Length != 8
? Validation.Invalid("Le code doit comporter 8 caractères.")
: Validation.Ok;
}
Intégration avec les frameworks
Vogen est conçu pour s'intégrer de manière transparente avec l'écosystème .NET courant.
Entity Framework Core (EF Core)
Vogen peut générer des convertisseurs de valeur pour EF Core afin que vos Value Objects soient stockés sous forme de types primitifs dans la base de données.
[ValueObject<Guid>(conversions: Conversions.EfCoreValueConverter)]
public partial struct CommandeId { }
// Configuration dans le DbContext
protected override void OnModelCreating(ModelBuilder mb)
{
mb.Entity<Commande>().Property(c => c.Id)
.HasConversion(new CommandeId.EfCoreValueConverter());
}
Sérialisation JSON (System.Text.Json)
Par défaut, Vogen génère des convertisseurs JSON. L'objet est sérialisé directement vers sa valeur sous-jacente, évitant ainsi un objet JSON imbriqué inutile.
var id = IdentifiantClient.From(101);
string json = JsonSerializer.Serialize(id); // Résultat : "101" au lieu de {"Value": 101}
Considérations de Performance
Le choix entre struct et class pour vos Value Objects impacte les performances :
- Struct (Recommandé) : Allocation sur la pile (stack), idéal pour les petits objets (ID, mesures). Performance quasiment identique aux types primitifs.
- Class : Allocation sur le tas (heap). À privilégier si l'objet est complexe ou nécessite une sémantique de référence (nullité).
| Type | Vitesse relative | Allocation mémoire |
|---|---|---|
| Int primitif | 1.0x | 0 B |
| Vogen Struct | ~1.05x | 0 B |
| Vogen Class | ~1.30x | 24 B+ |
Application Pratique : Architecture en Couches
Dans une architecture propre (Clean Architecture), les Value Objects Vogen renforcent le domaine :
// Couche Domaine
public class Produit
{
public ReferenceProduit Ref { get; }
public Prix PrixUnitaire { get; private set; }
public Produit(ReferenceProduit reference, Prix prix)
{
Ref = reference;
PrixUnitaire = prix;
}
}
// Couche Application (Handler)
public async Task TraiterCommande(CommandeDto dto)
{
// La conversion lève une exception si les données sont invalides
var refProduit = ReferenceProduit.From(dto.Sku);
var montant = Prix.From(dto.Prix);
var produit = new Produit(refProduit, montant);
await _repository.Save(produit);
}
En adoptant cette approche, vous déplacez la validation de la logique métier vers le système de types. Le code devient auto-documenté et les erreurs liées aux types primitifs disparaissent dès la compilation.