Using dependency injection to configure Entity Framework DbContexts

Akos Nagy
May 6, 2019

So let's say you have your own DbContext descendant with a couple of DbSet<T> properties that contain your entities and you also want to configure some of your entities. If you know EF, you probably know how to do this (and why to do it like this). Here's an example:

public class MyContext : DbContext
{
  public DbSet<Person> People { get; set; }
  public DbSet<Car> Cars { get; set; }
  
  protected override void OnModelCreating(DbModelBuilder modelBuilder)
  {
     modelBuilder.Add(new PersonEntityTypeConfiguration());
     modelBuilder.Add(new CarEntityTypeConfiguration());
  }
}

I have written hundreds of lines of code like these — and I just now realize, that I could you dependency injection to make my life easier.

My choice of DI container is Autofac. Autofac has a neat little feature called implicit relationship types. One of these relationship types is the enumeration that allows you to register multiple components to a service and then resolve an IEnumerable of the service that resolves to a list containing one of each originally registered component.

So to make my life easier, I could simply just find a common interface for all my entity configurations, register them and use constructor injection to request all the configuration objects and finally add them later in the OnModelCreating() method to the modelBuilder.

The problem is that there is no such interface. The entity configurations must implement IEntityTypeConfiguration<T> (or IComplexTypeConfiguration<T>). And since the interface is generic and must be closed with a different entity type for each configuration, there is no common interface (this is actually a good example of how generics totally break object-orientation — but that's a different post).

So first, I had to create two new interfaces:

public interface IEntityTypeConfiguration { }
public interface IComplexTypeConfiguration { }

And then, when an entity type is created, this new interface is added to the configuration type:

public class PersonEntityTypeConfiguration : EntityTypeConfiguration<Person>, IEntityTypeConfiguration
{
  //... add actual configuration
}

And now, the context can be amended like this:

public class MyContext : DbContext
{
  private readonly IEnumerable<IEntityTypeConfiguration> entityTypeConfigs;
  private readonly IEnumerable<IComplexTypeConfiguration> complexTypeConfigs;
  public DbSet<Person> People { get; set; }
  public DbSet<Car> Cars { get; set; }
  
  public MyContext(IEnumerable<IEntityTypeConfiguration> entityTypeConfigs,
                   IEnumerable<IComplexTypeConfiguration> complexTypeConfigs)
  {
    this.entityTypeConfigs = entityTypeConfigs;
    this.complexTypeConfigs = complexTypeConfigs;
  }
  
  protected override void OnModelCreating(DbModelBuilder modelBuilder)
  {
     // TODO: Add configs
  }
}

And then I simply have to register the configurations:

builder.RegisterType<PersonEntityTypeConfiguration>().As<IEntityTypeConfiguration>();
builder.RegisterType<CarEntityTypeConfiguration>().As<IEntityTypeConfiguration>();
builder.RegisterType<MyContext>();

And now, whenever the context is resolved, the configurations are also resolved and all of them are instantiated into the parameters thanks to the enumerable implicit relationship type.

The only remaining problem is that now, inside the OnModelCreating() method the objects saved in the constructor must be somehow added to the ConfigurationRegistrar. And to do this, reflection-magic must be applied (since there is no relation between the open-generic EntityTypeConfiguration<T> and my interface).

public static class EntityTypeConfigurationExtensions
{
  private static MethodInfo GetAddMethod(Type inputType) =>        
      typeof(ConfigurationRegistrar)
          .GetMethods()
          .Single(m => m.Name == nameof(ConfigurationRegistrar.Add) &&
           m.GetParameters().Count() == 1 &&
           m.GetParameters()[0].ParameterType.IsGenericType &&
           m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == inputType
           );
        
  private static readonly MethodInfo addEntityTypeConfigMethod = GetAddMethod(typeof(EntityTypeConfiguration<>));
  private static readonly MethodInfo addComplexTypeConfigMethod = GetAddMethod(typeof(ComplexTypeConfiguration<>));
  public static void Add(this ConfigurationRegistrar configurationRegistrar, IEntityTypeConfiguration entityTypeConfiguration) => 
     Add(configurationRegistrar, addEntityTypeConfigMethod, entityTypeConfiguration);
        

  public static void Add(this ConfigurationRegistrar configurationRegistrar, IComplexTypeConfiguration complexTypeConfiguration) =>            
     Add(configurationRegistrar, addComplexTypeConfigMethod, complexTypeConfiguration);
        
  private static void Add(ConfigurationRegistrar configurationRegistrar, MethodInfo addMethod, object configuration)
  {
    var type = configuration.GetType();            
    var genericAdd = addComplexTypeConfigMethod.MakeGenericMethod(type);
    genericAdd.Invoke(configurationRegistrar, new[] { configuration });
  }
}

This extension method makes it possible to add objects whose design-time type is IEntityTypeConfiguration (or IComplexTypeConfiguration). We can simply register them for the builder, and then write the context like this:

public class MyContext : DbContext
{
  private readonly IEnumerable<IEntityTypeConfiguration> entityTypeConfigs;
  private readonly IEnumerable<IComplexTypeConfiguration> complexTypeConfigs;
  public DbSet<Person> People { get; set; }
  public DbSet<Car> Cars { get; set; }
  
  public MyContext(IEnumerable<IEntityTypeConfiguration> entityTypeConfigs,
                   IEnumerable<IComplexTypeConfiguration> complexTypeConfigs)
  {
    this.entityTypeConfigs = entityTypeConfigs;
    this.complexTypeConfigs = complexTypeConfigs;
  }
  
  protected override void OnModelCreating(DbModelBuilder modelBuilder)
  {
     foreach (var config in entityTypeConfigs)
     {
       modelBuilder.Add(config);
     }
     foreach (var config in complexTypeConfigs)
     {
       modelBuilder.Add(config);
     }
  }
}

Not ideal, but now I don't have to worry about manually adding the new configurations in the OnModelCreating(). We do have to remember to register them, but if we use the assembly scanning feature of Autofac, this problem is solved as well.

I'm planning on using this method in my next project, so noone has to manually add new configurations and noone forgets adding them.

Akos Nagy