Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 287
Nat Jacobs Avatar asked Oct 31 '19 07:10

Nat Jacobs


1 Answers

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.

like image 122
Nat Jacobs Avatar answered Sep 30 '22 15:09

Nat Jacobs