Search code examples
c#asp.net-core.net-core.net-assembly

Generic Entity mapping in EF


I want to make a generic entity mapper that maps all entities to dbContext and EF. Currently, after I do Add-Migration init, I receive an error:

Cannot create an instance of Models.DataMapping.EntityMapper`1[TEntity] because Type.ContainsGenericParameters is true.

I do not understand what this error exacly means and why if type ContainsGenericParameters is true it should cause a crash. Any ideas what is wrong and how could I fix it?
This is my code:

namespace Models.DataMapping
{
    public interface IEntityMapper
    {
        void Map(ModelBuilder modelBuilder);
    }
}

namespace Models.DataMapping
{
    public abstract class EntityMapper<TEntity> : 
        IEntityMapper where TEntity : class
    {
        public void Map(ModelBuilder modelBuilder)
        {
            EntityTypeBuilder<TEntity> entityTypeBuilder = modelBuilder.Entity<TEntity>();

            MapEntity(entityTypeBuilder);
        }

        protected virtual void MapEntity(EntityTypeBuilder<TEntity> entityTypeBuilder) 
        {    
        }
    }
}
namespace Models.DataMapping
{
    public static class EntityMappersProvider
    {
        public static IReadOnlyCollection<IEntityMapper> GetLocalEntityMappers() {
            return Assembly.GetExecutingAssembly()
                .GetTypes()
                .Where(t => typeof(IEntityMapper).IsAssignableFrom(t) && t.IsClass)
                .Select(t => (IEntityMapper)Activator.CreateInstance(t))
                .ToArray();
        }
    }
}

Including provider in dbContext.

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

            IReadOnlyCollection<IEntityMapper> entityMappers = EntityMappersProvider.GetLocalEntityMappers();

            foreach (IEntityMapper mapper in entityMappers) {
                mapper.Map(modelBuilder);
            }
        }

Example of Entity relationships realization.

namespace Models.DataMapping.EntitiesMapping
{
    public class CourseMapper : EntityMapper<Course>
    {
        protected override void MapEntity(EntityTypeBuilder<Course> entityTypeBuilder)
        {
            entityTypeBuilder.ToTable("Courses");
            entityTypeBuilder.HasKey(a => a.Id);
            //....
        }
    }
}

Also what I have noticed that if I remove abstract keyword in EntityMapper and add t.IsAbstract in EntityMappersProvided empty migration gets created..


Solution

  • For me it looks like you are trying to reinvent IEntityTypeConfiguration which allows to move entity configuration code to separate classes :

    public class BlogEntityTypeConfiguration : IEntityTypeConfiguration<Blog>
    {
        public void Configure(EntityTypeBuilder<Blog> builder)
        {
            builder
                .Property(b => b.Url)
                .IsRequired();
        }
    }
    

    And provides convenience method to find all such configuration in assembly, which is available since EF Core 2.2:

    modelBuilder.ApplyConfigurationsFromAssembly(typeof(BlogEntityTypeConfiguration).Assembly);
    

    As for your error - it happens because your code tries to instantiate an open generic type (EntityMapper<TEntity>) with Activator.CreateInstance:

    public class OpenGeneric<T>{}
    Activator.CreateInstance(typeof(OpenGeneric<>)); // throws Cannot create an instance of Generic`1[T] because Type.ContainsGenericParameters is true.
    

    So you need to filter it out, for example by adding && !t.IsAbstract && ! t.ContainsGenericParameters in your Where clause.