Aller au contenu principal

Migrations et seeders EF Core

Cette page complète les bases CLI décrites dans Mise à jour de la base de données. Elle se concentre sur les conventions Lagoon : emplacement des migrations, nommage, snapshot EF Core, application au démarrage et initialisation des données.

Emplacement et nommage​

Les migrations EF Core d'une application Lagoon vivent dans le projet Model :

ÉlémentConvention
DossierApps\*\Model\Migrations\
Classe de migrationPascalCase, nom descriptif
Exemples de nomsInit, AddTableTrackedChanges, LastLoginDate
SnapshotApplicationDbContextModelSnapshot.cs
Snapshot généré

ApplicationDbContextModelSnapshot.cs est généré par EF Core. Ne le modifiez jamais manuellement.

Créer et appliquer une migration​

Les commandes de base sont détaillées dans la page de démarrage. Lorsque la solution contient des projets séparés, ciblez explicitement le projet Model et le projet de démarrage Server.

CLI EF Core
dotnet ef migrations add <Nom> --project Model\APP.Model.csproj --startup-project Server\APP.Server.csproj
dotnet ef database update --project Model\APP.Model.csproj --startup-project Server\APP.Server.csproj

Le contexte applicatif fournit généralement une méthode Configure(...) utilisée par le serveur et par DesignTimeDbContextFactory pour les migrations. Cette fabrique du projet Model dérive de DesignTimeApplicationDbContextFactory<T>. La classe de base charge la configuration du projet Server (appsettings.json, appsettings.Development.json, secrets utilisateur, variables d'environnement, placeholders ${Clé}) et prépare un DbContextOptionsBuilder avec les mêmes options Identity qu'à l'exécution. La fabrique n'a plus qu'à configurer le provider et instancier le contexte.

Model/Context/DesignTimeDbContextFactory.cs
public class DesignTimeDbContextFactory : DesignTimeApplicationDbContextFactory<ApplicationDbContext>
{
protected override ApplicationDbContext CreateDbContext(DesignTimeCreateDbContextEventArgs<ApplicationDbContext> args)
{
// Reuse the EF Core configuration of the application context with the loaded configuration.
ApplicationDbContext.Configure(args.Builder, args.Configuration, true);
return new ApplicationDbContext(args.Builder.Options);
}
}

Pour stocker la chaîne de connexion, ou seulement son mot de passe, hors du dépôt, voir Secrets et placeholders de configuration.

Application au démarrage​

Côté serveur, OnStartAsync(...) peut appeler UpdateDatabaseAsync(...). Cette méthode applique les migrations en attente puis déclenche OnUpdateDatabaseAsync(...).

Main.cs
protected override async Task OnStartAsync(IServiceProvider scopedServiceProvider, CancellationToken cancellationToken)
{
if (Configuration.GetValue("AutoUpdateDatabase", true))
{
// Applique les migrations en attente puis appelle OnUpdateDatabaseAsync.
await UpdateDatabaseAsync(scopedServiceProvider, cancellationToken);
}
}

protected override async Task OnUpdateDatabaseAsync(IServiceProvider scopedServiceProvider, ApplicationDbContext db, CancellationToken cancellationToken)
{
// Initialise les données de référence après la mise à jour du schéma.
UserManager<ApplicationUser> userManager = scopedServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
await DataSeeder.SeedAsync(db, userManager, cancellationToken);

if (ApplicationInformation.IsDevelopment)
{
// Ajoute des données de test uniquement en développement.
await FakeDataSeeder.SeedAsync(db, cancellationToken);
}
}

DataSeeder​

DataSeeder contient les données nécessaires au fonctionnement ou aux référentiels de l'application. Il doit être idempotent : vérifier l'existence avant d'insérer pour pouvoir être rejoué sans doublons.

DataSeeder.cs
public static class DataSeeder
{
public static async Task SeedAsync(ApplicationDbContext db, UserManager<ApplicationUser> userManager, CancellationToken cancellationToken)
{
// Crée le compte administrateur initial uniquement si aucun utilisateur n'existe.
if (!await userManager.Users.AnyAsync(cancellationToken))
{
ApplicationUser newAdmin = new()
{
Id = Guid.NewGuid(),
UserName = "admin",
EmailConfirmed = true,
Email = "demo-admin@lagoon.fr",
FirstName = "Admin",
LastName = "Demo",
};
await userManager.CreateAsync(newAdmin, "Test$1234");
await userManager.AddToRoleAsync(newAdmin, Roles.Admin);
await userManager.AddToRoleAsync(newAdmin, Roles.User);
}
}
}

Lagoon expose aussi le contrat IDatabaseSeeder avec la méthode SeedAsync(CancellationToken cancellationToken) pour les scénarios où le seeder est enregistré dans l'injection de dépendances.

IDatabaseSeeder.cs
public interface IDatabaseSeeder
{
Task SeedAsync(CancellationToken cancellationToken);
}

FakeDataSeeder​

FakeDataSeeder sert aux données de démonstration ou de test local. Les conventions Lagoon recommandent Bogus pour générer des jeux de données de développement.

FakeDataSeeder.cs
public static class FakeDataSeeder
{
public static async Task SeedAsync(ApplicationDbContext db, CancellationToken cancellationToken)
{
// Génère ou insère ici les données de test, puis persiste les changements.
await db.SaveChangesAsync(cancellationToken);
}
}
Environnement

Appelez FakeDataSeeder derrière un test d'environnement, par exemple ApplicationInformation.IsDevelopment, pour éviter d'insérer des données de démonstration en production.

Bonnes pratiques​

SujetRecommandation
IdempotenceTester l'existence des données avant insertion.
TransactionsUtiliser une transaction en cas de SaveChangesAsync(...) intermédiaire dans un traitement composé.
Données de testPlacer les données de développement dans FakeDataSeeder.
ProductionPréférer un script SQL validé au lieu d'un database update direct.
CollationCréer une migration après tout changement de collation.