Use IEntityTypeConfiguration with a base entity

18,071

Solution 1

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

Solution 2

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.

Solution 3

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

Solution 4

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;
    }
}
Share:
18,071

Related videos on Youtube

Collin Barrett
Author by

Collin Barrett

Crafting clean, SOLID solutions for complex issues I thrive in building and shipping quality software. For ten years, I have developed web, cloud, and desktop solutions for finance, retail, manufacturing, education, and defense. I pursue clean and maintainable code while reducing technical debt.

Updated on June 14, 2022

Comments

  • Collin Barrett
    Collin Barrett almost 2 years

    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
    Collin Barrett over 6 years
    Thanks, I'll give it a shot. What would you suggest my OnModelCreating() would look like with this solution?
  • SpruceMoose
    SpruceMoose over 6 years
    Should be the same. Just need to ensure to call base.Configure in each Configure() override.
  • dinotom
    dinotom about 6 years
    @CalC... does "b => b.CreatedDateUtc).HasDefaultValueSql("CURRENT_TIMESTAMP")" do the same thing as setting the property value in the class constructor?
  • SpruceMoose
    SpruceMoose about 6 years
    @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
  • serge
    serge over 3 years
    that method does not work if we have multiple inheritances: BaseEntity, IdEntity, NamedEntity etc
  • Wouter
    Wouter almost 3 years
    @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.
  • Eternal21
    Eternal21 almost 3 years
    @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
    Eternal21 almost 3 years
    @serge Have you been able to figure out the 'multiple inheritance'?
  • Wouter
    Wouter almost 3 years
    @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.
  • Junaid
    Junaid over 2 years
    Why make BaseEntityTypeConfiguration abstract?