Quand .NET 10 LTS est sorti, le réflexe attendu était de tout bumper : runtime, SDK, et Entity Framework Core 10 dans la foulée. J'ai fait le contraire. Le site que vous lisez tourne sur .NET 10 LTS avec EF Core capé à 9.0.x, et ce choix est volontaire. Voici pourquoi, et surtout les trois pièges concrets rencontrés en production sur les derniers sprints.
Pourquoi caper EF Core à 9.0.x
La raison tient en un mot : Pomelo. C'est le provider MySQL de référence pour EF Core, maintenu par la communauté, et il a structurellement un train de retard sur les sorties Microsoft. Environ deux versions, historiquement. Tant que Pomelo ne publie pas de version stable compatible EF Core 10, monter EF Core revient à construire sur un provider en préversion. Pour un site vitrine personnel, pourquoi pas. Pour une base de données de production, non.
Caper une dépendance n'est pas gratuit pour autant. Il faut surveiller les correctifs de sécurité qui arrivent par les versions patch. Exemple concret : la CVE GHSA-37gx-xxp4-5rgx sur System.Security.Cryptography.Xml a nécessité un pin explicite en 9.0.15, parce que la résolution transitive ne remontait pas la version corrigée toute seule. Un cap de version, ça s'entretient : on fige la mineure, on suit les patchs.
La règle que j'applique en mission est la même que chez moi : la version la plus récente que l'écosystème complet supporte, pas la plus récente tout court. Un ORM dernier cri sur un provider bancal coûte plus cher qu'une version stable d'il y a six mois.
Piège 1 : ajouter une colonne enum sans valeur par défaut
Le cas : une entité avec un statut stocké en chaîne via HasConversion<string>(), et une migration qui ajoute cette colonne sur une table déjà remplie.
public enum ContactStatus { New, Processed, Archived } builder.Property(c => c.Status) .HasConversion<string>() .HasMaxLength(32);
Sans HasDefaultValue, Pomelo backfille les lignes existantes avec une chaîne vide. Et une chaîne vide ne se désérialise vers aucune valeur de l'enum. Résultat : toute requête Where(c => c.Status == ContactStatus.New) se comporte mal sur les lignes historiques, et selon le chemin de lecture, vous récoltez une exception de conversion.
Le correctif tient en une ligne dans la configuration :
builder.Property(c => c.Status)
.HasConversion<string>()
.HasMaxLength(32)
.HasDefaultValue(ContactStatus.New);
La leçon générale : dès qu'une migration AddColumn touche une table avec des données, la question "que vaut cette colonne pour les lignes existantes ?" doit avoir une réponse explicite. Le défaut implicite du provider n'est jamais la bonne réponse.
Piège 2 : le casing mixte snake_case / PascalCase
Ma base applique une convention snake_case sur les tables métier. Mais ASP.NET Core Identity arrive avec ses propres tables, et un ToTable("admin_users") ne renomme que la table : les colonnes restent en PascalCase. On se retrouve donc avec une base où articles.published_at côtoie admin_users.NormalizedUserName.
EF Core s'en moque, puisqu'il génère le SQL depuis le modèle. Le problème surgit dès qu'on sort de l'ORM : une requête ad hoc en SSH sur le serveur de production, un script de migration de données, un export. Sous Linux, MySQL est sensible à la casse des identifiants, et la requête écrite de mémoire échoue ou, pire, passe en silence sur la mauvaise colonne.
Le réflexe que j'ai fini par ancrer : DESCRIBE <table> avant toute requête manuelle, systématiquement. Trente secondes qui évitent une heure de débogage sur un faux résultat. Et si vous démarrez un projet de zéro : appliquez la convention de nommage aux colonnes Identity aussi, dès la première migration. C'est le moment où ça coûte le moins cher.
Piège 3 : le re-seed admin après recréation de base
Scénario vécu : recréation complète de la base en production (drop + recreate + replay des migrations) lors d'une refonte de schéma. Les migrations rejouent parfaitement. Le déploiement est vert. Et l'administration renvoie 401 sur tous les comptes.
Évidemment : les migrations recréent le schéma, pas les données. La table admin_users est vide, et aucun monitoring ne s'en alarme puisque le site public, lui, fonctionne.
La réponse durable n'est pas "penser à recréer le compte", c'est outiller :
- une commande CLI
seed-admindans l'application elle-même, idempotente, qui crée le compte d'administration s'il n'existe pas ; - son intégration dans la procédure de déploiement, pour que le cas "base fraîche" soit couvert sans intervention humaine ;
- un point de vigilance sur l'identité d'exécution : la commande tourne sous l'utilisateur de service, avec son environnement préservé, sinon elle lit la mauvaise configuration et seed la mauvaise base.
Une commande qui crée un compte administrateur a de quoi inquiéter, donc précisons ce qu'elle est et ce qu'elle n'est pas. Ce n'est pas un endpoint HTTP ni une porte dérobée : c'est un mode CLI de l'application, exécutable uniquement avec un accès shell au serveur, c'est-à-dire par quelqu'un qui contrôle déjà la machine et la base. Côté application, aucune création de compte self-service n'existe : ce CLI est l'unique chemin d'amorçage, et c'est un choix de conception. Le mot de passe n'est jamais stocké en clair, il passe par le pipeline ASP.NET Core Identity (politique de complexité appliquée, hachage standard). Et l'idempotence joue aussi un rôle de sécurité : si un compte existe déjà pour cet email, la commande ne touche à rien, ni écrasement ni réinitialisation de mot de passe. Le seul vrai point de vigilance est opérationnel : un mot de passe passé en argument finit dans l'historique shell, donc la procédure documente comment l'en tenir à l'écart.
La règle générale dépasse EF Core : toute donnée dont l'application a besoin pour être opérable (compte admin, référentiels, paramètres) doit avoir un chemin de recréation scripté et idempotent. Si la procédure de reconstruction de votre environnement contient une étape "à la main", elle échouera le jour où vous serez pressé.
Ce que ça change pour une mission
Ces trois pièges ont un point commun : aucun n'apparaît sur un projet neuf qui tourne deux semaines. Ils apparaissent quand un schéma vit, quand des données s'accumulent, quand la production diverge du poste de développement. C'est exactement la différence entre une démo et un outil que vous utilisez encore dans trois ans.
C'est aussi pour ça que je livre les applications avec leurs migrations, leurs seeds scriptés et leur procédure de reconstruction documentée. Le code source vous appartient ; encore faut-il que la personne qui le reprend, chez vous ou ailleurs, puisse reconstruire l'environnement sans archéologie.
