Condividi tramite


Applicare migrazioni Entity Framework Core in .NET Aspire

Poiché i progetti .NET.NET Aspire utilizzano un'architettura containerizzata, i database sono temporanei e possono essere ricreati in qualsiasi momento. Entity Framework Core (EF Core) usa una funzionalità denominata migrazioni per creare e aggiornare gli schemi di database. Poiché i database vengono ricreati all'avvio dell'app, è necessario applicare le migrazioni per inizializzare lo schema del database ogni volta che viene avviata l'app. Questa operazione viene eseguita registrando un progetto del servizio di migrazione nell'app che esegue le migrazioni durante l'avvio.

In questo tutorial, impari come configurare i progetti .NET Aspire per eseguire le migrazioni EF Core quando l'app si avvia.

Prerequisiti

Per usare .NET.NET Aspire, è necessario che il codice seguente sia installato in locale:

Per altre informazioni, vedere .NET.NET Aspire configurazione e strumentie .NET.NET Aspire SDK.

Scaricare l'app di avvio

Questa esercitazione usa un'app di esempio che illustra come applicare EF Core migrazioni in .NET Aspire. Usare Visual Studio per clonare 'app di esempio da GitHub o usare il comando seguente:

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

L'app di esempio si trova nella cartella SupportTicketApi. Apri la soluzione in Visual Studio o VS Code, prendi un momento per esaminare l'app di esempio e assicurati che funzioni prima di procedere. L'app di esempio è un'API del ticket di supporto rudimentale e contiene i progetti seguenti:

  • SupportTicketApi.Api: Il progetto ASP.NET Core che ospita l'API.
  • SupportTicketApi.Data: contiene i contesti e i modelli di EF Core.
  • SupportTicketApi.AppHost: contiene l'host dell'applicazione e la configurazione .NET.NET Aspire.
  • SupportTicketApi.ServiceDefaults: contiene le configurazioni predefinite del servizio.

Eseguire l'app per assicurarsi che funzioni come previsto. Nel dashboard di .NET.NET Aspire, selezionare l'endpoint https Swagger e testare l'endpoint dell'API GET /api/SupportTickets espandendo l'operazione e selezionando Esegui una prova. Selezionare Esegui per inviare la richiesta e visualizzare la risposta:

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

Creare migrazioni

Inizia creando alcune migrazioni da applicare.

  1. Aprire un terminale (ctrl+` in Visual Studio).

  2. Impostare SupportTicketApiSupportTicketApi.Api come directory corrente.

  3. Usare lo strumento da riga di comando dotnet ef per creare una nuova migrazione per acquisire lo stato iniziale dello schema del database:

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

    Il comando seguente:

    • Esegue lo strumento da riga di comando per la migrazione nella directory SupportTicketApi.Api. dotnet ef viene eseguito in questa posizione perché il servizio API è dove viene usato il contesto del database.
    • Crea una migrazione denominata InitialCreate.
    • Crea la migrazione nella cartella Migrations nel progetto SupportTicketApi.Data.
  4. Modifica il modello in modo che includa una nuova proprietà. Aprire SupportTicketApi.DataModelsSupportTicket.cs e aggiungere una nuova proprietà alla 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. Creare un'altra nuova migrazione per acquisire le modifiche apportate al modello:

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

Adesso hai alcune migrazioni da applicare. Si creerà quindi un servizio di migrazione che applica queste migrazioni durante l'avvio dell'app.

Creare il servizio di migrazione

Per eseguire le migrazioni all'avvio, è necessario creare un servizio che applica le migrazioni.

  1. Aggiungere un nuovo progetto Worker Service alla soluzione. Se si usa Visual Studio, fare clic con il pulsante destro del mouse sulla soluzione in Esplora soluzioni e selezionare Add>New Project. Selezionare Worker Service e assegnare al progetto il nome SupportTicketApi.MigrationService. Se si usa la riga di comando, utilizzare i seguenti comandi dalla cartella della soluzione.

    dotnet new worker -n SupportTicketApi.MigrationService
    dotnet sln add SupportTicketApi.MigrationService
    
  2. Aggiungere i riferimenti al progetto SupportTicketApi.Data e SupportTicketApi.ServiceDefaults al progetto di SupportTicketApi.MigrationService usando Visual Studio o la riga di comando:

    dotnet add SupportTicketApi.MigrationService reference SupportTicketApi.Data
    dotnet add SupportTicketApi.MigrationService reference SupportTicketApi.ServiceDefaults
    
  3. Aggiungere il 📦Aspire.Microsoft.EntityFrameworkCore.SqlServer come riferimento al pacchetto NuGet nel progetto SupportTicketApi.MigrationService utilizzando Visual Studio o la riga di comando:

    dotnet add package Aspire.Microsoft.EntityFrameworkCore.SqlServer
    
  4. Aggiungere le righe evidenziate al file Program.cs nel progetto 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();
    

    Nel codice precedente:

  5. Sostituire il contenuto del file di Worker.cs nel progetto SupportTicketApi.MigrationService con il codice seguente:

    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);
            });
        }
    }
    

    Nel codice precedente:

    • Il metodo ExecuteAsync viene chiamato quando il lavoratore inizia. A sua volta esegue i passaggi seguenti:
      1. Ottiene un riferimento al servizio TicketContext dal fornitore di servizi.
      2. Chiama EnsureDatabaseAsync per creare il database, se questo non esiste.
      3. Chiama RunMigrationAsync per eseguire eventuali migrazioni in sospeso.
      4. Chiama SeedDataAsync per eseguire il seeding del database con i dati iniziali.
      5. Arresta il lavoratore con StopApplication.
    • I metodi EnsureDatabaseAsync, RunMigrationAsynce SeedDataAsync incapsulano tutte le rispettive operazioni di database usando strategie di esecuzione per gestire gli errori temporanei che possono verificarsi durante l'interazione con il database. Per ulteriori informazioni sulle strategie di esecuzione, consulta la sezione Resilienza delle connessioni.

Aggiungere il servizio di migrazione all'orchestratore

Il servizio di migrazione viene creato, ma deve essere aggiunto all'host dell'app .NET.NET Aspire in modo che venga eseguito all'avvio dell'app.

  1. Nel progetto SupportTicketApi.AppHost, apri il file Program.cs.

  2. Aggiungere il codice evidenziato seguente al metodo 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();
    

    In questo modo, il progetto SupportTicketApi.MigrationService viene elencato come servizio nell'host dell'app .NET.NET Aspire.

    Importante

    Se si usa Visual Studioe si è selezionata l'opzione Enlist in Aspire orchestration durante la creazione del progetto di Worker Service, il codice simile viene aggiunto automaticamente con il nome del servizio supportticketapi-migrationservice. Sostituisci quel codice con il codice precedente.

Rimuovere il codice di seeding esistente

Poiché il servizio di migrazione esegue il seeding del database, è necessario rimuovere il codice di seeding dei dati esistente dal progetto API.

  1. Nel progetto SupportTicketApi.Api, apri il file Program.cs.

  2. Eliminare le righe evidenziate.

    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();
            }
        }
    }
    

Testare il servizio di migrazione

Ora che il servizio di migrazione è configurato, eseguire l'app per testare le migrazioni.

  1. Avvia l'app e osserva il dashboard SupportTicketApi.

  2. Dopo un breve periodo di attesa, lo stato del servizio migrations mostrerà Completato.

    Istantanea del dashboard .NET.NET Aspire con il servizio di migrazione in uno stato completato.

  3. Selezionare il collegamento View nel servizio di migrazione per esaminare i log che mostrano i comandi SQL eseguiti.

Ottenere il codice

Puoi trovare l'applicazione di esempio completata su GitHub.

Altro codice di esempio

L'app di esempio Aspire Shop usa questo approccio per applicare le migrazioni. Consultare il progetto AspireShop.CatalogDbManager per l'implementazione del servizio di migrazione.