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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With