Partilhar via


Aplicar migrações Entity Framework Core no .NET Aspire

Como .NET.NET Aspire projetos usam uma arquitetura conteinerizada, os bancos de dados são efêmeros e podem ser recriados a qualquer momento. Entity Framework Core (EF Core) usa um recurso chamado migrações para criar e atualizar esquemas de banco de dados. Como os bancos de dados são recriados quando o aplicativo é iniciado, você precisa aplicar migrações para inicializar o esquema de banco de dados sempre que o aplicativo for iniciado. Isso é feito registrando um projeto de serviço de migração em seu aplicativo que executa migrações durante a inicialização.

Neste tutorial, você aprenderá a configurar .NET Aspire projetos para executar migrações de EF Core durante a inicialização do aplicativo.

Pré-requisitos

Para trabalhar com .NET.NET Aspire, você precisa do seguinte instalado localmente:

Para obter mais informações, consulte .NET Aspire, e .NETSDK.NET Aspire.

Obter a aplicação inicial

Este tutorial utiliza uma aplicação de exemplo que demonstra como aplicar migrações de EF Core em .NET Aspire. Use Visual Studio para clonar o aplicativo de exemplo do GitHub ou use o seguinte comando:

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

O aplicativo de exemplo está na pasta SupportTicketApi. Abra a solução no Visual Studio ou VS Code e reserve um momento para analisar o aplicativo de exemplo e verificar se ele é executado antes de prosseguir. A aplicação de exemplo é uma API de pedido de suporte rudimentar e contém os seguintes projetos:

  • SupportTicketApi.Api: O projeto ASP.NET Core que hospeda a API.
  • SupportTicketApi.Data: Contém os EF Core contextos e modelos.
  • SupportTicketApi.AppHost: contém a aplicação .NET.NET Aspire host e configuração.
  • SupportTicketApi.ServiceDefaults: Contém as configurações de serviço padrão.

Execute o aplicativo para garantir que ele funcione conforme o esperado. No painel .NET.NET Aspire, selecione o ponto de extremidade https Swagger e teste o ponto de extremidade GET /api/SupportTickets da API, expandindo a operação e selecionando Experimente. Selecione Executar para enviar a solicitação e visualizar a resposta:

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

Criar migrações

Comece por criar algumas migrações para aplicar.

  1. Abra um terminal (Ctrl+` no Visual Studio).

  2. Defina SupportTicketApiSupportTicketApi.Api como o diretório atual.

  3. Use a ferramenta de linha de comando dotnet ef para criar uma nova migração para capturar o estado inicial do esquema de banco de dados:

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

    O comando seguinte:

    • Executa a ferramenta de linha de comando de migração EF Core no diretório SupportTicketApi.Api. dotnet ef é executado neste local porque o serviço de API é onde o contexto de banco de dados é usado.
    • Cria uma migração chamada InitialCreate.
    • Cria a migração na pasta Migrations no projeto SupportTicketApi.Data.
  4. Modifique o modelo para que ele inclua uma nova propriedade. Abra SupportTicketApi.DataModelsSupportTicket.cs e adicione uma nova propriedade à 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. Crie outra nova migração para capturar as alterações no modelo:

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

Agora você tem algumas migrações para aplicar. Em seguida, você criará um serviço de migração que aplica essas migrações durante a inicialização do aplicativo.

Criar o serviço de migração

Para executar as migrações na inicialização, você precisa criar um serviço que aplique as migrações.

  1. Adicione um novo projeto Worker Service à solução. Se estiver usando Visual Studio, clique com o botão direito do mouse na solução no Gerenciador de Soluções e selecione Add>New Project. Selecione Worker Service e nomeie o projeto SupportTicketApi.MigrationService. Se estiver usando a linha de comando, use os seguintes comandos do diretório da solução:

    dotnet new worker -n SupportTicketApi.MigrationService
    dotnet sln add SupportTicketApi.MigrationService
    
  2. Adicione as referências de projeto SupportTicketApi.Data e SupportTicketApi.ServiceDefaults ao projeto SupportTicketApi.MigrationService usando Visual Studio ou a linha de comando:

    dotnet add SupportTicketApi.MigrationService reference SupportTicketApi.Data
    dotnet add SupportTicketApi.MigrationService reference SupportTicketApi.ServiceDefaults
    
  3. Adicione o 📦Aspire. Microsoft.EntityFrameworkCore.SqlServer referência de pacote NuGet para o projeto SupportTicketApi.MigrationService usando Visual Studio ou a linha de comando:

    dotnet add package Aspire.Microsoft.EntityFrameworkCore.SqlServer
    
  4. Adicione as linhas realçadas ao arquivo Program.cs no projeto 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();
    

    No código anterior:

  5. Substitua o conteúdo do arquivo Worker.cs no projeto SupportTicketApi.MigrationService com o seguinte código:

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

    No código anterior:

    • O método ExecuteAsync é chamado quando o trabalhador começa. Ele, por sua vez, executa as seguintes etapas:
      1. Obtém uma referência ao serviço TicketContext do provedor de serviços.
      2. Chama EnsureDatabaseAsync para criar a base de dados se esta não existir.
      3. Chama RunMigrationAsync para aplicar quaisquer migrações pendentes.
      4. Chama SeedDataAsync para semear o banco de dados com dados iniciais.
      5. Interrompe o trabalhador com StopApplication.
    • Os métodos EnsureDatabaseAsync, RunMigrationAsynce SeedDataAsync encapsulam suas respetivas operações de banco de dados usando estratégias de execução para lidar com erros transitórios que podem ocorrer ao interagir com o banco de dados. Para saber mais sobre estratégias de execução, consulte Resiliência de Conexão.

Adicionar o serviço de migração ao orquestrador

O serviço de migração é criado, mas precisa ser adicionado ao host do aplicativo .NET.NET Aspire para que seja executado quando o aplicativo for iniciado.

  1. No projeto SupportTicketApi.AppHost, abra o arquivo Program.cs.

  2. Adicione o seguinte código realçado ao método 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();
    

    Isso regista o projeto SupportTicketApi.MigrationService como um serviço no servidor de aplicações .NET.NET Aspire.

    Importante

    Se você estiver usando Visual Studioe tiver selecionado a opção Enlist in Aspire orchestration ao criar o projeto Worker Service, um código semelhante será adicionado automaticamente com o nome do serviço supportticketapi-migrationservice. Substitua esse código pelo código anterior.

Remover código de propagação existente

Como o serviço de migração semeia o banco de dados, você deve remover o código de propagação de dados existente do projeto de API.

  1. No projeto SupportTicketApi.Api, abra o arquivo Program.cs.

  2. Exclua as linhas realçadas.

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

Testar o serviço de migração

Agora que o serviço de migração está configurado, execute o aplicativo para testar as migrações.

  1. Execute o aplicativo e observe o painel SupportTicketApi.

  2. Após uma breve espera, o estado do serviço migrations exibirá Concluído.

    Uma captura de ecrã do painel de controlo .NET.NET Aspire com o serviço de migração em estado de Concluído.

  3. Selecione o link View no serviço de migração para investigar os logs que mostram os comandos SQL que foram executados.

Obter o código

Você pode encontrar o aplicativo de exemplo concluído em GitHub.

Mais código de exemplo

O aplicativo de exemplo Aspire Shop usa essa abordagem para aplicar migrações. Consulte o projeto AspireShop.CatalogDbManager para a implementação do serviço de migração.