in Architettura Software, Informatica

Entity Framework Core, ApplyConfigurationsFromAssembly multi context

Nello sviluppo di progetti con Entity Framework Core risulta molto comodo gestire le configurazioni delle singole entity a livello di file di configurazione, ed indicare a Entity Framework di caricarle a partire dall’assembly in cui sono posizionati.

Consideriamo ad esempio la classe

public class UserBuilder : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.ToTable("Users");
        builder.Property(e => e.Name).HasMaxLength(15);
    }
}

in cui viene definita la configurazione per l’entity Users:

  • viene definito il binding sulla tabella Users
  • viene definita la lunghezza massima che dovrà avere la property Name

Il poter suddividere la configurazione in diversi file, rende il codice sicuramente molto piu leggibile e modificabile, rispetto all’utilizzo dell’intera configurazione all’interno del metodo onModelCreating del Context.

Questo tipo di approccio prende il nome di Separate Configuration ed è disponibile a partire dalla versione 2.0 di Entity Framework Core.

Inoltre, è possibile gestire facilmente la registrazione di tutte le configurazioni utilizzando:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    if (modelBuilder is null)
    {
        throw new ArgumentNullException(nameof(modelBuilder));
    }

    modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
}

dove alla riga 8 viene specificato di leggere i file di configurazione all’interno dell’assembly. Questa configurazione ci permette di non doverci preoccupare di dover effettuare registrazioni di classi ed altro, perchè vengono effettuate direttamente in fase di compilazione.

Gestione multiContext

Questa è sicuramente la soluzione ottimale quando ci troviamo nella condizione di avere un unico Context all’interno dell’assembly. Ma come possiamo gestire multi-context all’interno dello stessso assembly? E’ necessario indicare all’interno dell’ onModelCreating quali entity (e quindi quali file di configurazione leggere) per ogni singolo context. Quello che ci serve è in pratica un filtro. Partendo della documentazione del metodo ApplyConfigurationsFromAssembly, che possiamo trovare qui, si può vedere che il metodo accetta un secondo parametro opzionale che riguarda proprio un filtro (il predicate).

Supponiamo, ad esempio, che per il context Contex001 tutte le entity ereditino da una classe base chiamata BaseEntity. Le entity del Context002 non ereditano da questa classe. La classe BaseEntity potrebbe essere semplicemente una classe all’interno della quale vengono definite le properties LastModifiedOn, CreatedOn e DeletedOn (data/ora della modifica/creazione/cancellazione) di un record. L’entity User che abbiamo utilizzato in precedenza è quindi definita in questo modo:

public class User : BaseEntity
{
    public string Name { get; set; }
}

Quello che dobbiamo implementare è un predicate che effettua la scansione di tutte le entities presenti nell’assembly e per ciascun tipo dovrà andare a verificare se implementa l’interfaccia IEntityTypeConfiguration e con T che eredita da BaseEntity. Questa sarà la condizione che dovremo andare ad impostare all’interno del predicate.

Creazione del Predicate

Il predicate:

    t => t.GetInterfaces().Any(i => 
                i.IsGenericType &&
                i.GetGenericTypeDefinition() == typeof(IEntityTypeConfiguration<>) &&
                typeof(BaseEntity).IsAssignableFrom(i.GenericTypeArguments[0]))

Come si può osservare viene effettuata una scansione di tutte le configurazioni (che ereditano da IEntityTypeConfiguration e che hanno come tipo T BaseEntity. A questo punto vengo assegnate.

Quindi all’interno del metodo onModelCreating del Context001 dovremo inserire:

builder.ApplyConfigurationsFromAssembly(
    Assembly.GetExecutingAssembly(), 
    t => t.GetInterfaces().Any(i => 
                i.IsGenericType &&
                i.GetGenericTypeDefinition() == typeof(IEntityTypeConfiguration<>) &&
                typeof(BaseEntity).IsAssignableFrom(i.GenericTypeArguments[0]))
);

Questo tipo di soluzione può risultare molto comoda all’interno di progetti in cui dobbiamo gestire piu context, indipendenti tra loro ma con Entities definite all’interno dello stesso assembly. Sicuramente un punto importante riguarda la definizione della regola del predicate, che non sempre può essere cosi immediata da comporre .

Questo tipo di approccio, comporta sicuramente un rallentamento inziale nella creazione del modello iniziale sul db (dovendo effettuare la scansione di tutte le Entities definite all’interno dell’assembly).

Un’altro punto importante riguarda i Context che sono già all’interno di un processo di migrazione. Potrebbe essere necessario escluderli dalle migrazione di altri Context. In questo caso può essere utile inserire all’interno di onModelCreating anche la seguente riga:

 modelBuilder.Entity<SomeEntity().Metadata.SetIsTableExcludedFromMigrations(true);

In soluzioni più complesse preferisco mantenere la separazione di Context che devono utilizzare le migrazioni all’interno di assembly separati.