Code First sans douleur : des migrations qui ne renversent pas la production

Code First reste confortable lorsque modèle, migration et base de production avancent ensemble. Entités, index, déploiement Azure et contrôles : rendre le changement prévisible.

L’article complet est disponible ci-dessous. Activez JavaScript pour les quiz et les simulations.

Leçon de production • EF Core • Azure • SQL

Code First devient serein quand la migration est une décision, pas un accident.

Code First est pratique, mais devient vite risqué si la base est traitée comme un effet secondaire automatique du modèle C#. Suivons le processus : modèle, migration, index, déploiement, console de contrôle et recette.

modèle migration Azure recette
01 Une entité n’est pas un formulaire. C’est un contrat avec la base.
02 Une migration exige un plan, un test et un retour possible.
03 Après déploiement, restent des preuves : journal, santé, requête et résultat.

Le problème commence souvent modestement. On ajoute des avis clients, un statut de modération, un historique, un paiement ou un indicateur d’administration. Tout fonctionne en local : formulaire, liste et bouton de publication. La production révèle ensuite si l’on a construit un système ou seulement un formulaire avec une table.

Dans un projet bien mené, la base n’est pas un endroit où l’on enregistrera « tant bien que mal ». Elle relie code, utilisateur, administration, sauvegarde, confidentialité et évolutions. Code First est excellent s’il ne dispense pas de penser les données.

arccloud migration lab

$ dotnet ef migrations add CustomerReviews

✓ instantané du modèle mis à jour

$ dotnet ef migrations script --idempotent -o db.sql

✓ script idempotent généré

$ az webapp deployment slot swap --slot staging

• smoke tests en cours : API, administration, blog public

✓ release validée ▌

Scénario : un petit module qui touche la production

Ajoutons des avis clients : nom, entreprise, note et commentaire. L’avis passe par la modération avant publication. L’e-mail sert uniquement à la vérification et reste privé. L’administrateur doit accepter ou refuser la contribution.

Quelques champs en apparence ; en réalité, des décisions sur confidentialité, index, statuts, antispam, validation, administration et comportement dans six mois.

Mauvaise approche

La table reproduit le formulaire : e-mail, contenu, note et booléen de publication. Puis arrivent modération, origine, antispam et historique. Le modèle grossit sans plan.

Bonne approche

Les données suivent le processus : en attente, accepté, refusé. L’e-mail reste privé ou est conservé sous forme de hash. Les index correspondent aux requêtes réelles du site et de l’administration. La migration est décrite et testée avant publication.

Modèle C# : un minimum pertinent

L’entité exprime les faits durables du système, pas l’apparence du formulaire. Elle porte les règles qui resteront valables quand l’interface changera.

public sealed class CustomerReview
{
    public Guid Id { get; set; }
    public string DisplayName { get; set; } = null!;
    public string? CompanyName { get; set; }
    public string EmailHash { get; set; } = null!;
    public int Rating { get; set; }
    public string Body { get; set; } = null!;
    public ReviewStatus Status { get; set; } = ReviewStatus.Pending;
    public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
    public DateTimeOffset? PublishedAt { get; set; }
}

public enum ReviewStatus
{
    Pending = 0,
    Approved = 1,
    Rejected = 2
}

Point essentiel : ce modèle ne publie pas l’e-mail privé. S’il sert à vérifier, il reste hors du modèle public, ou un hash est conservé pour dédupliquer. Le site a besoin de l’avis, de l’auteur public, de l’entreprise et de la note. L’administration a besoin de l’origine, de l’historique et de la décision.

Pourquoi un statut vaut-il mieux qu’un simple Published = true?

Le processus réel a plus de deux états : en attente, accepté, refusé, à corriger ou masqué ultérieurement. Le booléen tient jusqu’à la première exception. Un statut permet d’évoluer sans multiplier les indicateurs désordonnés.

Configuration EF Core : limites, index et intention

Si la configuration EF semble une formalité, personne n’a probablement demandé comment la table sera lue en production.

builder.Entity<CustomerReview>(e =>
{
    e.Property(x => x.DisplayName)
        .HasMaxLength(120)
        .IsRequired();

    e.Property(x => x.CompanyName)
        .HasMaxLength(160);

    e.Property(x => x.EmailHash)
        .HasMaxLength(96)
        .IsRequired();

    e.Property(x => x.Body)
        .HasMaxLength(1800)
        .IsRequired();

    e.Property(x => x.Rating)
        .IsRequired();

    e.HasIndex(x => new { x.Status, x.PublishedAt });
    e.HasIndex(x => x.EmailHash);
});

L’index Status + PublishedAt est pertinent : le site public lit les avis acceptés, généralement triés par date de publication. L’index sur EmailHash aide la modération et la limitation des abus. Ce sont des index pour des parcours connus, pas des précautions au hasard.

Formulairevalidation + consentement
APIlimitation du débit + hash
Basestatut + index
Administrationdécision + trace
Publicavis acceptés uniquement

Migration : un script que l’on peut lire

La migration ne doit pas être une boîte noire. Sans lire le script généré, l’équipe ignore réellement ce qui sera appliqué.

migrationBuilder.CreateTable(
    name: "CustomerReviews",
    columns: table => new
    {
        Id = table.Column<Guid>(nullable: false),
        DisplayName = table.Column<string>(maxLength: 120, nullable: false),
        CompanyName = table.Column<string>(maxLength: 160, nullable: true),
        EmailHash = table.Column<string>(maxLength: 96, nullable: false),
        Rating = table.Column<int>(nullable: false),
        Body = table.Column<string>(maxLength: 1800, nullable: false),
        Status = table.Column<int>(nullable: false),
        CreatedAt = table.Column<DateTimeOffset>(nullable: false),
        PublishedAt = table.Column<DateTimeOffset>(nullable: true)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_CustomerReviews", x => x.Id);
    });

migrationBuilder.CreateIndex(
    name: "IX_CustomerReviews_Status_PublishedAt",
    table: "CustomerReviews",
    columns: new[] { "Status", "PublishedAt" });

Une nouvelle table est relativement sûre. Modifier l’existant est plus délicat : ajouter une colonne NOT NULL, reconstruire un index, changer un type, remplir les anciennes données ou supprimer un champ. Il faut anticiper verrous et durée.

Que vérifier avant de modifier une table existante ?
  • la valeur par défaut et le risque de verrouillage complet d’une grande table,
  • la possibilité de remplir les données par lots,
  • la compatibilité du nouveau code avec les deux structures,
  • si le rollback concerne le code, les données ou les deux,
  • l’existence d’une sauvegarde et d’un test de restauration, pas seulement d’une promesse.

Azure : une migration explicite, pas de magie au démarrage

Les migrations au démarrage sont pratiques, mais risquées en production. Une migration lourde peut ralentir le lancement, atteindre un timeout ou bloquer plusieurs instances. Pour les changements simples, cela peut se maîtriser ; pour une base critique, une étape explicite est préférable.

dotnet ef migrations script --idempotent -o ./artifacts/db.sql

az webapp deployment slot swap \
  --name arccloud-api \
  --resource-group prod \
  --slot staging

psql "$DATABASE_URL" \
  -v ON_ERROR_STOP=1 \
  -f ./artifacts/db.sql

curl -fsS https://www.arccloudstudio.com/health

Il ne s’agit pas d’imposer une recette unique. Le principe est une migration visible, reproductible et vérifiable. En cas d’échec, il faut distinguer schéma, données, application, routage et configuration.

Runbook de déploiement de base

  1. Preflight : vérifier sauvegarde, connexion et version applicative.
  2. Script : générer un SQL idempotent et le lire avant exécution.
  3. Apply : exécuter la migration séparément, avec arrêt sur erreur.
  4. Smoke : vérifier endpoint, écriture, lecture et administration.
  5. Closeout : consigner journal, durée, résultat et modifications de la base.

Petite console de diagnostic : que se passe-t-il après le déploiement ?

Après déploiement, je demande « qu’est-ce qui prouve le fonctionnement ? ». Voici une recette minimale après évolution du modèle.

SELECT "Status", count(*)
FROM "CustomerReviews"
GROUP BY "Status";

SELECT indexname
FROM pg_indexes
WHERE tablename = 'CustomerReviews';

SELECT "DisplayName", "Rating", "PublishedAt"
FROM "CustomerReviews"
WHERE "Status" = 1
ORDER BY "PublishedAt" DESC
LIMIT 5;

SQL Nombre par statut : pending=3, approved=0, rejected=0

IDX IX_CustomerReviews_Status_PublishedAt trouvé

API l’endpoint public renvoie uniquement les avis acceptés

ADMIN l’action de modération crée une trace d’audit

L’erreur courante : un modèle qui grandit plus vite que la responsabilité

Dans un mauvais Code First, le modèle C# grandit plus vite que la responsabilité des données. Six mois plus tard : colonnes au sens flou, index sans raison, migrations sans description et enregistrements que personne ne sait corriger sans risque.

Un bon Code First garde la base comme contrat de production. Chaque changement explique qui écrit, qui lit, comment filtrer, ce qui est privé, comment revenir et comment vérifier après publication.

Liste de contrôle avant migration

  • Modèle : l’entité décrit le processus, pas seulement les champs du formulaire.
  • Limites : textes, noms et statuts ont des bornes réalistes.
  • Index : ils découlent des requêtes du site ou de l’administration.
  • Confidentialité : e-mails et données sensibles ne sortent pas dans l’API publique.
  • Déploiement : la migration est explicite, journalisée et répétable.
  • Recette : une preuve suit le déploiement, pas seulement « c’est passé ».

Petit test pour votre projet

Choisissez une table et répondez sans ouvrir le code :

Quels sont les trois endpoints qui la lisent le plus ? Quelles données privées ne doivent pas atteindre le frontend ? Quel index sert la liste principale ? Que faut-il vérifier après une migration ?

Une réponse hésitante ne condamne pas le projet. Elle signale que la base demande de l’ordre avant de devenir un problème coûteux.

Une modification de base arrive en production ?

Examinons modèle, migration et risques avant que les utilisateurs subissent le défaut.

Décrivez le module, la table ou le problème. Je proposerai un ordre concret : vérifications, corrections et recette du déploiement.

Présenter mon besoin