在 Entity Framework Core 中应用 .NET Aspire 迁移

由于 .NET.NET Aspire 项目使用容器化体系结构,因此数据库是临时的,可以随时重新创建。 Entity Framework Core(EF Core)使用一种称为迁移的功能来创建和更新数据库架构。 由于在应用启动时数据库会被重新创建,因此每次启动应用时都需要进行迁移以初始化数据库架构。 这可以通过在应用中注册一个在启动期间执行迁移的迁移服务的项目来实现。

本教程介绍如何配置 .NET Aspire 项目,以在应用启动期间运行 EF Core 迁移。

先决条件

若要使用 .NET.NET Aspire,需要在本地安装以下各项:

有关详细信息,请参阅 .NET.NET Aspire 设置和工具,以及 .NET.NET Aspire SDK

获取入门应用程序

本教程使用了一个示例应用程序,来演示如何在 EF Core中应用 .NET Aspire 迁移。 使用 Visual Studio 从 GitHub 克隆 示例应用 或使用以下命令:

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

示例应用位于 SupportTicketApi 文件夹中。 在 Visual Studio 或 VS Code 中打开解决方案,花点时间查看示例应用程序,并确保它能正常运行,然后再继续。 示例应用是一个基本的支持票证 API,其中包含以下项目:

  • SupportTicketApi.Api:托管 API 的 ASP.NET Core 项目。
  • SupportTicketApi.AppHost:包含 .NET.NET Aspire 应用主机和配置。
  • SupportTicketApi.Data:包含 EF Core 上下文和模型。
  • SupportTicketApi.ServiceDefaults:包含默认服务配置。

运行应用以确保它按预期工作。 在 .NET.NET Aspire 仪表板中,等待直到所有资源都正常运行且健康。 然后选择 https Swagger 终结点,并通过展开操作并选择 试用来测试 API 的 GET /api/SupportTickets 终结点。选择 执行 发送请求并查看响应:

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

关闭显示 Swagger 终结点和 .NET.NET Aspire 仪表板的浏览器选项卡,然后停止调试。

创建迁移

首先创建一些迁移文件以便应用。

  1. 中打开终端(+`Visual Studio)。

  2. SupportTicketApi\SupportTicketApi.Api 设置为当前目录。

  3. 使用 dotnet ef 命令行工具 创建新的迁移来捕获数据库架构的初始状态:

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

    接下来的命令

    • 在 EF Core 目录中运行 迁移命令行工具。 dotnet ef 在这里运行,因为这是 API 服务使用 DB 上下文的地方。
    • 创建名为 InitialCreate 的迁移。
    • SupportTicketApi.Data 项目中的 Migrations 文件夹中创建迁移。
  4. 修改模型,使其包含新属性。 打开 SupportTicketApi.Data\Models\SupportTicket.cs 并将新属性添加到 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. 创建另一个新迁移以记录模型更改。

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

现在你有一些迁移需要执行。 接下来,你将创建一个在应用程序启动时应用这些迁移的迁移服务。

创建迁移服务

若要在启动时运行迁移,需要创建一个应用迁移的服务。

  1. 向解决方案添加新 Worker Service 项目。 如果使用 Visual Studio,请在解决方案资源管理器中右键单击解决方案,然后选择 Add>New Project。 选择 Worker Service,将项目命名为 SupportTicketApi.MigrationService,并将目标设定为 .NET 8.0。 如果使用命令行,请使用解决方案目录中的以下命令:

    dotnet new worker -n SupportTicketApi.MigrationService -f "net8.0"
    dotnet sln add SupportTicketApi.MigrationService
    
  2. 使用 SupportTicketApi.Data 或命令行向 SupportTicketApi.ServiceDefaults 项目添加 SupportTicketApi.MigrationService 和 Visual Studio 项目引用:

    dotnet add SupportTicketApi.MigrationService reference SupportTicketApi.Data
    dotnet add SupportTicketApi.MigrationService reference SupportTicketApi.ServiceDefaults
    
  3. 请使用Visual Studio或命令行,将📦Aspire.Microsoft.EntityFrameworkCore.SqlServer NuGet 包引用添加到SupportTicketApi.MigrationService项目中。

    cd SupportTicketApi.MigrationService
    dotnet add package Aspire.Microsoft.EntityFrameworkCore.SqlServer -v "9.1.0"
    
  4. 将突出显示的行添加到 Program.cs 项目中的 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();
    

    在前面的代码中:

  5. Worker.cs 项目中 SupportTicketApi.MigrationService 文件的内容替换为以下代码:

    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 RunMigrationAsync(dbContext, cancellationToken);
                await SeedDataAsync(dbContext, cancellationToken);
            }
            catch (Exception ex)
            {
                activity?.RecordException(ex);
                throw;
            }
    
            hostApplicationLifetime.StopApplication();
        }
    
        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 dbContext.Database.MigrateAsync(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);
            });
        }
    }
    

    在前面的代码中:

    • 工作人员启动时调用 ExecuteAsync 方法。 它又执行以下步骤:
      1. 可以从服务提供商处获取对 TicketContext 服务的引用。
      2. 调用 RunMigrationAsync 来应用待处理的迁移。
      3. 调用 SeedDataAsync 来为数据库初始化初始数据。
      4. 使用 StopApplication终止工作进程。
    • RunMigrationAsyncSeedDataAsync 方法都使用执行策略封装各自的数据库作,以处理与数据库交互时可能发生的暂时性错误。 若要详细了解执行策略,请参阅 连接恢复

将迁移服务添加到编排器

迁移服务已创建,但需要将其添加到 .NET.NET Aspire 应用主机,以便在应用启动时运行。

  1. SupportTicketApi.AppHost 项目中,打开 Program.cs 文件。

  2. 将以下突出显示的代码添加到 ConfigureServices 方法:

    var builder = DistributedApplication.CreateBuilder(args);
    
    var sql = builder.AddSqlServer("sql", port: 14329)
                     .WithEndpoint(name: "sqlEndpoint", targetPort: 14330)
                     .AddDatabase("sqldata");
    
    builder.AddProject<Projects.SupportTicketApi_Api>("api")
        .WithReference(sql)
        .WaitFor(sql);
    
    builder.AddProject<Projects.SupportTicketApi_MigrationService>("migrations")
        .WithReference(sql)
        .WaitFor(sql);
    
    builder.Build().Run();
    

    这会将 SupportTicketApi.MigrationService 项目登记为 .NET.NET Aspire 应用主机中的服务。

  3. 如果代码无法解析迁移服务项目,请在 AppHost 项目中添加对迁移服务项目的引用:

    dotnet add SupportTicketApi.AppHost reference SupportTicketApi.MigrationService
    

    重要

    如果使用 Visual Studio,并且你在创建 Enlist in Aspire orchestration 项目时选择了 Worker Service 选项,则会自动使用服务名称 supportticketapi-migrationservice添加类似的代码。 将该代码替换为前面的代码。

删除现有播种代码

由于迁移服务为数据库设定种子,因此应从 API 项目中删除现有的数据种子设定代码。

  1. SupportTicketApi.Api 项目中,打开 Program.cs 文件。

  2. 删除突出显示的行。

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

测试迁移服务

配置迁移服务后,请运行应用来测试迁移。

  1. 运行应用并观察 SupportTicketApi 仪表板。

  2. 等待一段时间后,migrations 服务状态会显示 已完成

    .NET.NET Aspire 仪表板的屏幕截图,其中迁移服务处于“已完成”状态。

  3. 选择迁移服务上的 Console logs 图标,以调查显示执行的 SQL 命令的日志。

获取代码

可以在 上找到已完成的 示例应用。

更多示例代码

Aspire Shop 示例应用程序使用这种方法来执行迁移。 请参阅 AspireShop.CatalogDbManager 项目以了解迁移服务的实现。