17

I have entities derived from a base entity in my application which uses ef core code-first approach.

Base class

public abstract class BaseEntity<T> : IEntity<T>
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public T Id { get; set; }

    object IEntity.Id { get { return Id; } set { } }

    private DateTime? createdOn;
    [DataType(DataType.DateTime)]
    public DateTime CreatedOn { get => createdOn ?? DateTime.Now; set => createdOn = value; }

    [DataType(DataType.DateTime)]
    public DateTime? ModifiedOn { get; set; }

    public bool IsDeleted { get; set; }
    // Auto increment for all entities.
    public int OrderId { get; set; }
}

And an entity

public class UserEntity : BaseEntity<int>
{
    public string EmployeeId { get; set; }
    public string FullName { get; set; }
    public string Email { get; set; }
}

I can apply the .ValueGeneratedOnAdd() method on property OrderId in OnModelCreating for each entity however, is there a way to apply a general rule for all entities without repeatig yourself?

ibubi
  • 2,469
  • 3
  • 29
  • 50
  • Can't you use `[DatabaseGenerated(DatabaseGeneratedOption.Identity)]` on `OrderId` as well? – Jogge Nov 13 '18 at 07:55
  • There is already a priamary key column with identity option. So it is not allowed to have with this attribute anymore. – ibubi Nov 13 '18 at 11:06
  • Unrelated to the question, but `IEntity` knows `Id` is a `T` so `IEntity.Id` needn't return `object`. – sellotape Nov 15 '18 at 19:27
  • @sellotape please refer to this [link](https://cpratt.co/generic-entity-base-class/) and see **Implementation** paragraph there for details.. – ibubi Nov 15 '18 at 20:44
  • @ibubi - fair enough; I should have realised it's `IEntity.Id`, not `IEntity.Id`... – sellotape Nov 15 '18 at 20:52

4 Answers4

32

With the lack of custom conventions, you could use the typical modelBuilder.Model.GetEntityTypes() loop, identify the target entity types and invoke common configuration.

Identification in your case is a bit complicated because of the base generic class, but doable by iterating down Type.BaseType and check for BaseEntity<>. Once you find it, you can retrieve the generic argument T which you'll need later.

If you don't want to use generic class implementing IEnityTypeConfiguration<TEntity>, then the idea is to put the implementation in generic constrained method like this

static void Configure<TEntity, T>(ModelBuilder modelBuilder)
    where TEntity : BaseEntity<T>
{
    modelBuilder.Entity<TEntity>(builder =>
    {
        builder.Property(e => e.OrderId).ValueGeneratedOnAdd();
    });
}

Passing the actual entity type TEntity to modelBuilder.Enity method is crucial, because otherwise EF Core will consider whatever you pass to be an entity type and configure TPH inheritance.

Calling the method requires reflection - finding the generic method definition, using MakeGenericMethod and then Invoke.

Here is all that encapsulated in a static class:

public static class BaseEntityConfiguration
{
    static void Configure<TEntity, T>(ModelBuilder modelBuilder)
        where TEntity : BaseEntity<T>
    {
        modelBuilder.Entity<TEntity>(builder =>
        {
            builder.Property(e => e.OrderId).ValueGeneratedOnAdd();
        });
    }

    public static ModelBuilder ApplyBaseEntityConfiguration(this ModelBuilder modelBuilder)
    {
        var method = typeof(BaseEntityConfiguration).GetTypeInfo().DeclaredMethods
            .Single(m => m.Name == nameof(Configure));
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            if (entityType.ClrType.IsBaseEntity(out var T))
                method.MakeGenericMethod(entityType.ClrType, T).Invoke(null, new[] { modelBuilder });
        }
        return modelBuilder;
    }

    static bool IsBaseEntity(this Type type, out Type T)
    {
        for (var baseType = type.BaseType; baseType != null; baseType = baseType.BaseType)
        {
            if (baseType.IsGenericType && baseType.GetGenericTypeDefinition() == typeof(BaseEntity<>))
            {
                T = baseType.GetGenericArguments()[0];
                return true;
            }
        }
        T = null;
        return false;
    }
}

Now all you need is to call it from inside your OnModelCreating override:

modelBuilder.ApplyBaseEntityConfiguration();
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • Thanks for this detailed answer. – ibubi Nov 15 '18 at 20:43
  • @Ivan Stoev, Its not working for me in case of EF Core 3.0. Note: My BaseEnity is not Generic. – TanvirArjel Nov 21 '19 at 12:46
  • 6
    @TanvirArjel If your base entity is not generic, then it should be simpler. Remove the `T` from `Configure` method (e.g. `static void Configure(ModelBuilder modelBuilder) where TEntity : BaseEntity`) and from `MakeGenericMethod`. Also `IsBaseEntity` method is not needed, use `if (entityType.ClrType.IsSubclassOf(typeof(BaseEntity))` criteria for instance. – Ivan Stoev Nov 21 '19 at 13:21
  • It would probably be helpful to know that efcore threw the error `Collection was modified after the enumerator was instantiated.` upon Add-Migration when I called the extension method `modelBuilder.ApplyBaseEntityConfiguration();` before calling `modelBuilder.ApplyConfigurationsFromAssembly()` in my `OnModelCreating` override. Once I revered the order of the calls, everything went smoothly. There were no compilation errors in either case. Not sure about the specifics of why this happens but I thought should just leave a comment here for anyone who is trying this approach. – Suhaib Syed Apr 27 '20 at 13:33
  • Very powerfull! Is there a smilar way to access all properties of a generic type? – sinsedrix Jun 09 '20 at 09:56
  • @sinsedrix Not sure I follow. For regular types you have reflection, and for EF Core entity types you have `GetProperties()`, `GetNavigations()` etc. – Ivan Stoev Jun 09 '20 at 11:55
  • 1
    @IvanStoev Thats's because my entities don't inherit from BaseEntity (as they are indepedent from EF). So I would need to access all properties from modelBuilder like in EF6 (ie: `modelBuilder.Properties().Configure(x => x.HasColumnType("VARCHAR2"));`) – sinsedrix Jun 09 '20 at 14:27
  • 1
    @sinsedrix https://stackoverflow.com/questions/41468722/loop-reflect-through-all-properties-in-all-ef-models-to-set-column-type/41469383#41469383 – Ivan Stoev Jun 09 '20 at 15:17
  • This is by far the best sollution i came accross and it works with EF Core 5.0, just one thing that i see thats missing is this will not work with Guid fields so you need to change the Configure function as folows just put the rest of the code after an else statement and it works like wonders. if (typeof(T) == typeof(Guid)) { modelBuilder.Entity(builder => { builder.Property(e => e.Id).ValueGeneratedOnAdd().HasDefaultValueSql("NEWID()"); }); } – AirWolf Dec 01 '20 at 18:45
2

If you want every property named OrderId to be ValueGeneratedOnAddYou you could solve it like this without repeating yourself:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    foreach (var et in modelBuilder.Model.GetEntityTypes())
    {
        foreach (var prop in et.GetProperties())
        {
            if (prop.Name == "OrderId")
            {
                prop.ValueGenerated = Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.OnAdd;
            }
        }
    }
}

Source:

https://stackoverflow.com/a/62380293/3850405

This can also be used to set maximum string length for all strings as an example:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);
    
    foreach (var property in modelBuilder.Model.GetEntityTypes()
        .SelectMany(t => t.GetProperties())
        .Where(p => p.ClrType == typeof(string)))
    {
        if (property.GetMaxLength() == null)
            property.SetMaxLength(450);
    }
}

Source:

https://stackoverflow.com/a/50852517/3850405

You can also set mark every entity that inherits from IEntity as IsTemporal:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    foreach (var et in modelBuilder.Model.GetEntityTypes())
    {
        if (et.ClrType.IsAssignableTo(typeof(IEntity)))
        {
            et.SetIsTemporal(true);
        }
    }
}

Source:

https://stackoverflow.com/a/75496573/3850405

Ogglas
  • 62,132
  • 37
  • 328
  • 418
0

In EF6 you can use:

modelBuilder.Properties<int>().Where(p=>p.Name == "OrderId").Configure(c => c.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity));
Aldert
  • 4,209
  • 1
  • 9
  • 23
0
foreach (var item in modelBuilder.Model.GetEntityTypes())
{
    item.FindProperty("CreatedOn")?.SetDefaultValueSql("GETDATE()");
    item.FindProperty("ModifiedOn")?.SetDefaultValueSql("GETDATE()");
    item.FindProperty("AAStatus")?.SetDefaultValue("Alive");
}

i researched and found this thread, my solution after reading it is

wwwwww3q
  • 1
  • 1