简体   繁体   中英

How to add the same column to all entities in EF Core?

Imagine that I want to add an IsDeleted colum or some auditing columns to all of my entities. I could create a base class from which all of my entities will inherit and this will solve my problem, however I cannot specify the order in which the column will be created so I will end up with all the auditing fields before the fields of my entity, which I do not want. I want them to be at the end of the table.

In the standard version of entity framework we can do this by using annotations that specify the order of the columns. However, such a thing does not exist for EF core at the moment.

I could do it with the fluent api on the OnModelCreating() method, the problem is that I only know how to do it individually for each of my entities, which means I would have to write the same code for every entity I have.

Is there any way I can do it generically for all of my entities? Some sort of for loop that iterates through all the entities registered in the DbSets on my dbcontext?

Your question title is about adding the same properties to multiple entities. However, you actually know how to achieve this (use a base type) and your actual question is how to ensure that these properties come last in the generated tables' columns.

Although column order shouldn't really matter nowadays, I'll show an alternative that you may like better than a base type and also positions the common properties at the end of the table. It makes use of shadow properties :

Shadow properties are properties that are not defined in your .NET entity class but are defined for that entity type in the EF Core model.

Most of the times, auditing properties don't need much visibility in the application, so I think shadow properties is exactly what you need. Here's an example:

I have two classes:

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; }
}

As you see: they don't have auditing properties, they're nicely mean and lean POCOs. (By the way, for convenience I lump IsDeleted together with "audit properties", although it isn't one and it may require another approach).

And maybe that's the main message here: the class model isn't bothered with auditing concerns ( single responsibility ), it's all EF's business.

The audit properties are added as shadow properties. Since we want to do that for each entity we define a 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();
    }
}

The concrete configurations are derived from this base class:

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);
    }
}

These should be added to the context's model in OnModelCreating :

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

This will generate database tables having columns InsertDateTime , IsDeleted and UpdateDateTime at the end (independent of when base.Configure(builder) is called, BTW), albeit in that order (alphabetical). I guess that's close enough.

To make the picture complete, here's how to set the values fully automatically in a SaveChanges override:

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();
}

Small detail: I make sure that when an entity is inserted the database defaults set both fields (see above: ValueGeneratedOnAdd() , and hence the exclusion of added entities) so there won't be confusing differences caused by client clocks being slightly off. I assume that updating will always be well later.

And to set IsDeleted you could add this method to the context:

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;
    }
}

...or turn to one of the proposed mechanisms out there to convert EntityState.Deleted to IsDeleted = true .

You can always generate an initial migration for the model and manually rearrange the column order in the Migration.

Here is the open issue tracking support for explicit column ordering in EF Core: https://github.com/aspnet/EntityFrameworkCore/issues/10059

Also see this question and answer on using Shadow Properties and Query Filters for soft deletes. EF Core: Soft delete with shadow properties and query filters

Explanation with an easy example.

We can use Shadow properties if we want to use the same column in multiple/all tables. You can configure shadow properties on all entities at once, rather than configuring them manually for all.

For example, we can configure CreatedDate and UpdatedDate on all the entities at once, as shown below.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var allEntities = modelBuilder.Model.GetEntityTypes();

    foreach (var entity in allEntities)
    {
        entity.AddProperty("CreatedDate",typeof(DateTime));
        entity.AddProperty("UpdatedDate",typeof(DateTime));
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM