25

In EF Core 2.0, we have the ability to derive from IEntityTypeConfiguration for cleaner Fluent API mappings (source).

How can I extend this pattern to utilize a base entity? In the example below, how can I have a BaseEntityConfiguration to reduce duplication in LanguageConfiguration and MaintainerConfiguration, modifying properties that are in the BaseEntity only in the BaseEntityConfiguration? What would such a BaseEntityConfiguration look like; and how would it be used, if at all, in OnModelCreating()? See the TODOs in-code near the end of the example.

Example:

public abstract class BaseEntity
{
    public long Id { get; set; }
    public DateTime CreatedDateUtc { get; set; }
    public DateTime? ModifiedDateUtc { get; set; }
}

public class Language : BaseEntity
{
    public string Iso6392 { get; set; }
    public string LocalName { get; set; }
    public string Name { get; set; }
}

public class Maintainer : BaseEntity
{
    public string Email { get; set; }
    public string Name { get; set; }
}

public class FilterListsDbContext : DbContext
{
    public FilterListsDbContext(DbContextOptions options) : base(options)
    {
    }

    public DbSet<Language> Languages { get; set; }
    public DbSet<Maintainer> Maintainers { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        //TODO: Possibly add something like BaseEntityConfiguration?
        modelBuilder.ApplyConfiguration(new LanguageConfiguration());
        modelBuilder.ApplyConfiguration(new MaintainerConfiguration());
    }
}

public class LanguageConfiguration : IEntityTypeConfiguration<Language>
{
    public void Configure(EntityTypeBuilder<Language> entityTypeBuilder)
    {
        //TODO: Move this to something like BaseEntityConfiguration?
        entityTypeBuilder.Property(b => b.CreatedDateUtc).HasDefaultValueSql("CURRENT_TIMESTAMP");
    }
}

public class MaintainerConfiguration : IEntityTypeConfiguration<Maintainer>
{
    public void Configure(EntityTypeBuilder<Maintainer> entityTypeBuilder)
    {
        //TODO: Move this to something like BaseEntityConfiguration?
        entityTypeBuilder.Property(b => b.CreatedDateUtc).HasDefaultValueSql("CURRENT_TIMESTAMP");
    }
}
Collin Barrett
  • 2,441
  • 5
  • 32
  • 53

5 Answers5

44

Something like this could work (untested)?

public abstract class BaseEntityTypeConfiguration<TBase> : IEntityTypeConfiguration<TBase>
    where TBase : BaseEntity
{
    public virtual void Configure(EntityTypeBuilder<TBase> entityTypeBuilder)
    {
        //Base Configuration
    }
}

public class MaintainerConfiguration : BaseEntityTypeConfiguration<Maintainer>
{
    public override void Configure(EntityTypeBuilder<Maintainer> entityTypeBuilder)
    {
        entityTypeBuilder.Property(b => b.CreatedDateUtc).HasDefaultValueSql("CURRENT_TIMESTAMP");
        base.Configure(entityTypeBuilder);
    }
}
SpruceMoose
  • 9,737
  • 4
  • 39
  • 53
  • Thanks, I'll give it a shot. What would you suggest my OnModelCreating() would look like with this solution? – Collin Barrett Oct 27 '17 at 15:42
  • Should be the same. Just need to ensure to call base.Configure in each Configure() override. – SpruceMoose Oct 27 '17 at 15:56
  • @CalC... does "b => b.CreatedDateUtc).HasDefaultValueSql("CURRENT_TIMESTAMP")" do the same thing as setting the property value in the class constructor? – dinotom Apr 12 '18 at 12:47
  • 1
    @dinotom - it has a similar effect, however, it should be noted that when using CURRENT_TIMESTAMP the time will be derived from the operating system of the computer on which the *database instance* is running and will not take into account a database time zone offset - see [CURRENT_TIMESTAMP](https://learn.microsoft.com/en-us/sql/t-sql/functions/current-timestamp-transact-sql) – SpruceMoose Apr 12 '18 at 12:58
  • 1
    Why make `BaseEntityTypeConfiguration` abstract? – Junaid Jan 06 '22 at 15:09
9

There is another way to solve the problem, and that is to use Template Method Design Pattern. Like this:

public abstract class BaseEntityTypeConfiguration<TBase> : IEntityTypeConfiguration<TBase>
    where TBase : BaseEntity
{
    public void Configure(EntityTypeBuilder<TBase> entityTypeBuilder)
    {
        //Base Configuration

        ConfigureOtherProperties(builder);
    }

    public abstract void ConfigureOtherProperties(EntityTypeBuilder<TEntity> builder);
}

public class MaintainerConfiguration : BaseEntityTypeConfiguration<Maintainer>
{
    public override void ConfigureOtherProperties(EntityTypeBuilder<Maintainer> entityTypeBuilder)
    {
        entityTypeBuilder.Property(b => b.CreatedDateUtc).HasDefaultValueSql("CURRENT_TIMESTAMP");        
    }
}

With this way you don't need to write any single line in child configuration.

Armin Kianian
  • 143
  • 2
  • 10
  • that method does not work if we have multiple inheritances: BaseEntity, IdEntity, NamedEntity etc – serge Jan 31 '21 at 22:41
  • 1
    @serge I guess you mean a deeper inheritance tree (not multiple inheritance). For that you could just reply the pattern and call another abstract/virtual method in the MaintainerConfiguration. – Wouter Jun 22 '21 at 19:25
  • @Wouter I spent a couple hours trying to get 2 levels of inheritance working (IdEntity > AuditableEntity > ConcreteEntity), and it doesn't seem possible. I would always end up with my migrations only grabbing configuration from final base, ignoring the ones in the middle. I think the problem is, you can have only one IEntityTypeConfiguration in your inheritance chain (if you try to apply it in the middle configuration, the base.Configure will complain about mismatching types). – Eternal21 Jul 21 '21 at 03:01
  • @serge Have you been able to figure out the 'multiple inheritance'? – Eternal21 Jul 21 '21 at 03:03
  • 1
    @Eternal21 Because you override and don't call the intermediate configures this pattern seems a bit broken I will post the one I ended up with. – Wouter Jul 21 '21 at 09:24
2

Another approach if you dont want to repeat the column Definitions for all of your Models that inherit from the same base Entity like this:

protected override void OnModelCreating(ModelBuilder modelBuilder){
        modelBuilder.Entity<Order>()
            .Property(b => b.CreatedDateTime)
            .HasDefaultValueSql("CURRENT_TIMESTAMP ");

        modelBuilder.Entity<Adress>()
            .Property(b => b.CreatedDateTime)
            .HasDefaultValueSql("CURRENT_TIMESTAMP ");
        // …

}

is to find all the Entites that inhert from the base Entity, loop over them and call the generic Method as shown below, in which the redundant Logic is placed:

protected override void OnModelCreating(ModelBuilder modelBuilder){
    foreach (Type type in GetEntityTypes(typeof(BaseEntity))){
        var method = SetGlobalQueryMethod.MakeGenericMethod(type);
        method.Invoke(this, new object[] { modelBuilder });
    }
}

static readonly MethodInfo SetGlobalQueryMethod = typeof(/*your*/Context)
    .GetMethods(BindingFlags.Public | BindingFlags.Instance)
    .Single(t => t.IsGenericMethod && t.Name == "SetGlobalQuery");

public void SetGlobalQuery<T>(ModelBuilder builder) where T : BaseEntity{
    builder.Entity<T>().Property(o => o.CreatedDateTime).HasDefaultValueSql("CURRENT_TIMESTAMP");
    // Additional Statements
}

For the "GetEntityTypes" Method you need the Nuget Package „Microsoft.Extensions.DependencyModel“

private static IList<Type> _entityTypeCache;
private static IList<Type> GetEntityTypes(Type type)
{
    if (_entityTypeCache != null && _entityTypeCache.First().BaseType == type)
    {
        return _entityTypeCache.ToList();
    }

    _entityTypeCache = (from a in GetReferencingAssemblies()
                        from t in a.DefinedTypes
                        where t.BaseType == type
                        select t.AsType()).ToList();

    return _entityTypeCache;
}

private static IEnumerable<Assembly> GetReferencingAssemblies()
{
    var assemblies = new List<Assembly>();
    var dependencies = DependencyContext.Default.RuntimeLibraries;

    foreach (var library in dependencies)
    {
        try
        {
            var assembly = Assembly.Load(new AssemblyName(library.Name));
            assemblies.Add(assembly);
        }
        catch (FileNotFoundException)
        { }
    }
    return assemblies;
}

Its a bit hacky in my opinion, but works fine for me!

The source with more details:

https://www.codingame.com/playgrounds/5514/multi-tenant-asp-net-core-4---applying-tenant-rules-to-all-enitites

JIT Solution
  • 693
  • 11
  • 14
2

I'm late to the party, but this is what I did in the OnModelCreating method to achieve similar results.

Basically, I have (4) properties that inherit from a BaseEntity. Two of those are dates why two are strings.

For the dates, I wanted the default to be SQL's GETUTCDATE and the string to be "SystemGenerated." Using a static helper that allows me to retrieve the property name from BaseEntity in a strongly-typed manner, I grab the (4) property names. Then, I iterate over all of the iterate over all of the ModelBuilder entities after my primary mappings are set-up. This allows modelBuilder.Model.GetEntityTypes to return the entities that the modelBuidler is aware of. Then it's a matter of looking at the ClrType.BaseType to see if the type inherits from my BaseEntity and setting the defaults on the PropertyBuilder.

I tested this directly and through EF Migrations which confirmed that the proper SQL was generated.

var createdAtUtc = StaticHelpers.GetPropertyName<BaseEntity>(x => x.CreatedAtUtc);
var lastModifiedAtUtc = StaticHelpers.GetPropertyName<BaseEntity>(x => x.LastModifiedAtUtc);
var createdBy = StaticHelpers.GetPropertyName<BaseEntity>(x => x.CreatedBy);
var lastModifiedBy = StaticHelpers.GetPropertyName<BaseEntity>(x => x.LastModifiedBy);
foreach (var t in modelBuilder.Model.GetEntityTypes())
{
    if (t.ClrType.BaseType == typeof(BaseEntity))
    {
        modelBuilder.Entity(t.ClrType).Property(createdAtUtc).HasDefaultValueSql("GETUTCDATE()");
        modelBuilder.Entity(t.ClrType).Property(lastModifiedAtUtc).HasDefaultValueSql("GETUTCDATE()");
        modelBuilder.Entity(t.ClrType).Property(createdBy).HasDefaultValueSql("SystemGenerated");
        modelBuilder.Entity(t.ClrType).Property(lastModifiedBy).HasDefaultValueSql("SystemGenerated");
    }
}

Here is the the static helper for getting property names for a given type..

public static string GetPropertyName<T>(Expression<Func<T, object>> expression)
{
    if (expression.Body is MemberExpression)
    {
        return ((MemberExpression)expression.Body).Member.Name;
    }
    else
    {
        var op = ((UnaryExpression)expression.Body).Operand;
        return ((MemberExpression)op).Member.Name;
    }
}
long2know
  • 1,280
  • 10
  • 9
1

Enhanced solution with explicit interface implementations:

Here's an enhanced version that doesn't require you to call base.Configure(builder) in your deriving configurations (as in the accepted answer), and also doesn't require you to come up with a new name for the Configure method for every level of hierarchy (as in this answer):

We can do this by making use of explicit interface implementations:

public abstract class BaseEntityConfiguration<T> : IEntityTypeConfiguration<T> where T : BaseEntity
{
    void IEntityTypeConfiguration<T>.Configure(EntityTypeBuilder<T> builder)
    {
        // Do all the configuration specific to `BaseEntity`
        builder.Property(v => v.Id)
            .Whatever();

        Configure(builder);
    }

    protected abstract void Configure(EntityTypeBuilder<T> builder);
}

And then:

public class DerivedEntityConfiguration : BaseEntityConfiguration<DerivedEntity>
{
    protected override void Configure(EntityTypeBuilder<DerivedEntity> builder)
    {
        // Do all the configuration specific to `DerivedEntity`
    }
}

Tested with EF Core 7.0 and it works as expected.

Arad Alvand
  • 8,607
  • 10
  • 51
  • 71