Entity Framework Core (EF Core) 是一个轻量级的、可扩展的ORM(对象关系映射)框架,用于.NET平台。在使用EF Core进行数据库迁移时,EF Core默认会在数据库中创建一个名为 __EFMigrationsHistory 的表,用于记录迁移历史。本文将介绍如何自定义这个迁移历史表的名称、架构、列名,以及如何添加带有默认值的列和必填列。
默认情况下,迁移历史表的名称是 __EFMigrationsHistory,架构是 dbo。如果需要自定义迁移表的名称和架构,可以通过DbContext的配置来实现。以下是一个示例代码,展示了如何将迁移表的名称更改为 __Migrations,并将架构更改为 track。
public class AppDb : DbContext
{
    public DbSet<Process> Process { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var config = new ConfigurationBuilder()
            .AddJsonFile(Path.Combine(AppContext.BaseDirectory, "appsettings.json"), optional: false, reloadOnChange: true)
            .Build();
        optionsBuilder.UseSqlServer(config.GetConnectionString("DatabaseConnection"), d => { d.MigrationsHistoryTable("__Migrations", "track"); });
    }
}
    
在这个示例中,通过DbContext的OnConfiguring方法,使用DbContextOptionsBuilder的MigrationsHistoryTable方法,指定了迁移历史表的名称和架构。
迁移历史表默认包含两个列:MigrationId和ProductVersion。如果需要自定义这些列的名称,可以通过自定义IHistoryRepository服务来实现。以下是一个示例代码,展示了如何将MigrationId列的名称更改为Id,将ProductVersion列的名称更改为Version。
public class AppDb : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var config = new ConfigurationBuilder()
            .AddJsonFile(Path.Combine(AppContext.BaseDirectory, "appsettings.json"), optional: false, reloadOnChange: true)
            .Build();
        optionsBuilder.UseSqlServer(config.GetConnectionString("DatabaseConnection"))
            .ReplaceService<IHistoryRepository, HistoryRepository>();
    }
}
internal class HistoryRepository : SqlServerHistoryRepository
{
    public HistoryRepository(HistoryRepositoryDependencies dependencies) : base(dependencies) { }
    protected override void ConfigureTable(EntityTypeBuilder<HistoryRow> history)
    {
        base.ConfigureTable(history);
        history.Property(h => h.MigrationId).HasColumnName("Id");
        history.Property(h => h.ProductVersion).HasColumnName("Version");
    }
}
    
在这个示例中,首先通过DbContext的OnConfiguring方法,使用DbContextOptionsBuilder的ReplaceService方法,替换了默认的IHistoryRepository服务。然后在自定义的HistoryRepository类中,通过重写ConfigureTable方法,使用EntityTypeBuilder的Property方法,指定了列的新名称。
有时候,可能需要在迁移历史表中添加一个带有默认值的列。以下是一个示例代码,展示了如何在迁移历史表中添加一个名为AppliedAtUtc的列,并设置其默认值为当前的UTC日期和时间。
public partial class Init : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.Sql("ALTER TABLE [track].[__Migrations] ADD AppliedAtUtc DATETIME NULL;");
        migrationBuilder.Sql("ALTER TABLE [track].[__Migrations] ADD CONSTRAINT DF__Migrations_AppliedAtUtc DEFAULT GETUTCDATE() FOR [AppliedAtUtc];");
    }
    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropCheckConstraint("DF__Migrations_AppliedAtUtc", "__Migrations", "track");
        migrationBuilder.DropColumn("AppliedAtUtc", "__Migrations", "track");
    }
}
    
在这个示例中,首先通过Sql方法添加了一个新的列AppliedAtUtc,并设置其数据类型为DATETIME,允许空值。然后通过Sql方法添加了一个名为DF__Migrations_AppliedAtUtc的约束,将AppliedAtUtc列的默认值设置为当前的UTC日期和时间。在Down方法中,通过DropCheckConstraint方法和DropColumn方法,分别移除了约束和列。
有时候,可能需要在迁移历史表中添加一个必填列。以下是一个示例代码,展示了如何在迁移历史表中添加一个名为ProjectName的必填列。
public class AppDb : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var config = new ConfigurationBuilder()
            .AddJsonFile(Path.Combine(AppContext.BaseDirectory, "appsettings.json"), optional: false, reloadOnChange: true)
            .Build();
        optionsBuilder.UseSqlServer(config.GetConnectionString("DatabaseConnection"))
            .ReplaceService<IHistoryRepository, HistoryRepository>();
    }
}
public class ContextConstants
{
    public const string ProjectName = "Console";
}
internal class HistoryRepository : SqlServerHistoryRepository
{
    public const string CustomColumnName = "ProjectName";
    public HistoryRepository(HistoryRepositoryDependencies dependencies) : base(dependencies) { }
    protected override void ConfigureTable(EntityTypeBuilder<HistoryRow> history)
    {
        base.ConfigureTable(history);
        history.Property<string>(CustomColumnName).HasMaxLength(300).IsRequired();
    }
    public override string GetInsertScript(HistoryRow row)
    {
        var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string));
        return new StringBuilder()
            .Append("INSERT INTO ")
            .Append(SqlGenerationHelper.DelimitIdentifier(TableName, TableSchema))
            .Append("(")
            .Append(SqlGenerationHelper.DelimitIdentifier(MigrationIdColumnName))
            .Append(", ")
            .Append(SqlGenerationHelper.DelimitIdentifier(ProductVersionColumnName))
            .Append(", ")
            .Append(SqlGenerationHelper.DelimitIdentifier(CustomColumnName))
            .Append(") ")
            .Append("VALUES (")
            .Append(stringTypeMapping.GenerateSqlLiteral(row.MigrationId))
            .Append(", ")
            .Append(stringTypeMapping.GenerateSqlLiteral(row.ProductVersion))
            .Append(", ")
            .Append(stringTypeMapping.GenerateSqlLiteral(ContextConstants.ProjectName))
            .Append(")")
            .AppendLine(SqlGenerationHelper.StatementTerminator)
            .ToString();
    }
}
    
在这个示例中,首先通过DbContext的OnConfiguring方法,使用DbContextOptionsBuilder的ReplaceService方法,替换了默认的IHistoryRepository服务。然后在自定义的HistoryRepository类中,通过重写ConfigureTable方法,使用EntityTypeBuilder的Property方法,指定了新列的名称、最大长度和是否必填。然后在GetInsertScript方法中,自定义了插入语句,将新列的值设置为ContextConstants中的ProjectName常量。
本文的代码示例基于Visual Studio 2022和EF Core6(也测试了EF Core 5)。测试时需要将Db.Custom项目设置为启动项目。在appsettings.json文件中,可以找到目标数据库连接字符串。