6

In my EF core project I have a few entities that inherit from a base class DBEntity:

public abstract class DBEntity
{
    public int Id { get; set; }

    public DateTime CreatedOn { get; set; }

    public DateTime UpdatedOn { get; set; }

    public EntityStatus EntityStatus { get; set; }
}

I want to set some specific properties with default value for every entity that inherits from DBEntity. Currently I'm using this code:

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {

        var entities = modelBuilder.Model
            .GetEntityTypes()
            .Where(w => w.ClrType.IsSubclassOf(typeof(DBEntity)))
            .Select(p => modelBuilder.Entity(p.ClrType));

        foreach (var entity in entities)
        {

            entity
                .Property("CreatedOn")
                .HasDefaultValueSql("GETDATE()");

            entity
                .Property("UpdatedOn")
                .HasComputedColumnSql("GETDATE()");

            entity
                .Property("EntityStatus")
                .HasDefaultValue(EntityStatus.Created);

        }
    }

This works fine but I don't want to specify property names using string constants (bad for refactoring etc.).

Is there a way to loop through entities that inherit from DBEntity and use property expression to configure default values like this:

foreach (var entity in entities)
{
    entity
      .Property(k => k.CreatedOn)
      .HasDefaultValueSql("GETDATE()");

// ....

}

Important constraint: I cannot directly call modelBuilder.Entity().Property(k => k.CreatedOn) because it will mess up all tables.

iPath ツ
  • 2,468
  • 20
  • 31

2 Answers2

9

As pointed out in the comments, you can simply replace the string constants with nameof(DBEntity.CreatedOn) etc.

But if you want to work with typed accessors, you can move the base entity configuration code to a generic method (with the generic argument being the actual entity type) and call it via reflection.

For instance, add the following to your DbContext derived class:

static void ConfigureDBEntity<TEntity>(ModelBuilder modelBuilder)
    where TEntity : DBEntity
{
    var entity = modelBuilder.Entity<TEntity>();

    entity
        .Property(e => e.CreatedOn)
        .HasDefaultValueSql("GETDATE()");

    entity
        .Property(e => e.UpdatedOn)
        .HasComputedColumnSql("GETDATE()");

    entity
        .Property(e => e.EntityStatus)
        .HasDefaultValue(EntityStatus.Created);

}

and then use something like this:

var entityTypes = modelBuilder.Model
        .GetEntityTypes()
        .Where(t => t.ClrType.IsSubclassOf(typeof(DBEntity)));

var configureMethod = GetType().GetTypeInfo().DeclaredMethods.Single(m => m.Name == nameof(ConfigureDBEntity));
var args = new object[] { modelBuilder };
foreach (var entityType in entityTypes)
    configureMethod.MakeGenericMethod(entityType.ClrType).Invoke(null, args);
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • I am trying to implement your suggested solution but my base entity takes a generic type argument (BaseEntity). So when I pass the typeof(BaseEntity) to IsSubclassOf(), it gives an error: `Error CS0305 Using the generic type 'BaseEntity' requires 1 type arguments`. I am not sure how to pass the type of a class with type arguments in this case. Any help will be much appreciated. Thanks. – Suhaib Syed Apr 27 '20 at 06:48
  • 1
    @SuhaibSyed Here https://stackoverflow.com/questions/53275567/how-to-apply-common-configuration-to-all-entities-in-ef-core/53326519#53326519 is an example how you can do that - `IsBaseEntity` method, you can eliminate `out T` parameter if you don`t need it. – Ivan Stoev Apr 27 '20 at 07:09
3

What about, in your context, adding something like this

    public override int SaveChanges()
    {
        AddTimestamps();
        return base.SaveChanges();
    }

    public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
    {
        AddTimestamps();
        return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
    }

    private void AddTimestamps()
    {
        var entities = ChangeTracker.Entries()
            .Where(x => (x.Entity is DBEntity) && (x.State == EntityState.Added || x.State == EntityState.Modified));

        var now = DateTime.UtcNow; // current datetime

        foreach (var entity in entities)
        {
            if (entity.State == EntityState.Added)
            {
                ((DBEntity)entity.Entity).CreatedOn = now;
                ((DBEntity)entity.Entity).EntityStatus = EntityStatus.Created;
            }
            ((DBEntity)entity.Entity).UpdatedOn= now;
        }
    }

You have the drawback to not enforcing those default values at db level (at this point you are db agnostic) but stil able to refactor

nicecatch
  • 1,687
  • 2
  • 21
  • 38