PAYATHON 2026

如何让 EF core 10 中继承的审计属性生成在表的最后几列?

技术老齐

可以保留 AuditableEntity : IAuditable 的继承结构,无需在每个实体中重复定义审计属性。EF Core 10 没有内置规则,能通过 [Column(Order = ...)] 将某些属性单独标记为“始终排在最后”。

如果每个实体各自映射一张表,可以在 OnModelCreating 中统一排序:先排普通属性,再排审计属性,然后自动生成列顺序编号,无需在实体类中逐个填写。

为什么只给审计属性设置大编号无效?

EF Core 10 生成建表操作时,会先将设置了列顺序的列按编号排序,再追加没有设置顺序的列。因此,即使将审计属性设为 Order = 10000,它仍会排在未设置顺序的业务属性之前。EF Core 10 排序实现

编号大小只影响已设置顺序的列,无法表示“放在所有未设置顺序的列之后”。未设置顺序时,EF Core 也有自己的迁移排序逻辑,不能仅凭反射返回的属性顺序判断最终结果。

保留继承结构,集中生成列顺序

假设审计属性只有 CreatedAt 和 UpdatedAt:

public interface IAuditable
{
    DateTime CreatedAt { get; set; }
    DateTime? UpdatedAt { get; set; }
}

public abstract class AuditableEntity : IAuditable
{
    public DateTime CreatedAt { get; set; }
    public DateTime? UpdatedAt { get; set; }
}

public class Order : AuditableEntity
{
    public int Id { get; set; }
    public string Number { get; set; } = string.Empty;
}

在现有 DbContext 的 OnModelCreating 中,将排序逻辑放在其他实体配置之后:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    // 在这里完成现有的实体配置。
    modelBuilder.Entity<Order>();

    // 审计属性的名称,以及它们在审计列内部的顺序。
    var auditOrder = new Dictionary<string, int>(StringComparer.Ordinal)
    {
        [nameof(AuditableEntity.CreatedAt)] = 0,
        [nameof(AuditableEntity.UpdatedAt)] = 1
    };

    foreach (var entityType in modelBuilder.Model.GetEntityTypes())
    {
        if (!typeof(AuditableEntity).IsAssignableFrom(entityType.ClrType))
        {
            continue;
        }

        var properties = entityType.GetProperties().ToArray();

        var businessProperties = properties
            .Where(p => !auditOrder.ContainsKey(p.Name))
            .OrderBy(p => p.IsPrimaryKey() ? 0 : 1)
            .ThenBy(p => p.Name, StringComparer.Ordinal);

        var auditProperties = properties
            .Where(p => auditOrder.ContainsKey(p.Name))
            .OrderBy(p => auditOrder[p.Name]);

        var columnOrder = 0;

        foreach (var property in businessProperties.Concat(auditProperties))
        {
            property.SetColumnOrder(columnOrder++);
        }
    }
}

SetColumnOrder 是关系型元数据 API,用于设置属性所映射的数据库列的顺序。API 文档

排序规则如下:

  • 主键属性排在普通属性前面。
  • 其他业务属性按名称排序,保证结果稳定。
  • 审计属性放在最后,内部按 auditOrder 中的编号排序。

示例中 Order 的目标列顺序为:

Id, Number, CreatedAt, UpdatedAt

以后新增业务属性时,排序逻辑会自动将其纳入;新增审计属性时,只需在基类和 auditOrder 中补充定义。

各属性仍会设置列顺序元数据,编号由这段集中配置自动生成。维护时,只需指定哪些属性属于审计列。

生成 migration 后检查什么?

完成配置后,生成包含新表的 migration:

dotnet ef migrations add AddAuditableTables
dotnet ef migrations script

检查 migration 中 CreateTable 的列定义,以及生成的 SQL 中 CREATE TABLE 的列顺序。

如果对应的 migration 已经生成,修改模型配置不会自动改写它。如果最后一个 migration 尚未应用,且允许重新生成,可以先移除,再重新生成。

使用时的边界

上述示例适用于每个业务实体独立映射一张表、AuditableEntity 仅作为 CLR 公共基类的情况。不要为了复用属性,额外将它注册为 EF 实体继承层次的根类型。

如果采用 TPH、TPT、TPC,或存在表拆分、映射到同一张表的 owned entity、复杂类型属性,就需要按实际表映射统一处理。在这些场景下,列与实体属性之间的关系更复杂,示例中对 GetProperties() 的排序无法直接保证整张表的最终列顺序。

这段代码会覆盖目标实体上已有的列顺序配置。如果业务属性有特定的布局要求,应调整 businessProperties 的排序规则。由于审计属性按名称识别,也应避免在派生类中用 new 隐藏同名属性。

列顺序配置主要用于新建表。对于已有表,修改 HasColumnOrder 或 SetColumnOrder 通常无法重新排列数据库中的现有列;后续新增业务列时,也不能保证它会自动插入审计列之前。列顺序文档

备注:内容仅供参考。