Search code examples
c#dependency-injectionentity-framework-coreautofac

Access DI services inside an IEntityTypeConfiguration<T> when using ApplyConfigurationsFromAssembly() assembly scanning


I need to access some DI'd services inside my IEntityTypeConfiguration classes in order to find some user session info and perform some query filtering.

I can achieve this the 'manual' way by doing the following...

    // setup config to use injection (everything normal here)
    public class MyEntityConfig: IEntityTypeConfiguration<MyEntity>
    {
        private readonly IService _service;

        public MyEntityConfig(IService service)
        {
            IService = service;
        }


        public void Configure(EntityTypeBuilder<MyEntity> entity)
        {
            // do some stuff to entity here using injected _service
        }
    }

    //use my normal DI (autofac) to inject into my context, then manually inject into config
    public class MyContext: DbContext
    {
        private readonly IService _service;

        public MyContext(DbContextOptions options, IService service) : base(options)
        {
            _service = service;
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            //this works no problem
            modelBuilder.ApplyConfiguration(new MyEntityConfig(_service));
        }
    }

What I want to do in the last part is use assembly scanning to pull in my config via...

 \\modelBuilder.ApplyConfiguration(new MyEntityConfig(_service));
 modelBuilder.ApplyConfigurationsFromAssembly(typeof(MyContext).Assembly);

But doing it this way always calls the default ctor for the IEntityTypeConfiguration<> so my injected services will all be empty.

I considered trying to roll my own version of ApplyConfigurationsFromAssembly by using reflection to get the configs and then calling the ctor's myself, but that seemed unpleasant.

Any ideas?


Solution

  • So this is what I came up with after I followed @Cyril's lead and looked into the source. I 'borrowed' the existing ModelBuilder.ApplyConfigurationsFromAssembly() method and re-wrote a new version (as a model builder extension) that can take param list of services.

            /// <summary>
            /// This extension was built from code ripped out of the EF source.  I re-jigged it to find
            /// both constructors that are empty (like normal) and also those that have services injection
            /// in them and run the appropriate constructor for them and then run the config within them.
            ///
            /// This allows us to write EF configs that have injected services in them.
            /// </summary>
            public static ModelBuilder ApplyConfigurationsFromAssemblyWithServiceInjection(this ModelBuilder modelBuilder, Assembly assembly, params object[] services)
            {
                // get the method 'ApplyConfiguration()' so we can invoke it against instances when we find them
                var applyConfigurationMethod = typeof(ModelBuilder).GetMethods().Single(e => e.Name == "ApplyConfiguration" && e.ContainsGenericParameters &&
                                                                                e.GetParameters().SingleOrDefault()?.ParameterType.GetGenericTypeDefinition() ==
                                                                                typeof(IEntityTypeConfiguration<>));
    
    
                // test to find IEntityTypeConfiguration<> classes
                static bool IsEntityTypeConfiguration(Type i) => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEntityTypeConfiguration<>);
    
                // find all appropriate classes, then create an instance and invoke the configure method on them
                assembly.GetConstructableTypes()
                    .ToList()
                    .ForEach(t => t.GetInterfaces()
                        .Where(IsEntityTypeConfiguration)
                        .ToList()
                        .ForEach(i =>
                        {
                            {
                                var hasServiceConstructor = t.GetConstructor(services.Select(s => s.GetType()).ToArray()) != null;
                                var hasEmptyConstructor = t.GetConstructor(Type.EmptyTypes) != null;
    
                                if (hasServiceConstructor)
                                {
                                    applyConfigurationMethod
                                        .MakeGenericMethod(i.GenericTypeArguments[0])
                                        .Invoke(modelBuilder, new[] { Activator.CreateInstance(t, services) });
                                    Log.Information("Registering EF Config {type} with {count} injected services {services}", t.Name, services.Length, services);
                                }
                                else if (hasEmptyConstructor)
                                {
                                    applyConfigurationMethod
                                        .MakeGenericMethod(i.GenericTypeArguments[0])
                                        .Invoke(modelBuilder, new[] { Activator.CreateInstance(t) });
                                    Log.Information("Registering EF Config {type} without injected services", t.Name, services.Length);
                                }
                            }
                        })
                    );
    
                return modelBuilder;
            }
    
            private static IEnumerable<TypeInfo> GetConstructableTypes(this Assembly assembly)
            {
                return assembly.GetLoadableDefinedTypes().Where(t => !t.IsAbstract && !t.IsGenericTypeDefinition);
            }
    
            private static IEnumerable<TypeInfo> GetLoadableDefinedTypes(this Assembly assembly)
            {
                try
                {
                    return assembly.DefinedTypes;
                }
                catch (ReflectionTypeLoadException ex)
                {
                    return ex.Types.Where(t => t != null as Type).Select(IntrospectionExtensions.GetTypeInfo);
                }
            }
        }
    
    

    Then in my OnModelCreating() I just call my extension...

    modelBuilder.ApplyConfigurationsFromAssemblyWithServiceInjection(typeof(MyContext).Assembly, myService, myOtherService);
    

    This implementation is not ideal as all your configs must have either a parameter-less constructor or a constructor with a fixed list of services (ie can't have ClassA(serviceA), ClassB(ServiceB); you can only have ClassA(serviceA, serviceB), ClassB(serviceA, serviceB) but that is not a problem for my use case, as this is exactly what I need at the moment.

    If I needed a more flexible path I was going to go down the path of making the modelbuilder container aware and then doing the service resolution inside using the DI container, but I don't need that at the moment.