如何将同一列添加到 EF Core 中的所有实体?

Posted

技术标签:

【中文标题】如何将同一列添加到 EF Core 中的所有实体?【英文标题】:How to add the same column to all entities in EF Core? 【发布时间】:2018-08-25 18:27:02 【问题描述】:

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

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

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

有什么方法可以通用地为我的所有实体执行此操作?某种 for 循环遍历我的 dbcontext 上 DbSets 中注册的所有实体?

【问题讨论】:

您为什么要关心表格中的哪些订单字段? 我也有同样的问题,感觉更像是一个原则问题(我确实理解)但现在我不会打扰。 我也很疑惑你为什么关心顺序,但是本着帮助的精神,你不能只做一个通用方法然后添加列吗?还是我错过了什么? @MortenBork,不错。是的,我可以,但是我必须为我的每个实体调用该方法。如果我有 30 个实体,我将不得不调用它 30 次。我一直在寻找更光滑的东西。对于我的实体来说,有点像某种全局配置。 如果您真的那么热衷于订单,我相信使用流畅的 API 和反射是可能的。您需要将属性循环到基类的最低级别,然后设置列顺序。 【参考方案1】:

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

虽然现在列顺序并不重要,但我将展示一个您可能更喜欢的替代方法,而不是基本类型,并且将公共属性放在表格的末尾。它利用shadow properties:

影子属性是未在 .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 的全部业务。

审计属性被添加为影子属性。由于我们想为每个实体执行此操作,因此我们定义了一个基础 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());

这将生成具有列 InsertDateTimeIsDeletedUpdateDateTime 的数据库表(与何时调用 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.Deleted 转换为IsDeleted = true

【讨论】:

谢谢,这似乎是解决此问题的最佳方式。非常干净的方法,每个问题都清楚地分开。 虽然现在列顺序已经不重要了你的意思是在 EF 类中还是在数据库中? @JohnyL 两者。例如,请参阅this。在旧版本的 Sql Server(我相信是 Oracle)中,BLOB 列的位置确实很重要。这可能是复制中的一个问题,但我认为这不是 OP 所考虑的。 那么这是否意味着你需要为每个实体创建一个新的配置文件,而不是大约相同数量的代码@GertArnold @csharpdudeni77 与...相比,数量相同?【参考方案2】:

您始终可以为模型生成初始迁移并手动重新排列迁移中的列顺序。

这是 EF Core 中对显式列排序的开放问题跟踪支持:https://github.com/aspnet/EntityFrameworkCore/issues/10059

另请参阅有关使用影子属性和查询过滤器进行软删除的问题和答案。 EF Core: Soft delete with shadow properties and query filters

【讨论】:

以上是关于如何将同一列添加到 EF Core 中的所有实体?的主要内容,如果未能解决你的问题,请参考以下文章

EF Core - 无法添加具有相同相关对象的实体

EF Core 过滤掉多对多关系中的重复实体

EF Core 在对现有查询添加查询时附加所有实体 [重复]

EF Core中怎么实现自动更新实体的属性值到数据库

EF Core 在映射到 Select 中的对象时查询 SQL 中的所有列

ef core 中的 TPH 模式用于不同实体之间的共享表