Collin Barrett
Collin Barrett

Reputation: 2569

Use IEntityTypeConfiguration with a base entity

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

Upvotes: 29

Views: 29668

Answers (5)

Arad
Arad

Reputation: 12852

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.

Upvotes: 7

long2know
long2know

Reputation: 1360

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

Upvotes: 2

Armin Kianian
Armin Kianian

Reputation: 143

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.

Upvotes: 9

JIT Solution
JIT Solution

Reputation: 1063

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

Upvotes: 2

SpruceMoose
SpruceMoose

Reputation: 10320

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

Upvotes: 47

Related Questions