Partager via


Appliquer des migrations de Entity Framework Core dans .NET Aspire

Étant donné que .NET.NET Aspire projets utilisent une architecture conteneurisée, les bases de données sont éphémères et peuvent être recréées à tout moment. Entity Framework Core (EF Core) utilise une fonctionnalité appelée migrations pour créer et mettre à jour des schémas de base de données. Étant donné que les bases de données sont recréées au démarrage de l’application, vous devez appliquer des migrations pour initialiser le schéma de base de données chaque fois que votre application démarre. Pour ce faire, inscrivez un projet de service de migration dans votre application qui exécute des migrations au démarrage.

Dans ce tutoriel, vous allez apprendre à configurer les projets .NET Aspire pour exécuter des migrations EF Core lors du démarrage de l'application.

Conditions préalables

Pour utiliser .NET.NET Aspire, vous avez besoin de l’installation locale suivante :

Pour plus d’informations, consultez .NETla configuration et les outils.NET Aspire, et .NETle SDK.NET Aspire.

Obtenir l’application de démarrage

Ce tutoriel utilise un exemple d’application qui montre comment appliquer des migrations EF Core dans .NET Aspire. Utilisez Visual Studio pour cloner l’exemple d’application à partir de GitHub ou utilisez la commande suivante :

git clone https://github.com/MicrosoftDocs/aspire-docs-samples/

L’exemple d’application se trouve dans le dossier SupportTicketApi. Ouvrez la solution dans Visual Studio ou VS Code et prenez un moment pour passer en revue l’exemple d’application et assurez-vous qu’elle s’exécute avant de continuer. L’exemple d’application est une API de ticket de support rudimentaire et contient les projets suivants :

  • SupportTicketApi.Api: projet ASP.NET Core qui héberge l’API.
  • SupportTicketApi.Data: contient les contextes et modèles EF Core.
  • SupportTicketApi.AppHost: contient l’hôte et la configuration de l’application .NET.NET Aspire.
  • SupportTicketApi.ServiceDefaults: contient les configurations de service par défaut.

Exécutez l’application pour vous assurer qu’elle fonctionne comme prévu. Dans le tableau de bord , sélectionnez le point de terminaison https https Swagger et testez le point de terminaison GET /api/SupportTickets de l’API en développant l’opération et en sélectionnant Essayer. Sélectionnez Exécuter pour envoyer la demande et afficher la réponse :

[
  {
    "id": 1,
    "title": "Initial Ticket",
    "description": "Test ticket, please ignore."
  }
]

Créer des migrations

Commencez par créer des migrations à appliquer.

  1. Ouvrez un terminal (Ctrl+` dans Visual Studio).

  2. Définissez SupportTicketApiSupportTicketApi.Api comme répertoire actif.

  3. Utilisez l’outil en ligne de commande dotnet ef pour créer une migration pour capturer l’état initial du schéma de base de données :

    dotnet ef migrations add InitialCreate --project ..\SupportTicketApi.Data\SupportTicketApi.Data.csproj
    

    La commande suivante :

    • Exécute l'outil de ligne de commande de migration EF Core dans le répertoire SupportTicketApi.Api. dotnet ef est exécuté à cet emplacement, car le service d’API est l’emplacement où le contexte de base de données est utilisé.
    • Crée une migration nommée InitialCreate.
    • Crée la migration dans le dossier Migrations dans le projet SupportTicketApi.Data.
  4. Modifiez le modèle afin qu’il inclut une nouvelle propriété. Ouvrez SupportTicketApi.DataModelsSupportTicket.cs et ajoutez une nouvelle propriété à la classe SupportTicket :

    public sealed class SupportTicket
    {
        public int Id { get; set; }
        [Required]
        public string Title { get; set; } = string.Empty;
        [Required]
        public string Description { get; set; } = string.Empty;
        public bool Completed { get; set; }
    }
    
  5. Créez une autre migration pour capturer les modifications apportées au modèle :

    dotnet ef migrations add AddCompleted --project ..\SupportTicketApi.Data\SupportTicketApi.Data.csproj
    

Vous avez maintenant des migrations à appliquer. Ensuite, vous allez créer un service de migration qui applique ces migrations au démarrage de l’application.

Créer le service de migration

Pour exécuter les migrations au démarrage, vous devez créer un service qui applique les migrations.

  1. Ajoutez un nouveau projet Worker Service à la solution. Si vous utilisez Visual Studio, cliquez avec le bouton droit sur la solution dans l’Explorateur de solutions et sélectionnez Add>New Project. Sélectionnez Worker Service et nommez le projet SupportTicketApi.MigrationService. Si vous utilisez la ligne de commande, utilisez les commandes suivantes à partir du répertoire de solution :

    dotnet new worker -n SupportTicketApi.MigrationService
    dotnet sln add SupportTicketApi.MigrationService
    
  2. Ajoutez les références de projet SupportTicketApi.Data et de SupportTicketApi.ServiceDefaults au projet SupportTicketApi.MigrationService à l’aide de Visual Studio ou de la ligne de commande :

    dotnet add SupportTicketApi.MigrationService reference SupportTicketApi.Data
    dotnet add SupportTicketApi.MigrationService reference SupportTicketApi.ServiceDefaults
    
  3. Ajoutez la 📦Aspire. Microsoft.EntityFrameworkCore.SqlServer référence de package NuGet au projet SupportTicketApi.MigrationService à l’aide de Visual Studio ou de la ligne de commande :

    dotnet add package Aspire.Microsoft.EntityFrameworkCore.SqlServer
    
  4. Ajoutez les lignes mises en surbrillance au fichier Program.cs dans le projet SupportTicketApi.MigrationService :

    using SupportTicketApi.Data.Contexts;
    using SupportTicketApi.MigrationService;
    
    var builder = Host.CreateApplicationBuilder(args);
    
    builder.AddServiceDefaults();
    builder.Services.AddHostedService<Worker>();
    
    builder.Services.AddOpenTelemetry()
        .WithTracing(tracing => tracing.AddSource(Worker.ActivitySourceName));
    
    builder.AddSqlServerDbContext<TicketContext>("sqldata");
    
    var host = builder.Build();
    host.Run();
    

    Dans le code précédent :

  5. Remplacez le contenu du fichier Worker.cs dans le projet SupportTicketApi.MigrationService par le code suivant :

    using System.Diagnostics;
    
    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.Infrastructure;
    using Microsoft.EntityFrameworkCore.Storage;
    
    using OpenTelemetry.Trace;
    
    using SupportTicketApi.Data.Contexts;
    using SupportTicketApi.Data.Models;
    
    namespace SupportTicketApi.MigrationService;
    
    public class Worker(
        IServiceProvider serviceProvider,
        IHostApplicationLifetime hostApplicationLifetime) : BackgroundService
    {
        public const string ActivitySourceName = "Migrations";
        private static readonly ActivitySource s_activitySource = new(ActivitySourceName);
    
        protected override async Task ExecuteAsync(CancellationToken cancellationToken)
        {
            using var activity = s_activitySource.StartActivity("Migrating database", ActivityKind.Client);
    
            try
            {
                using var scope = serviceProvider.CreateScope();
                var dbContext = scope.ServiceProvider.GetRequiredService<TicketContext>();
    
                await EnsureDatabaseAsync(dbContext, cancellationToken);
                await RunMigrationAsync(dbContext, cancellationToken);
                await SeedDataAsync(dbContext, cancellationToken);
            }
            catch (Exception ex)
            {
                activity?.RecordException(ex);
                throw;
            }
    
            hostApplicationLifetime.StopApplication();
        }
    
        private static async Task EnsureDatabaseAsync(TicketContext dbContext, CancellationToken cancellationToken)
        {
            var dbCreator = dbContext.GetService<IRelationalDatabaseCreator>();
    
            var strategy = dbContext.Database.CreateExecutionStrategy();
            await strategy.ExecuteAsync(async () =>
            {
                // Create the database if it does not exist.
                // Do this first so there is then a database to start a transaction against.
                if (!await dbCreator.ExistsAsync(cancellationToken))
                {
                    await dbCreator.CreateAsync(cancellationToken);
                }
            });
        }
    
        private static async Task RunMigrationAsync(TicketContext dbContext, CancellationToken cancellationToken)
        {
            var strategy = dbContext.Database.CreateExecutionStrategy();
            await strategy.ExecuteAsync(async () =>
            {
                // Run migration in a transaction to avoid partial migration if it fails.
                await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
                await dbContext.Database.MigrateAsync(cancellationToken);
                await transaction.CommitAsync(cancellationToken);
            });
        }
    
        private static async Task SeedDataAsync(TicketContext dbContext, CancellationToken cancellationToken)
        {
            SupportTicket firstTicket = new()
            {
                Title = "Test Ticket",
                Description = "Default ticket, please ignore!",
                Completed = true
            };
    
            var strategy = dbContext.Database.CreateExecutionStrategy();
            await strategy.ExecuteAsync(async () =>
            {
                // Seed the database
                await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
                await dbContext.Tickets.AddAsync(firstTicket, cancellationToken);
                await dbContext.SaveChangesAsync(cancellationToken);
                await transaction.CommitAsync(cancellationToken);
            });
        }
    }
    

    Dans le code précédent :

    • La méthode ExecuteAsync est appelée au démarrage du worker. Elle effectue à son tour les étapes suivantes :
      1. Obtient une référence au service TicketContext du fournisseur de services.
      2. Appelle EnsureDatabaseAsync pour créer la base de données si elle n’existe pas.
      3. Appelle RunMigrationAsync pour appliquer les migrations en attente.
      4. Appelle SeedDataAsync pour amorçage de la base de données avec des données initiales.
      5. Arrête le worker avec StopApplication.
    • Les méthodes EnsureDatabaseAsync, RunMigrationAsyncet SeedDataAsync encapsulent toutes leurs opérations de base de données respectives à l’aide de stratégies d’exécution pour gérer les erreurs temporaires qui peuvent se produire lors de l’interaction avec la base de données. Pour en savoir plus sur les stratégies d’exécution, consultez Résilience de Connexion.

Ajouter le service de migration à l’orchestrateur

Le service de migration est créé, mais il doit être ajouté à l’hôte d’application .NET.NET Aspire afin qu’il s’exécute au démarrage de l’application.

  1. Dans le projet SupportTicketApi.AppHost, ouvrez le fichier Program.cs.

  2. Ajoutez le code en surbrillance suivant à la méthode ConfigureServices :

    var builder = DistributedApplication.CreateBuilder(args);
    
    var sql = builder.AddSqlServer("sql")
                     .AddDatabase("sqldata");
    
    builder.AddProject<Projects.SupportTicketApi_Api>("api")
        .WithReference(sql);
    
    builder.AddProject<Projects.SupportTicketApi_MigrationService>("migrations")
        .WithReference(sql);
    
    builder.Build().Run();
    

    Cela inscrit le projet SupportTicketApi.MigrationService en tant que service dans l’hôte d’application .NET.NET Aspire.

    Important

    Si vous utilisez Visual Studioet que vous avez sélectionné l’option Enlist in Aspire orchestration lors de la création du projet Worker Service, le code similaire est ajouté automatiquement avec le nom du service supportticketapi-migrationservice. Remplacez ce code par le code précédent.

Supprimer le code d’amorçage existant

Étant donné que le service de migration crée la base de données, vous devez supprimer le code d’amorçage de données existant du projet d’API.

  1. Dans le projet SupportTicketApi.Api, ouvrez le fichier Program.cs.

  2. Supprimez les lignes en surbrillance.

    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI();
    
        using (var scope = app.Services.CreateScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<TicketContext>();
            context.Database.EnsureCreated();
    
            if(!context.Tickets.Any())
            {
                context.Tickets.Add(new SupportTicket { Title = "Initial Ticket", Description = "Test ticket, please ignore." });
                context.SaveChanges();
            }
        }
    }
    

Tester le service de migration

Maintenant que le service de migration est configuré, exécutez l’application pour tester les migrations.

  1. Exécutez l’application et observez le tableau de bord SupportTicketApi.

  2. Après une courte attente, l’état du service migrations s’affiche terminé.

    Capture d’écran du tableau de bord .NET.NET Aspire avec le service de migration dans un état terminé.

  3. Sélectionnez le lien View sur le service de migration pour examiner les journaux montrant les commandes SQL exécutées.

Obtenir le code

Vous trouverez l’exemple d’application terminé sur GitHub.

Autres exemples de code

L’exemple d’application Aspire Shop utilise cette approche pour appliquer des migrations. Consultez le projet AspireShop.CatalogDbManager pour l’implémentation du service de migration.