Dynamically Building a Model with Code First

I’ve answered a few emails recently on this topic so it felt like time to turn it into a blog post.

In this scenario the set of classes that make up your model isn’t known at compile time and is discovered dynamically at runtime. An example of such a scenario is a WordPress/Orchard/etc. style website that allows modules to be plugged in. These modules live in a separate assembly and may contain classes that represent data to be persisted in the application database. These classes are all pulled into a central Code First model to be used for data access.

 

Finding the Classes

There are lots of different approaches for identifying the classes to be included in the model. For this example lets use a [Persistent] attribute, something like this:

[AttributeUsage(AttributeTargets.Class)]
public class PersistentAttribute : Attribute
{
}

Now we can add some logic to the OnModelCreating method of our context to scan assemblies and add any classes with the [Persist] attribute. We’re going to assume that the assemblies that may contain classes are all loaded into the current AppDomain, of course you may have some other mechanism that provides a list of the assemblies to check.

public class MyContext : DbContext
{
  protected override void OnModelCreating(DbModelBuilder modelBuilder)
  {
    var entityMethod = typeof(DbModelBuilder).GetMethod("Entity");

    foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
    {
      var entityTypes = assembly
        .GetTypes()
        .Where(t =>
          t.GetCustomAttributes(typeof(PersistentAttribute), inherit: true)
          .Any());

      foreach (var type in entityTypes)
      {
        entityMethod.MakeGenericMethod(type)
          .Invoke(modelBuilder, new object[] { });
      }
    }
  }
}

In this example the entire model is dynamically discovered, but you could also have some static parts of the model that are registered with modelBuilder too.

 

An Alternative Using EntityConfiguration<TEntity>

Code First allows you to create a class that derives from EntityTypeConfiguration<TEntity> to encapsulate the Fluent API configuration for an entity. If your using this approach to configuration you can just look for these configuration classes and register them, instead of finding the entities to register. Notice that we are filtering out the EntityFramework assembly since it has some configuration classes that it uses internally.

public class MyContext : DbContext
{
  protected override void OnModelCreating(DbModelBuilder modelBuilder)
  {
    var addMethod = typeof(ConfigurationRegistrar)
      .GetMethods()
      .Single(m => 
        m.Name == "Add" 
        && m.GetGenericArguments().Any(a => a.Name == "TEntityType"));

    foreach (var assembly in AppDomain.CurrentDomain
      .GetAssemblies()
      .Where(a => a.GetName().Name != "EntityFramework"))
    {
      var configTypes = assembly
        .GetTypes()
        .Where(t => t.BaseType != null
          && t.BaseType.IsGenericType
          && t.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>));

      foreach (var type in configTypes)
      {
        var entityType = type.BaseType.GetGenericArguments().Single();

        var entityConfig = assembly.CreateInstance(type.FullName);
        addMethod.MakeGenericMethod(entityType)
          .Invoke(modelBuilder.Configurations, new object[] { entityConfig });
      }
    }
  }
}

 

What if the Model Changes?

Code First Migrations to the rescue… You may not be able to use Migrations from the Package Manager Console because the logic to discover your model may require you full application to be running. Fortunately those commands are just thin wrappers over an API. Here is some code to automatically change the database when new classes or properties are added to the model. My recent blog post has more details on invoking Migrations from code.

var config = new DbMigrationsConfiguration<MyContext> { AutomaticMigrationsEnabled = true };
var migrator = new DbMigrator(config);
migrator.Update();
Advertisements