Cargar Dinámicamente Configuraciones de Entidades en Entity Framework y Code-First

En estos días he estado muy activo empleando la nueva versión del Entity Framework de Microsoft.

Una de las cosas que hace poderoso a este framework y que en particular a mi me encanta es la posibilidad de definir y configurar todo el repositorio de datos (la base de datos per se) completamente desde código fuente gestionado, un enfoque que se conoce como Code-First.

A través de Code-First es posible definir las clases POCO y también configuradores (clases que bien heradan de EntityTypeConfiguration o de ComplexTypeConfiguration) y que nos permiten definir las restricciones sobre el modelo (como claves primarias y no nulidades) o los nombres de las tablas y sus columnas.

La parte interesante es que en modelos grandes y complejos un desarrollo termina llenándose de muchos configuradores que deben ser agregados al modelo en el momento de su creación (lo cual ocurre durante el evento OnModelCreating de la clase DbContext) y que termina incrementando dos factores muy poco positivos de un código: la complejidad ciclomática y el acoplamiento de clases, ambos elementos que son reportados por el Visual Studio a través del Code Analyzer como una violación a la regla CA1506.

Para evitar esto, lo ideal seria encontrar un mecanismo para cargar los configuradores de forma dinámica; pero el problema es que el método Add para el ModelBuilder que se emplea para cargarlos no admite como parámetro una clase base o genérica que pudiera ayudarnos a través de herencia, sino que explícitamente sólo acepta un EntityTypeConfiguration o un ComplexTypeConfiguration.

Por suerte, la plataforma .NET y el lenguaje C# cuentan con una palabra que logra salvar la patria: dynamic; que se encargará de indicar al compilador que no haga validaciones del tipo de dato/objeto que se está pasando como parámetro al método y que más bien este tipo de verificaciones se practiquen en tiempo de ejecución, es decir, que el compilador confíe en que nosotros como desarrolladores sabemos que estamos haciendo.

Así, el primer paso sería utilizar Reflection sobre nuestro ensamblado (que como ya está cargado en memoria durante la ejecución del evento OnModelCreating no impactará [negativamente] en el desempeño de nuestro aplicativo)

/// <summary>
/// Method calleded when the model for a derived context has been initialized, but before the model has 
/// been locked down and used to initialize the context.
/// </summary>
/// <param name="modelBuilder">The builder that defines the model for the context being created.</param>
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    if (modelBuilder != null)
    {
        IEnumerable<Type> entityTypeConfigurationTypes = Assembly.GetExecutingAssembly().GetTypes().Where(type => !type.IsAbstract 
        && (type.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration) || type.BaseType.GetGenericTypeDefinition() == typeof(ComplexTypeConfiguration)));

        foreach (Type type in entityTypeConfigurationTypes)
        {
            // The 'Add' method of the DbModelBuilder accepts both "ComplexTypeConfiguration<TComplexType>" or "EntityTypeConfiguration<TEntityType>".
            // At first glance, the type of configurator is not know during compile time.
            // The dynamic type enables the operations in which it occurs to bypass compile-time type checking, in other words,
            // types are not resolved or checked by the compiler; instead, these operations are resolved at run time (when the explicit configuration
            // type is known).
            // Reference: http://msdn.microsoft.com/en-us/library/dd264741.aspx
            dynamic entityTypeConfigurationInstance = Activator.CreateInstance(type);
            modelBuilder.Configurations.Add(entityTypeConfigurationInstance);
        }
    }
}

De esta manera incluso podemos modificar el acceso a las clases de configuración como internal; sin embargo será entonces necesario ignorar el warning CA1812 del Code Analyzer (la cual podemos suprimir de forma segura ya que sabemos que sí son consumidas por Reflection).

Actualización 21/01/2013: En la nueva versión del Entity Framework, la 6.0, encontraremos en la clase ConfigurationRegistrar un nuevo método llamado AddFromAssembly que nos permitirá hacer exactamente lo que se describe en este artículo. La clase ConfigurationRegistrar habitualmente la accedemos desde el método/evento OnModelCreating a través de la propiedad Configurations.

Referencias: