首页 > 解决方案 > 如何将同一列添加到 EF Core 中的所有实体?

问题描述

想象一下,我想向我的所有实体添加一个 IsDeleted 列或一些审计列。我可以创建一个基类,我的所有实体都将从该基类继承,这将解决我的问题,但是我无法指定创建列的顺序,因此我将在我的实体字段之前得到所有审计字段,这是我不想要的。我希望他们排在桌子的最后。

在实体框架的标准版本中,我们可以通过使用指定列顺序的注释来做到这一点。但是,目前 EF 核心不存在这样的事情。

我可以使用 OnModelCreating() 方法上的 fluent api 来完成它,问题是我只知道如何为我的每个实体单独执行它,这意味着我必须为我拥有的每个实体编写相同的代码。

有什么方法可以为我的所有实体通用地做到这一点?某种 for 循环遍历在我的 dbcontext 上的 DbSets 中注册的所有实体?

标签: c#entity-framework.net-coreentity-framework-core

解决方案


您的问题标题是关于向多个实体添加相同的属性。但是,您实际上知道如何实现这一点(使用基本类型),而您的实际问题是如何确保这些属性在生成的表的列中排在最后。

尽管现在列顺序并不重要,但我将展示一个您可能比基本类型更喜欢的替代方法,并将公共属性放在表的末尾。它利用阴影属性

影子属性是未在 .NET 实体类中定义但在 EF Core 模型中为该实体类型定义的属性。

大多数时候,审计属性在应用程序中不需要太多可见性,所以我认为影子属性正是您所需要的。这是一个例子:

我有两节课:

public class Planet
{
    public Planet()
    {
        Moons = new HashSet<Moon>();
    }
    public int ID { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Moon> Moons { get; set; }
}

public class Moon
{
    public int ID { get; set; }
    public int PlanetID { get; set; }
    public string Name { get; set; }
    public Planet Planet { get; set; }
}

如您所见:他们没有审计属性,他们是非常卑鄙和精干的 POCO。(顺便说一句,为方便起见,我IsDeleted将其与“审计属性”混为一谈,尽管它不是一个,并且可能需要另一种方法)。

也许这就是这里的主要信息:类模型不受审计问题的困扰(单一职责),这都是 EF 的业务。

审计属性被添加为影子属性。由于我们想为每个实体执行此操作,因此我们定义了一个 base IEntityTypeConfiguration

public abstract class BaseEntityTypeConfiguration<T> : IEntityTypeConfiguration<T>
    where T : class
{
    public virtual void Configure(EntityTypeBuilder<T> builder)
    {
        builder.Property<bool>("IsDeleted")
            .IsRequired()
            .HasDefaultValue(false);
        builder.Property<DateTime>("InsertDateTime")
            .IsRequired()
            .HasDefaultValueSql("SYSDATETIME()")
            .ValueGeneratedOnAdd();
        builder.Property<DateTime>("UpdateDateTime")
            .IsRequired()
            .HasDefaultValueSql("SYSDATETIME()")
            .ValueGeneratedOnAdd();
    }
}

具体配置派生自这个基类:

public class PlanetConfig : BaseEntityTypeConfiguration<Planet>
{
    public override void Configure(EntityTypeBuilder<Planet> builder)
    {
        builder.Property(p => p.ID).ValueGeneratedOnAdd();
        // Follows the default convention but added to make a difference :)
        builder.HasMany(p => p.Moons)
            .WithOne(m => m.Planet)
            .IsRequired()
            .HasForeignKey(m => m.PlanetID);
        base.Configure(builder);
    }
}

public class MoonConfig : BaseEntityTypeConfiguration<Moon>
{
    public override void Configure(EntityTypeBuilder<Moon> builder)
    {
        builder.Property(p => p.ID).ValueGeneratedOnAdd();
        base.Configure(builder);
    }
}

这些应该添加到上下文的模型中OnModelCreating

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfiguration(new PlanetConfig());
    modelBuilder.ApplyConfiguration(new MoonConfig());
}

这将生成具有列的数据库表InsertDateTimeIsDeleted并且UpdateDateTime在最后(独立于何时base.Configure(builder)调用,顺便说一句),尽管按顺序(字母顺序)。我想这已经足够接近了。

为了使图片完整,以下是如何在SaveChanges覆盖中完全自动设置值:

public override int SaveChanges()
{
    foreach(var entry in this.ChangeTracker.Entries()
        .Where(e => e.Properties.Any(p => p.Metadata.Name == "UpdateDateTime")
                 && e.State != Microsoft.EntityFrameworkCore.EntityState.Added))
    {
        entry.Property("UpdateDateTime").CurrentValue = DateTime.Now;
    }
    return base.SaveChanges();
}

小细节:我确保当插入实体时,数据库默认设置两个字段(见上文:ValueGeneratedOnAdd(),因此排除添加的实体),因此不会因客户端时钟略微偏离而造成混淆差异。我认为更新总是会在以后。

并设置IsDeleted您可以将此方法添加到上下文中:

public void MarkForDelete<T>(T entity)
    where T : class
{
    var entry = this.Entry(entity);
    // TODO: check entry.State
    if(entry.Properties.Any(p => p.Metadata.Name == "IsDeleted"))
    {
        entry.Property("IsDeleted").CurrentValue = true;
    }
    else
    {
        entry.State = Microsoft.EntityFrameworkCore.EntityState.Deleted;
    }
}

...或转向其中一种建议的机制以转换EntityState.DeletedIsDeleted = true.


推荐阅读