Skip to content

迁移文件管理 ​

在团队协作和长期项目中,EF Core 迁移文件的管理变得至关重要。本章将深入探讨迁移文件的组织、命名规范、冲突解决、版本控制和自动化部署策略。

目录 ​


1. 迁移文件结构解析 ​

1.1 迁移文件组成 ​

每个 EF Core 迁移由三个文件组成:

Migrations/
├── 20240101120000_InitialCreate.cs              // 主迁移类
├── 20240101120000_InitialCreate.Designer.cs     // 元数据(自动生成)
└── AppDbContextModelSnapshot.cs                 // 当前模型快照

主迁移类分析:

csharp
// 文件名: 20240101120000_InitialCreate.cs
[DbContext(typeof(AppDbContext))]
[Migration("20240101120000_InitialCreate")]
public partial class InitialCreate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Products",
            columns: table => new
            {
                Id = table.Column<int>(type: "int", nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                Name = table.Column<string>(type: "nvarchar(200)", nullable: false),
                Price = table.Column<decimal>(type: "decimal(18,2)", nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Products", x => x.Id);
            });
        
        migrationBuilder.CreateIndex(
            name: "IX_Products_Name",
            table: "Products",
            column: "Name");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(name: "Products");
    }
}

Designer 文件(自动生成,不应手动修改):

csharp
// 文件名: 20240101120000_InitialCreate.Designer.cs
[DbContext(typeof(AppDbContext))]
[Migration("20240101120000_InitialCreate")]
partial class InitialCreate
{
    protected override void BuildTargetModel(ModelBuilder modelBuilder)
    {
#pragma warning disable 612, 618
        modelBuilder.Entity("Product", b =>
        {
            b.Property<int>("Id")
                .ValueGeneratedOnAdd()
                .HasColumnType("int")
                .HasAnnotation("SqlServer:ValueGenerationStrategy", 
                    SqlServerValueGenerationStrategy.IdentityColumn);
            
            b.Property<string>("Name")
                .IsRequired()
                .HasMaxLength(200)
                .HasColumnType("nvarchar(200)");
            
            b.Property<decimal>("Price")
                .HasColumnType("decimal(18,2)");
            
            b.HasKey("Id");
            b.ToTable("Products");
        });
#pragma warning restore 612, 618
    }
}

模型快照文件(整个数据库的最终状态):

csharp
// 文件名: AppDbContextModelSnapshot.cs
[DbContext(typeof(AppDbContext))]
partial class AppDbContextModelSnapshot : ModelSnapshot
{
    protected override void BuildModel(ModelBuilder modelBuilder)
    {
#pragma warning disable 612, 618
        // 包含所有实体的最终模型状态
        modelBuilder.Entity("Product", b => { /* ... */ });
        modelBuilder.Entity("Order", b => { /* ... */ });
        modelBuilder.Entity("Customer", b => { /* ... */ });
#pragma warning restore 612, 618
    }
}

1.2 迁移文件元数据 ​

MigrationAttribute 解析:

csharp
// 属性定义
[AttributeUsage(AttributeTargets.Class)]
public class MigrationAttribute : Attribute
{
    public string Id { get; }      // 唯一标识符(时间戳)
    public string Name { get; }    // 迁移名称
    
    public MigrationAttribute(string idAndName)
    {
        var parts = idAndName.Split('_', 2);
        Id = parts[0];             // "20240101120000"
        Name = parts.Length > 1 ? parts[1] : "";  // "InitialCreate"
    }
}

迁移 ID 生成规则:

csharp
// EF Core 内部实现(简化版)
public static class MigrationIdGenerator
{
    public static string GenerateId(DateTime timestamp)
    {
        // 格式: yyyyMMddHHmmss (UTC 时间)
        return timestamp.ToUniversalTime().ToString("yyyyMMddHHmmss");
    }
}

// 示例:
// 2024-01-01 12:00:00 UTC → "20240101120000"
// 2024-06-15 09:30:45 UTC → "20240615093045"

2. 迁移文件命名规范 ​

2.1 自动命名 vs 手动命名 ​

命令行创建迁移:

bash
# 方式1: 自动使用时间戳(推荐)
dotnet ef migrations add AddProductCategory

# 生成的文件:
# 20240115103000_AddProductCategory.cs

# 方式2: 自定义前缀(不推荐,容易混乱)
dotnet ef migrations add 20240115_CustomPrefix

# 生成的文件:
# 20240115103000_20240115_CustomPrefix.cs  # 重复的时间戳!

2.2 命名最佳实践 ​

✅ 推荐的命名模式:

bash
# 1. 动词 + 名词 (清晰表达意图)
dotnet ef migrations add AddProductCategory
dotnet ef migrations add RemoveCustomerPhone
dotnet ef migrations add RenameOrderTotal
dotnet ef migrations add SplitCustomerAddress

# 2. 使用 PascalCase (与 C# 类名一致)
dotnet ef migrations add CreateOrdersTable       # ✅ 好
dotnet ef migrations add create_orders_table     # ❌ 避免

# 3. 保持简洁但描述性
dotnet ef migrations add AddAuditFields          # ✅ 好
dotnet ef migrations add AddCreatedAtUpdatedAtCreatedByUpdatedByToAllTables  # ❌ 太长

# 4. 按功能模块分组
dotnet ef migrations add Product_AddCategory
dotnet ef migrations add Order_AddShippingAddress
dotnet ef migrations add Customer_AddLoyaltyPoints

❌ 避免的命名:

bash
# 1. 模糊的名称
dotnet ef migrations add Update1
dotnet ef migrations add Fix
dotnet ef migrations add Changes

# 2. 包含技术细节
dotnet ef migrations add AddColumnCategoryIdToProductsTable
dotnet ef migrations add CreateIndexOnProductName

# 3. 使用时间戳作为名称的一部分
dotnet ef migrations add 20240115_AddProduct  # 时间戳已存在于文件名中

2.3 迁移注释文档 ​

在迁移类中添加 XML 注释:

csharp
/// <summary>
/// 为产品表添加分类支持
/// - 新增 Categories 表
/// - 为 Products 表添加 CategoryId 外键
/// - 迁移历史数据到默认分类
/// </summary>
[DbContext(typeof(AppDbContext))]
[Migration("20240115103000_AddProductCategory")]
public partial class AddProductCategory : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // 步骤1: 创建分类表
        migrationBuilder.CreateTable(
            name: "Categories",
            columns: table => new
            {
                Id = table.Column<int>(type: "int", nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                Name = table.Column<string>(type: "nvarchar(100)", nullable: false),
                Description = table.Column<string>(type: "nvarchar(500)", nullable: true)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Categories", x => x.Id);
            });
        
        // 步骤2: 为产品表添加外键
        migrationBuilder.AddColumn<int>(
            name: "CategoryId",
            table: "Products",
            type: "int",
            nullable: false,
            defaultValue: 1);  // 默认为"未分类"
        
        migrationBuilder.AddForeignKey(
            name: "FK_Products_Categories_CategoryId",
            table: "Products",
            column: "CategoryId",
            principalTable: "Categories",
            principalColumn: "Id",
            onDelete: ReferentialAction.Restrict);
        
        // 步骤3: 插入默认分类
        migrationBuilder.Sql(@"
            INSERT INTO Categories (Id, Name, Description)
            VALUES (1, '未分类', '默认分类')
        ");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        // 反向操作...
    }
}

3. 迁移文件组织策略 ​

3.1 默认目录结构 ​

Project/
├── Migrations/
│   ├── 20240101120000_InitialCreate.cs
│   ├── 20240101120000_InitialCreate.Designer.cs
│   ├── 20240115103000_AddProductCategory.cs
│   ├── 20240115103000_AddProductCategory.Designer.cs
│   └── AppDbContextModelSnapshot.cs

3.2 按功能模块组织 ​

对于大型项目,可以按模块分文件夹:

Project/
├── Migrations/
│   ├── Core/                          // 核心模块
│   │   ├── 20240101120000_InitialCreate.cs
│   │   └── 20240105140000_AddAuditFields.cs
│   ├── Products/                      // 产品模块
│   │   ├── 20240110100000_AddCategories.cs
│   │   └── 20240115120000_AddProductImages.cs
│   ├── Orders/                        // 订单模块
│   │   ├── 20240120090000_CreateOrders.cs
│   │   └── 20240125150000_AddOrderTracking.cs
│   └── AppDbContextModelSnapshot.cs

配置自定义迁移目录:

csharp
// 在 DbContext 中指定
[Migration("20240115103000_AddProductCategory")]
public partial class AddProductCategory : Migration
{
    // ...
}

// 命令行指定输出目录
dotnet ef migrations add AddProductCategory --output-dir Migrations/Products

// 或者在 .csproj 中配置
<Project>
  <PropertyGroup>
    <EfMigrationsDir>Migrations/Core</EfMigrationsDir>
  </PropertyGroup>
</Project>

3.3 多 DbContext 的迁移管理 ​

当项目有多个 DbContext 时:

Project/
├── Migrations/
│   ├── AppDbContext/
│   │   ├── 20240101120000_InitialCreate.cs
│   │   └── AppDbContextModelSnapshot.cs
│   ├── IdentityDbContext/
│   │   ├── 20240101120000_InitialCreate.cs
│   │   └── IdentityDbContextModelSnapshot.cs
│   └── AuditDbContext/
│       ├── 20240101120000_InitialCreate.cs
│       └── AuditDbContextModelSnapshot.cs

命令行指定 Context:

bash
# 为特定的 DbContext 创建迁移
dotnet ef migrations add AddUserRoles --context IdentityDbContext
dotnet ef migrations add AddAuditLogs --context AuditDbContext

# 应用特定 Context 的迁移
dotnet ef database update --context IdentityDbContext
dotnet ef database update --context AuditDbContext

# 生成脚本
dotnet ef migrations script --context AppDbContext

4. 迁移冲突检测与解决 ​

4.1 什么是迁移冲突? ​

冲突场景:

开发者 A                         开发者 B
  |                                 |
  |-- 基于快照 S1                    |-- 基于快照 S1
  |                                 |
  |-- 创建迁移 M_A                   |-- 创建迁移 M_B
  |                                 |
  |-- 更新快照 → S2                  |-- 更新快照 → S2' (过时!)
  |                                 |
  |-- 提交代码                       |-- 尝试提交代码 (冲突!)

冲突表现:

bash
# 开发者 B 拉取代码后尝试创建新迁移
dotnet ef migrations add AddNewFeature

# 错误信息:
# The model snapshot is out of sync with the latest migration.
# Please update your model snapshot to match the latest migration.

4.2 冲突预防策略 ​

策略1: 频繁同步代码

bash
# 每次创建迁移前先拉取最新代码
git pull origin main

# 确保本地快照是最新的
dotnet ef dbcontext info

# 然后再创建迁移
dotnet ef migrations add AddNewFeature

策略2: 迁移审查流程

markdown
## Pull Request 检查清单

- [ ] 确认迁移基于最新的模型快照
- [ ] 检查迁移文件名是否遵循命名规范
- [ ] 验证 Up 和 Down 方法是否正确
- [ ] 测试迁移是否可以成功应用
- [ ] 测试迁移是否可以回滚
- [ ] 确认迁移不会破坏现有数据

策略3: CI/CD 自动化检测

yaml
# GitHub Actions 示例
name: Check EF Migrations
on: [pull_request]

jobs:
  check-migrations:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup .NET
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: '8.0.x'
      
      - name: Install EF Tools
        run: dotnet tool install --global dotnet-ef
      
      - name: Check for pending migrations
        run: |
          dotnet ef migrations bundle
          if [ $? -ne 0 ]; then
            echo "Error: Model changes detected without corresponding migration"
            exit 1
          fi
      
      - name: Verify migration can be applied
        run: |
          dotnet ef database update --connection "$TEST_DB_CONNECTION"
          if [ $? -ne 0 ]; then
            echo "Error: Migration cannot be applied"
            exit 1
          fi

4.3 冲突解决方法 ​

方法1: 重新生成迁移(推荐)

bash
# 步骤1: 删除冲突的迁移文件
rm Migrations/20240115103000_ConflictingMigration.cs
rm Migrations/20240115103000_ConflictingMigration.Designer.cs

# 步骤2: 拉取最新代码
git pull origin main

# 步骤3: 基于最新快照重新创建迁移
dotnet ef migrations add AddNewFeature

# 步骤4: 验证新生成的迁移
dotnet ef migrations script

方法2: 合并迁移(复杂场景)

bash
# 场景: 两个开发者都修改了同一个实体

# 开发者 A 的迁移
public partial class AddProductWeight : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AddColumn<decimal>(
            name: "Weight",
            table: "Products",
            type: "decimal(18,2)",
            nullable: false,
            defaultValue: 0m);
    }
}

// 开发者 B 的迁移(也修改了 Products 表)
public partial class AddProductDimensions : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AddColumn<decimal>(
            name: "Length",
            table: "Products",
            type: "decimal(18,2)",
            nullable: false,
            defaultValue: 0m);
        
        migrationBuilder.AddColumn<decimal>(
            name: "Width",
            table: "Products",
            type: "decimal(18,2)",
            nullable: false,
            defaultValue: 0m);
    }
}

// ✅ 解决方案: 创建一个合并迁移
dotnet ef migrations add MergeProductSchemaChanges

public partial class MergeProductSchemaChanges : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // 合并所有变更到一个迁移
        migrationBuilder.AddColumn<decimal>(
            name: "Weight",
            table: "Products",
            type: "decimal(18,2)",
            nullable: false,
            defaultValue: 0m);
        
        migrationBuilder.AddColumn<decimal>(
            name: "Length",
            table: "Products",
            type: "decimal(18,2)",
            nullable: false,
            defaultValue: 0m);
        
        migrationBuilder.AddColumn<decimal>(
            name: "Width",
            table: "Products",
            type: "decimal(18,2)",
            nullable: false,
            defaultValue: 0m);
    }
}

// 删除旧的冲突迁移
rm Migrations/*_AddProductWeight.cs
rm Migrations/*_AddProductDimensions.cs

方法3: 重置迁移历史(仅开发环境)

bash
# ⚠️ 警告: 仅在开发环境使用!

# 步骤1: 删除所有迁移文件
rm -rf Migrations/*.cs

# 步骤2: 删除数据库
dotnet ef database drop --force

# 步骤3: 重新创建初始迁移
dotnet ef migrations add InitialCreate

# 步骤4: 创建数据库
dotnet ef database update

5. 迁移文件版本控制 ​

5.1 Git 中的迁移文件 ​

.gitignore 配置:

gitignore
# 不要忽略迁移文件!
# Migrations/*.cs 应该被追踪

# 但可以忽略临时文件
Migrations/*.tmp
Migrations/backup/

提交迁移的最佳实践:

bash
# 1. 单独提交迁移文件(不与业务逻辑混合)
git add Migrations/20240115103000_AddProductCategory.cs
git add Migrations/20240115103000_AddProductCategory.Designer.cs
git commit -m "Add migration for product category support"

# 2. 然后提交相关代码变更
git add src/Models/Product.cs
git add src/Services/ProductService.cs
git commit -m "Implement product category feature"

5.2 迁移文件标签化 ​

使用 Git 标签标记重要迁移:

bash
# 为生产环境的迁移打标签
git tag -a v1.0.0-migration-20240101 -m "Initial production migration"
git push origin v1.0.0-migration-20240101

# 为重大架构变更打标签
git tag -a v2.0.0-migration-multitenancy -m "Multi-tenancy migration"
git push origin v2.0.0-migration-multitenancy

# 查看迁移标签
git tag -l "*migration*"

5.3 迁移文档化 ​

维护 MIGRATIONS.md 文件:

markdown
# EF Core 迁移记录

## 迁移列表

| 迁移ID | 名称 | 日期 | 作者 | 描述 | 状态 |
|--------|------|------|------|------|------|
| 20240101120000 | InitialCreate | 2024-01-01 | 张三 | 初始数据库架构 | ✅ 生产 |
| 20240115103000 | AddProductCategory | 2024-01-15 | 李四 | 产品分类功能 | ✅ 生产 |
| 20240201140000 | AddOrderTracking | 2024-02-01 | 王五 | 订单追踪功能 | 🚧 测试 |
| 20240210090000 | MultiTenancy | 2024-02-10 | 张三 | 多租户支持 | 📝 开发 |

## 重要变更记录

### 20240210090000_MultiTenancy
**影响范围**: 所有表  
**停机时间**: 预计 30 分钟  
**回滚策略**: 从备份还原  

**变更详情**:
- 为所有表添加 TenantId 列
- 创建租户隔离索引
- 迁移历史数据到默认租户

**部署步骤**:
1. 备份数据库
2. 应用迁移: `dotnet ef database update`
3. 运行数据迁移脚本
4. 验证数据完整性
5. 更新应用配置

6. 迁移文件清理与维护 ​

6.1 何时清理迁移文件? ​

适合清理的场景:

✅ 开发阶段的原型迁移
✅ 测试用的临时迁移
✅ 合并冲突后的旧迁移
✅ 超过一定数量的累积迁移(可选)

不应该清理的场景:

❌ 已部署到生产环境的迁移
❌ 其他团队依赖的迁移
❌ 需要回滚支持的迁移

6.2 清理策略 ​

策略1: 压缩迁移(Squash Migrations)

bash
# 场景: 有 50 个开发迁移,想简化为一个

# 步骤1: 删除所有迁移文件
rm -rf Migrations/*.cs

# 步骤2: 基于当前数据库创建新迁移
dotnet ef migrations add ConsolidatedMigration

# 步骤3: 更新模型快照
# (自动完成)

# ⚠️ 警告: 这会丢失迁移历史!

策略2: 保留关键迁移,删除中间迁移

原始迁移历史:
├── 20240101_InitialCreate           ← 保留(起点)
├── 20240102_AddField1               ← 删除
├── 20240103_AddField2               ← 删除
├── 20240104_ModifyField3            ← 删除
└── 20240105_AddMajorFeature         ← 保留(里程碑)

压缩后:
├── 20240101_InitialCreate           ← 保留
└── 20240105_AddMajorFeature         ← 保留(包含所有变更)

策略3: 归档旧迁移

bash
# 创建归档目录
mkdir -p Migrations/Archive/2024-Q1

# 移动旧迁移(已部署到生产的除外)
mv Migrations/202401*.cs Migrations/Archive/2024-Q1/

# 创建归档说明
cat > Migrations/Archive/2024-Q1/README.md << EOF
# 迁移归档 - 2024年第一季度

这些迁移已部署到生产环境,不应再次应用。
仅用于参考和历史记录。

## 迁移列表
- 20240101120000_InitialCreate.cs
- 20240115103000_AddProductCategory.cs
EOF

6.3 重建迁移历史 ​

完全重置(仅新项目):

bash
# 步骤1: 删除数据库
dotnet ef database drop --force

# 步骤2: 删除所有迁移文件
rm -rf Migrations/*.cs

# 步骤3: 创建干净的初始迁移
dotnet ef migrations add InitialCreate

# 步骤4: 创建数据库
dotnet ef database update

# 步骤5: 提交到版本控制
git add Migrations/
git commit -m "Reset migration history"

7. 多环境迁移管理 ​

7.1 环境配置 ​

appsettings 配置:

json
// appsettings.Development.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=MyApp_Dev;"
  },
  "MigrationSettings": {
    "AutoApplyMigrations": true,
    "RequireConfirmation": false
  }
}

// appsettings.Staging.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=staging-server;Database=MyApp_Staging;"
  },
  "MigrationSettings": {
    "AutoApplyMigrations": false,
    "RequireConfirmation": true
  }
}

// appsettings.Production.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=prod-server;Database=MyApp_Production;"
  },
  "MigrationSettings": {
    "AutoApplyMigrations": false,
    "RequireConfirmation": true,
    "GenerateScriptOnly": true
  }
}

7.2 环境特定的迁移服务 ​

csharp
public class MigrationService
{
    private readonly AppDbContext _context;
    private readonly IConfiguration _configuration;
    private readonly ILogger<MigrationService> _logger;
    
    public MigrationService(
        AppDbContext context,
        IConfiguration configuration,
        ILogger<MigrationService> logger)
    {
        _context = context;
        _configuration = configuration;
        _logger = logger;
    }
    
    public async Task ApplyMigrationsAsync()
    {
        var migrationSettings = _configuration.GetSection("MigrationSettings");
        var autoApply = migrationSettings.GetValue<bool>("AutoApplyMigrations");
        
        if (!autoApply)
        {
            _logger.LogInformation("Auto migration is disabled");
            return;
        }
        
        try
        {
            // 获取待应用的迁移
            var pendingMigrations = await _context.Database.GetPendingMigrationsAsync();
            
            if (!pendingMigrations.Any())
            {
                _logger.LogInformation("No pending migrations");
                return;
            }
            
            _logger.LogInformation("Applying {Count} pending migrations: {@Migrations}",
                pendingMigrations.Count(),
                pendingMigrations);
            
            // 应用迁移
            await _context.Database.MigrateAsync();
            
            _logger.LogInformation("Migrations applied successfully");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to apply migrations");
            throw;
        }
    }
    
    public async Task GenerateMigrationScriptAsync(string outputPath)
    {
        var pendingMigrations = await _context.Database.GetPendingMigrationsAsync();
        
        if (!pendingMigrations.Any())
        {
            _logger.LogInformation("No pending migrations to script");
            return;
        }
        
        // 生成 SQL 脚本
        var script = await _context.Database.GenerateCreateScriptAsync();
        
        await File.WriteAllTextAsync(outputPath, script);
        
        _logger.LogInformation("Migration script generated: {Path}", outputPath);
    }
}

7.3 环境特定的迁移命令 ​

bash
# 开发环境 - 自动应用
dotnet ef database update --environment Development

# 预生产环境 - 生成脚本审查
dotnet ef migrations script --idempotent --output migration_staging.sql

# 生产环境 - 仅生成脚本,手动执行
dotnet ef migrations script --idempotent --output migration_production.sql

# 使用环境变量指定连接字符串
DOTNET_ConnectionStrings__DefaultConnection="Server=prod;..." \
  dotnet ef migrations script --idempotent

8. 自动化迁移部署 ​

8.1 CI/CD 流水线集成 ​

GitHub Actions 完整示例:

yaml
name: Deploy Database Migrations

on:
  push:
    branches: [main]
    paths:
      - 'Migrations/**'

jobs:
  validate-migrations:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup .NET
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: '8.0.x'
      
      - name: Install EF Tools
        run: dotnet tool install --global dotnet-ef
      
      - name: Build Project
        run: dotnet build --configuration Release
      
      - name: Validate Migrations
        run: |
          dotnet ef migrations bundle --force
          ./efbundle --connection "${{ secrets.TEST_DB_CONNECTION }}"
      
      - name: Run Tests
        run: dotnet test --no-build --verbosity normal

  deploy-staging:
    needs: validate-migrations
    runs-on: ubuntu-latest
    environment: staging
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup .NET
        uses: actions/setup-dotnet@v3
      
      - name: Install EF Tools
        run: dotnet tool install --global dotnet-ef
      
      - name: Generate Migration Script
        run: |
          dotnet ef migrations script --idempotent \
            --output migration_staging.sql \
            --connection "${{ secrets.STAGING_DB_CONNECTION }}"
      
      - name: Review Migration Script
        run: cat migration_staging.sql
      
      - name: Apply Migrations to Staging
        run: |
          dotnet ef database update \
            --connection "${{ secrets.STAGING_DB_CONNECTION }}"
      
      - name: Run Integration Tests
        run: dotnet test --filter "Category=Integration"

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup .NET
        uses: actions/setup-dotnet@v3
      
      - name: Install EF Tools
        run: dotnet tool install --global dotnet-ef
      
      - name: Generate Production Migration Script
        run: |
          dotnet ef migrations script --idempotent \
            --output migration_production.sql \
            --connection "${{ secrets.PROD_DB_CONNECTION }}"
      
      - name: Upload Migration Script
        uses: actions/upload-artifact@v3
        with:
          name: production-migration
          path: migration_production.sql
      
      - name: Create Backup Before Migration
        run: |
          # 调用备份 API 或脚本
          curl -X POST "${{ secrets.BACKUP_API_URL }}" \
            -H "Authorization: Bearer ${{ secrets.BACKUP_TOKEN }}"
      
      - name: Wait for Approval
        uses: trstringer/manual-approval@v1
        with:
          secret: ${{ github.TOKEN }}
          approvers: admin1,admin2
          minimum-approvals: 1
      
      - name: Apply Migrations to Production
        run: |
          dotnet ef database update \
            --connection "${{ secrets.PROD_DB_CONNECTION }}"
      
      - name: Verify Deployment
        run: |
          # 运行健康检查
          curl -f "${{ secrets.APP_URL }}/health" || exit 1
          
          # 检查迁移版本
          dotnet ef migrations list --connection "${{ secrets.PROD_DB_CONNECTION }}"

8.2 Azure DevOps 管道 ​

yaml
# azure-pipelines.yml
trigger:
  branches:
    include:
      - main
  paths:
    include:
      - Migrations/*

stages:
  - stage: Validate
    jobs:
      - job: TestMigrations
        pool:
          vmImage: 'ubuntu-latest'
        
        steps:
          - task: DotNetCoreCLI@2
            displayName: 'Install EF Tools'
            inputs:
              command: 'custom'
              custom: 'tool'
              arguments: 'install --global dotnet-ef'
          
          - task: DotNetCoreCLI@2
            displayName: 'Validate Migrations'
            inputs:
              command: 'custom'
              custom: 'ef'
              arguments: 'database update --connection $(TestDbConnection)'

  - stage: DeployStaging
    dependsOn: Validate
    jobs:
      - deployment: DeployToStaging
        environment: staging
        strategy:
          runOnce:
            deploy:
              steps:
                - script: |
                    dotnet ef database update --connection $(StagingDbConnection)
                  displayName: 'Apply Migrations'

  - stage: DeployProduction
    dependsOn: DeployStaging
    jobs:
      - deployment: DeployToProduction
        environment: production
        strategy:
          runOnce:
            deploy:
              steps:
                - script: |
                    dotnet ef migrations script --idempotent --output migration.sql
                  displayName: 'Generate Script'
                
                - script: |
                    sqlcmd -S $(ProdServer) -d $(ProdDatabase) -i migration.sql
                  displayName: 'Execute Migration'

8.3 Docker 容器中的迁移 ​

Dockerfile 集成:

dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet build -c Release -o /app/build

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/build .

# 安装 EF Tools
RUN dotnet tool install --global dotnet-ef
ENV PATH="${PATH}:/root/.dotnet/tools"

# 复制迁移文件
COPY Migrations ./Migrations

# 入口点脚本
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh

ENTRYPOINT ["./entrypoint.sh"]

entrypoint.sh:

bash
#!/bin/bash
set -e

echo "Waiting for database to be ready..."
until dotnet ef database info --connection "$CONNECTION_STRING" > /dev/null 2>&1; do
  sleep 2
done

echo "Applying migrations..."
dotnet ef database update --connection "$CONNECTION_STRING"

if [ $? -eq 0 ]; then
  echo "Migrations applied successfully"
else
  echo "Failed to apply migrations"
  exit 1
fi

echo "Starting application..."
exec dotnet MyApp.dll

总结 ​

迁移文件管理是 EF Core 项目长期健康的关键:

核心原则 ​

✅ 命名规范 - 使用清晰的动词+名词模式
✅ 版本控制 - 所有迁移文件都应纳入 Git 管理
✅ 冲突预防 - 频繁同步,及时审查
✅ 环境隔离 - 不同环境采用不同策略
✅ 自动化部署 - CI/CD 集成,减少人为错误

最佳实践 ​

  1. 开发环境 - 允许自动迁移,快速迭代
  2. 测试环境 - 生成脚本审查后应用
  3. 生产环境 - 仅生成脚本,DBA 手动执行
  4. 迁移压缩 - 定期清理开发迁移,保留里程碑
  5. 文档化 - 维护 MIGRATIONS.md 记录所有变更

工具链 ​

  • dotnet ef CLI - 核心工具
  • CI/CD 集成 - GitHub Actions, Azure DevOps
  • Docker 支持 - 容器化部署
  • 监控告警 - 迁移失败自动通知

掌握这些迁移文件管理技巧,你可以高效地协作开发并保证数据库架构的一致性!

基于 MIT 许可发布