5

Let's say I have some interface like:

public interface ISoftDeletable
{
    bool IsActive { get; set }
}

And I have many entities that implement it:

public class Entity1 : ISoftDeletable
{
    public int Id { get; set }
    public bool IsActive { get; set; }
}

public class Entity2 : ISoftDeletable
{
    public int Id { get; set }
    public bool IsActive { get; set; }
}

In OnModelCreating:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Entity1>().Property(e => e.IsActive).HasDefaultValue(true);
    modelBuilder.Entity<Entity2>().Property(e => e.IsActive).HasDefaultValue(true);
}

Is there any way to refactor this so I can set HasDefaultValue for all entities implementing ISoftDeletable instead of doing this like above?

I can probably solve this specific case using a default constructor for each entity with IsActive = true or even create a base abstract class but I don't like it very much.

Similar question: Ef core fluent api set all column types of interface

Is there any better way?

Konrad
  • 6,385
  • 12
  • 53
  • 96
  • 1
    Extension method? Still will need to list every entity you need bindings for, but won't need to keep doing the Property.HasDefaultValue. – Erndob Aug 09 '18 at 09:53
  • 1
    Use auto property initializers in the entity classes? `public bool IsActive { get; set; } = true;` Or "invert" the name the property to something like "IsDeleted" so it automatically gets the correct default value. – mm8 Aug 09 '18 at 09:53
  • 1
    Having abstract class with the property and default value looks like good choice though. Not sure why you don't want it. – Erndob Aug 09 '18 at 09:57
  • The problem with default values set in code is that they won't be set on the database columns. That's not such a big problem but still. I'm not going to edit the database directly though. – Konrad Aug 09 '18 at 10:00
  • 1
    Then you have that option in another question, a base configuration class, which will set what you need. If you don't want to change the entire structure how you do configuration and introduce new configuration classes, you are left with an option of an extension method, which would abstract it at least partially. – Erndob Aug 09 '18 at 10:11
  • 1
    Comming from EF6, I'd use something like `modelBuilder.Types().Configure(c => c.Property(e => e.IsActive).HasDefaultValue(true))` however EF6 doesn't have a `HasDefaultValue` and I don't know whether EF Core still supports the `Types` configuration. – grek40 Aug 09 '18 at 12:39
  • @grek40 Interesting. EF Core doesn't have anything like `Types`. – Konrad Aug 09 '18 at 12:42
  • https://github.com/aspnet/EntityFramework6/blob/527ae18fe23f7649712e9461de0c90ed67c3dca9/src/EntityFramework/DbModelBuilder.cs – Konrad Aug 09 '18 at 12:45
  • Would be nice to see this in the EF Core as well. – Konrad Aug 09 '18 at 12:45

2 Answers2

22

I found some answer here: GetEntityTypes: configure entity properties using the generic version of .Property<TEntity> in EF Core

Apart from comments above, there's a way to do it without calling it for each entity. This probably could be refactored to some extension method as mentioned by Erndob's comment under my question.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    foreach (var entityType in modelBuilder.Model.GetEntityTypes())
    {
        if (typeof(ISoftDeletable).IsAssignableFrom(entityType.ClrType))
        {
            modelBuilder.Entity(entityType.ClrType).Property<bool>(nameof(ISoftDeletable.IsActive)).HasDefaultValue(true);
        }
    }
}

The solution is to use ModelBuilder.Model.GetEntityTypes() and find entity types that are assignable from ISoftDeletable.

In my opinion, this is much better than configuring it manually or even creating an abstract IEntityTypeConfiguration<> class because you don't have to remember to use it for all ISoftDeletable classes.


More clean looking:

public static class ModelBuilderExtensions
{
    public static ModelBuilder EntitiesOfType<T>(this ModelBuilder modelBuilder,
        Action<EntityTypeBuilder> buildAction) where T : class
    {
        return modelBuilder.EntitiesOfType(typeof(T), buildAction);
    }

    public static ModelBuilder EntitiesOfType(this ModelBuilder modelBuilder, Type type,
        Action<EntityTypeBuilder> buildAction)
    {
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
            if (type.IsAssignableFrom(entityType.ClrType))
                buildAction(modelBuilder.Entity(entityType.ClrType));

        return modelBuilder;
    }
}

And OnModelCreating:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.EntitiesOfType<ISoftDeletable>(builder =>
    {
        builder.Property<bool>(nameof(ISoftDeletable.IsActive)).HasDefaultValue(true);

        // query filters :)
        var param = Expression.Parameter(builder.Metadata.ClrType, "p");
        var body = Expression.Equal(Expression.Property(param, nameof(ISoftDeletable.IsActive)), Expression.Constant(true));
        builder.HasQueryFilter(Expression.Lambda(body, param));
    });
}
Konrad
  • 6,385
  • 12
  • 53
  • 96
  • 1
    Nice. Also didn't know you can select property simply with a string. – Erndob Aug 09 '18 at 11:22
  • @Erndob Yeah it would be much cleaner if it used generic `EntityTypeBuilder` in action argument but that would require to use reflection – Konrad Aug 09 '18 at 11:25
  • @Konrad I attempted to do use the generic `EntityTypeBuilder` because it seemed like a good solution, but ran into a problem. When I extracted the `EntityTypeBuilder` from the `ModelBuilder`'s `Entity()` method via reflection I received this error when invoking it. `System.ArgumentException: The specified type 'IExample' must be a non-interface reference type to be used as an entity type` So it might just not be possible. :/ – zxcv Feb 25 '20 at 19:00
  • @zxcv I don't remember. But my extension method worked fine with interfaces last time I was doing it. Don't know if something changed in 3.0+ – Konrad Feb 25 '20 at 21:56
  • IIRC there is some issue on github about this. I think this is it https://github.com/dotnet/efcore/issues/6787 – Konrad Feb 25 '20 at 22:00
  • You might also be interested in https://stackoverflow.com/questions/50727860/ef-core-2-1-hasconversion-on-all-properties-of-type-datetime/52184765#52184765 and you should be good to go :) – Konrad Feb 25 '20 at 22:03
  • 1
    ahhh ClrType - this is where i was going wrong, thanks man!!! – Phil Mar 10 '20 at 14:53
  • Good answer! Noticed one thing when implementing a similar solution myself though: You can't get modelbuilders for owned types (exception is thrown) as those are configured through their owning types, so you might want to filter on `!entityType.IsOwned()`. – Shazi Jul 19 '23 at 09:15
4

I wanted to do something similar to this but use the IEntityTypeConfiguration interface to hold my generic configurations. I ended up having to use reflection but it works:

Interface:

public interface IHasDisplayId
{
    Guid DisplayId { get; }
}

EntityTypeConfig:

public class HasDisplayIdEntityTypeConfiguration<T> : IEntityTypeConfiguration<T> where T : class, IHasDisplayId
{
    public void Configure(EntityTypeBuilder<T> builder)
    {
        builder.Property(e => e.DisplayId).IsRequired();
        builder.HasIndex(e => e.DisplayId);
    }
}

Extension method:

public static ModelBuilder ApplyConfiguration<T>(this ModelBuilder modelBuilder, Type configurationType, Type entityType)
{
    if (typeof(T).IsAssignableFrom(entityType))
    {
        // Build IEntityTypeConfiguration type with generic type parameter
        var configurationGenericType = configurationType.MakeGenericType(entityType);
        // Create an instance of the IEntityTypeConfiguration implementation
        var configuration = Activator.CreateInstance(configurationGenericType);
        // Get the ApplyConfiguration method of ModelBuilder via reflection
        var applyEntityConfigurationMethod = typeof(ModelBuilder)
            .GetMethods()
            .Single(e => e.Name == nameof(ModelBuilder.ApplyConfiguration)
                         && e.ContainsGenericParameters
                         && e.GetParameters().SingleOrDefault()?.ParameterType.GetGenericTypeDefinition() == typeof(IEntityTypeConfiguration<>));
        // Create a generic ApplyConfiguration method with our entity type
        var target = applyEntityConfigurationMethod.MakeGenericMethod(entityType);
        // Invoke ApplyConfiguration, passing our IEntityTypeConfiguration instance
        target.Invoke(modelBuilder, new[] { configuration });
    }

    return modelBuilder;
}

Usage:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    foreach (var entityType in modelBuilder.Model.GetEntityTypes())
    {
        modelBuilder.ApplyConfiguration<IHasDisplayId>(typeof(HasDisplayIdEntityTypeConfiguration<>), entityType.ClrType);
    }
}
Codemunkie
  • 433
  • 3
  • 14