Compartir a través de


Aplicar migraciones de Entity Framework Core en .NET Aspire

Dado que .NET.NET Aspire proyectos usan una arquitectura en contenedores, las bases de datos son efímeras y se pueden volver a crear en cualquier momento. Entity Framework Core (EF Core) usa una característica denominada migraciones para crear y actualizar esquemas de base de datos. Dado que las bases de datos se vuelven a crear cuando se inicia la aplicación, debe aplicar migraciones para inicializar el esquema de la base de datos cada vez que se inicia la aplicación. Esto se logra registrando un proyecto de servicio de migración en la aplicación que ejecuta migraciones durante el inicio.

En este tutorial, aprenderá a configurar .NET Aspire proyectos para ejecutar migraciones EF Core durante el inicio de la aplicación.

Prerrequisitos

Para trabajar con .NET.NET Aspire, necesita lo siguiente instalado localmente:

Para obtener más información, consulte configuración y herramientas de .NET.NET Aspirey sdk de .NET.NET Aspire.

Obtención de la aplicación de inicio

En este tutorial se usa una aplicación de ejemplo que muestra cómo aplicar migraciones de EF Core en .NET Aspire. Use Visual Studio para clonar la aplicación de ejemplo desde GitHub o use el comando siguiente:

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

La aplicación de ejemplo se encuentra en la carpeta SupportTicketApi. Abra la solución en Visual Studio o VS Code y dedique un momento a revisar la aplicación de ejemplo y asegúrese de que se ejecuta antes de continuar. La aplicación de ejemplo es una API de incidencias de soporte técnico rudimentaria y contiene los siguientes proyectos:

  • SupportTicketApi.Api: el proyecto ASP.NET Core que alberga la API.
  • SupportTicketApi.Data: contiene los contextos y modelos de EF Core.
  • SupportTicketApi.AppHost: contiene el host de la aplicación y la configuración de .NET.NET Aspire.
  • supportTicketApi.ServiceDefaults: contiene las configuraciones de servicio predeterminadas.

Ejecute la aplicación para asegurarse de que funciona según lo previsto. En el panel de .NET.NET Aspire, seleccione la https punto de conexión de Swagger y pruebe el punto de conexión GET /api/SupportTickets; para ello, expanda la operación y seleccione Pruébelo. Seleccione Ejecutar para enviar la solicitud y ver la respuesta:

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

Crear migraciones

Empiece por crear algunas migraciones para aplicar.

  1. Abra un terminal (Ctrl+` en Visual Studio).

  2. Establezca SupportTicketApiSupportTicketApi.Api como directorio actual.

  3. Use la herramienta de línea de comandos dotnet ef para crear una nueva migración para capturar el estado inicial del esquema de la base de datos:

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

    Comando de procedimiento:

    • En el directorio SupportTicketApi.Api, ejecuta la herramienta de línea de comandos de migración . dotnet ef se ejecuta en esta ubicación porque el servicio de API es donde se usa el contexto de base de datos.
    • Crea una migración denominada InitialCreate.
    • Crea la migración en la carpeta Migrations del proyecto SupportTicketApi.Data.
  4. Modifique el modelo para que incluya una nueva propiedad. Abra SupportTicketApi.DataModelsSupportTicket.cs y agregue una nueva propiedad a la clase 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. Cree otra nueva migración para capturar los cambios en el modelo:

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

Ahora tienes que aplicar algunas migraciones. A continuación, creará un servicio de migración que aplica estas migraciones durante el inicio de la aplicación.

Creación del servicio de migración

Para ejecutar las migraciones al inicio, debe crear un servicio que aplique las migraciones.

  1. Agregue un nuevo proyecto de Worker Service a la solución. Si usa Visual Studio, haga clic con el botón derecho en la solución en el Explorador de soluciones y seleccione Add>New Project. Seleccione Worker Service y asigne al proyecto el nombre SupportTicketApi.MigrationService. Si usa la línea de comandos, use los siguientes comandos desde el directorio de la solución:

    dotnet new worker -n SupportTicketApi.MigrationService
    dotnet sln add SupportTicketApi.MigrationService
    
  2. Agregue las referencias del proyecto SupportTicketApi.Data y SupportTicketApi.ServiceDefaults al proyecto de SupportTicketApi.MigrationService mediante Visual Studio o la línea de comandos:

    dotnet add SupportTicketApi.MigrationService reference SupportTicketApi.Data
    dotnet add SupportTicketApi.MigrationService reference SupportTicketApi.ServiceDefaults
    
  3. Agregue el 📦Aspire. Referencia del paquete NuGet Microsoft.EntityFrameworkCore.SqlServer al proyecto de SupportTicketApi.MigrationService mediante Visual Studio o la línea de comandos:

    dotnet add package Aspire.Microsoft.EntityFrameworkCore.SqlServer
    
  4. Agregue las líneas resaltadas al archivo Program.cs en el proyecto de 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();
    

    En el código anterior:

  5. Reemplace el contenido del archivo Worker.cs en el proyecto de SupportTicketApi.MigrationService por el código siguiente:

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

    En el código anterior:

    • Se llama al método ExecuteAsync cuando se inicia el trabajo. A su vez, realiza los pasos siguientes:
      1. Obtiene una referencia al servicio TicketContext del proveedor de servicios.
      2. Llama a EnsureDatabaseAsync para crear la base de datos si no existe.
      3. Llama a RunMigrationAsync para aplicar cualquier migración pendiente.
      4. Llama a SeedDataAsync para inicializar la base de datos con datos iniciales.
      5. Detiene al trabajador con StopApplication.
    • Los métodos EnsureDatabaseAsync, RunMigrationAsyncy SeedDataAsync encapsulan sus respectivas operaciones de base de datos mediante estrategias de ejecución para controlar errores transitorios que pueden producirse al interactuar con la base de datos. Para obtener más información sobre las estrategias de ejecución, consulte resistencia de conexión.

Añadir el servicio de migración al orquestador

Se crea el servicio de migración, pero debe agregarse al host de la aplicación .NET.NET Aspire para que se ejecute cuando se inicie la aplicación.

  1. En el proyecto SupportTicketApi.AppHost, abra el archivo Program.cs.

  2. Agregue el código resaltado siguiente al 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();
    

    Esto inscribe el proyecto SupportTicketApi.MigrationService como servicio en la aplicación host .NET.NET Aspire.

    Importante

    Si usa Visual Studioy seleccionó la opción Enlist in Aspire orchestration al crear el proyecto de Worker Service, se agrega código similar automáticamente con el nombre del servicio supportticketapi-migrationservice. Reemplace ese código por el código anterior.

Eliminación del código de propagación existente

Dado que el servicio de migración inicializa la base de datos, debe quitar el código de inicialización de datos existente del proyecto de API.

  1. En el proyecto SupportTicketApi.Api, abra el archivo Program.cs.

  2. Elimine las líneas resaltadas.

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

Prueba del servicio de migración

Ahora que el servicio de migración está configurado, ejecute la aplicación para probar las migraciones.

  1. Ejecute la aplicación y observe el panel de SupportTicketApi.

  2. Después de una breve espera, el estado del servicio migrations mostrará Finalizado.

    captura de pantalla del panel de .NET.NET Aspire con el servicio de migración en estado Finalizado.

  3. Seleccione el vínculo View en el servicio de migración para investigar los registros que muestran los comandos SQL que se ejecutaron.

Obtención del código

Puede encontrar la aplicación de ejemplo completada en GitHub.

Más código de ejemplo

La aplicación de ejemplo Aspire Shop usa este enfoque para aplicar migraciones. Consulte el proyecto de AspireShop.CatalogDbManager para la implementación del servicio de migración.