使用 ApplyConfigurationsFromAssembly() 程序集扫描时访问 IEntityTypeConfiguration<T> 内的 DI 服务

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

我需要访问我的 IEntityTypeConfiguration 类 中的一些 DI 服务,以便找到一些用户会话信息并执行一些查询过滤。

我可以通过执行以下 'manual' 方式实现此...

    // 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));
        }
    }

最后一部分我想做的是使用程序集扫描通过...提取我的配置...

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

但这样做总是会调用 IEntityTypeConfiguration<> 的默认构造函数,因此我注入的服务将全部为空。

我考虑过使用反射获取配置然后自己调用 ctor 来尝试推出我自己的 ApplyConfigurationsFromAssembly 版本,但这似乎令人不快。

有什么想法吗?

这就是我在跟随@Cyril 的领导并查看源代码后想到的。我 'borrowed' 现有的 ModelBuilder.ApplyConfigurationsFromAssembly() 方法并重新编写了一个新版本(作为模型构建器扩展),它可以采用参数服务列表。

        /// <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);
            }
        }
    }

然后在我的 OnModelCreating() 中调用我的分机...

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

此实现并不理想,因为您的所有配置都必须具有无参数构造函数或具有固定服务列表的构造函数(即不能具有 ClassA(serviceA)、ClassB(ServiceB);您只能有 ClassA(serviceA, serviceB), ClassB(serviceA, serviceB) 但这对我的用例来说不是问题,因为这正是我目前需要的。

如果我需要一个更灵活的路径,我打算走下让模型构建器容器感知的路径,然后使用 DI 容器在内部进行服务解析,但我现在不需要那个。