Le type d'identifiant idéal pour vos entités métier en C#
Publié le 19/02/2025
Par  Christophe MOMMER

Lorsque l'on design un système avec une architecture avancée, il est nécessaire de donner un identifiant à ses entités métier. Attention, je parle ici des entités métier au sens DDD (Domain Driven Design), et non pas d'entités métier qui se trouveraient être utilisées pour le stockage en base de données. D'ailleurs, pour le choix des clés primaires dans ces cas là, il y a un excellent article sur le blog qui en parle ici.

Chaque entité/aggrégat doit pouvoir être identifié de façon unique avec un identifiant. Dans certains cas, les types d'identifiant sont imposés par le business. Par exemple, dans un système de gestion de santé, il fait sens que l'identifiant d'un patient soit son numéro de sécurité sociale, et donc le type string fait totalement sens ici.

Mais très souvent, le business impose des valeurs plutôt que des identifiants, et l'unicité n'étant pas garantie, il incombe au développeur de faire le nécessaire. À cet effet reviens souvent la même question : quel type d'identifiant choisir.

On va très naturellement récupérer des éléments de ce que l'on connait, donc le débat int vs Guid va entrer en vigueur. Analysons rapidement les avantages et inconvénients de chaque approche.

Dans le cas d'un int, il va sans dire que ça fait facilement rapport au sens logique des humains : compter les éléments. Ainsi, c'est naturel et évident qu'après l'entité 1 vienne l'entité 2. Lorsqu'on décide d'analyser, dans une session de debug par exemple, les liens entre les éléments de notre système, on comprend vite les liens et on a facile à retenir qu'il s'agit là de l'entité N. Les entiers semblent donc un type de clé naturel, facile à lire, comprendre et retenir.

Néanmoins, ils ne sont pas sans désavantages. Déjà, on pourra leur opposer qu'ils sont prévisibles. Ainsi, si vous décidez d'exposer d'une façon ou d'une autre votre entité (comme par le biais d'une API par exemple), il ne faudra probablement pas remonter la valeur système de type entière, car cela ouvrira une potentielle faille de sécurité pour les personnes mal intentionnées qui pourront prédire l'existence de certains éléments quand bien même elles me sont pas censées y avoir accès. Bien sûr, il est possible de sécuriser cela, mais au prix d'un effort supplémentaire dont on pourrait se passer en cachant cette valeur.

Mais pour moi, le principal désavantage n'est pas ce dernier. C'est plutôt la nécessité de respecter une séquence pour éviter les doublons et arriver à gérer correctement "les trous" lorsque l'on supprime une entité. Il faudra ainsi avoir un service qui se chargera de tracker la valeur du compteur pour s'assurer que chaque entité dans le système possède bien un identifiant unique, avec tous les challenges que ça implique (je pense notamment au multi-threading pour garantir l'unicité d'une valeur). Bien entendu, les BDD dignes de ce nom vous proposer des int auto-increment pour vos lignes en BDD, mais souvenons-nous ici que certaines entités métiers ne sont pas forcément liées à une table bien particulière, c'est donc pour moi une fausse solution. De plus, dans certains systèmes, il peut faire sens de déléguer au client la responsabilité de créer la valeur de l'identifiant, ce qui rend encore plus complexe cette solution à base d'entier.

De ce fait, l'alternative est d'avoir une valeur pseudo-aléatoire, dont la génération ne se base pas sur un système logique et prédictible : le Guid.

Je pense que tout le monde voit immédiatement le désavantage de ce dernier : il est long et échappe à la logique humaine. Il est toujours difficile de se se rappeler de la totalité d'un Guid lors d'une session de debug et vous faire probablement comme moi : vous retenez les premiers et les derniers caractères. 

Cependant, le Guid offre cet avantage qu'il se génere sans contraintes et, en .NET tout du moins, peut être utilisé dans un contexte où la charge est importante, le risque de collision étant extrêmement réduit. 

Seulement voilà, ce dernier n'est pas sans faille. Car au-delà de son côté illisible, le Guid en v4 offre un iconvénient majeur : sa géneration est totalement aléatoire et ainsi deux éléments générés à quelques millisecondes d'intervalle auront une valeur diamétralement opposée. C'est pourquoi la v7 du Guid tend à corriger cela en incorporant dans la première partie de la valeur la notion temporelle. Et depuis .NET 9, on peut en génerer facilement !

Guid id = Guid.CreateVersion7();

Cela corrige ce souci d'ordre des entités qui seraient triées par rapport à leur id et, même s'il est plus gourmand en BDD qu'un int, l'ordre nous permet d'échapper à de potentiels soucis de performances (voir le blog mentionné au début du post)

Mais ... Ce n'est pas parfait. Simplement car la v7 est plus gourmande et plus longue à génerer que la v4 (en .NET 9, c'est significatif, peut-être que les performances vont s'améliorer dans le version futures)

De plus, même si la v7 est une avancée considérable, il existe un risque de fragmentation si on met le système sous pression.

D'où nous arrivons à notre type "ultime" : le ULID (Universally Unique Lexicographically Sortable Identifier)

La spécification de ce dernier tend à essayer de "corriger" les manquements de son grand-frère, le Guid v7, notamment grâce à son design pour les environnements subissant une charge lourde, son efficacité d'encodage et l'utilisation de la base32 Crockford pour éviter les caractères ambigüs.

Maintenant, reste une question en suspens : comment les générer en .NET ?

Pour le moment, seule l'approche communautaire nous offre une solution, avec le package Ulid sur Nuget.

L'utilisation ressemble à s'y méprendre à celle de la classe Guid et offre des performances de haut vol (voici le résultat du benchmark de la génération d'une nouvelle valeur avec .NET 9) :

Bien entendu, ce type d'identifiant peut aussi être généré par le client et nous permet de respecter une séquence. L'élimination des caractères ambigüs nous permet aussi de simplifier (un peu) la lecture par à d'autres types d'identifiant aléatoires. Pour moi, ça en fait un excellent candidat pour les identifiants de vos objets métiers ! (même si, évidemment, chaque système a ses propres contraintes et cela nécessitera d'être étudié selon votre situation)