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:
- .NET 8,0 ou .NET 9,0
- Um ambiente de execução de contentor compatível com OCI, como:
- Docker Desktop ou Podman. Para obter mais informações, consulte Container runtime.
- Um ambiente de desenvolvedor integrado (IDE) ou editor de código, como:
- Visual Studio 2022 versão 17.9 ou superior (opcional)
-
Visual Studio Code (Opcional)
- C# Dev Kit: Extensão (Opcional)
- JetBrains Rider com o plugin .NET.NET Aspire (Opcional)
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.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.
Abra um terminal (Ctrl+` no Visual Studio).
Defina SupportTicketApiSupportTicketApi.Api como o diretório atual.
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.
- Executa a ferramenta de linha de comando de migração EF Core no diretório SupportTicketApi.Api.
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; } }
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.
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
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
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
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:
- O método de extensão
AddServiceDefaults
adiciona a funcionalidade padrão de serviço. - O método de extensão
AddOpenTelemetry
configura OpenTelemetry funcionalidade. - O método de extensão
AddSqlServerDbContext
adiciona o serviçoTicketContext
à coleção de serviços. Este serviço é usado para executar migrações e semear o banco de dados.
- O método de extensão
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:- Obtém uma referência ao serviço
TicketContext
do provedor de serviços. - Chama
EnsureDatabaseAsync
para criar a base de dados se esta não existir. - Chama
RunMigrationAsync
para aplicar quaisquer migrações pendentes. - Chama
SeedDataAsync
para semear o banco de dados com dados iniciais. - Interrompe o trabalhador com
StopApplication
.
- Obtém uma referência ao serviço
- Os métodos
EnsureDatabaseAsync
,RunMigrationAsync
eSeedDataAsync
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.
- O método
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.
No projeto SupportTicketApi.AppHost, abra o arquivo Program.cs.
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.
No projeto SupportTicketApi.Api, abra o arquivo Program.cs.
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.
Execute o aplicativo e observe o painel SupportTicketApi.
Após uma breve espera, o estado do serviço
migrations
exibirá Concluído.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.