Custom Entity Framework Code First Convention for Discriminator Values

Since version 6, Entity Framework Code First allows the injection of custom conventions. These conventions define rules that will be applied by default to all mapped entities and properties, unless explicitly changed.

The conventions API includes a couple of interfaces: IConvention (marker only, should always be included), IConceptualModelConvention<T> (for the conceptual space of the model) and IStoreModelConvention<T> (for the store, or physical, side of the model). Worthy of mention, there is also a convenience class, Convention, that allows access to all mapped types and properties and doesn’t override any of the other conventions, and also TypeAttributeConfigurationConvention<T>, for tying a convention to a custom attribute. Some of the included attributes leverage these interfaces to configure some aspects of the mappings at design time, other configuration needs to be done explicitly in an override of OnModelCreating.

Entity Framework permits using a column for distinguishing between different types, when the Table Per Class Hierarchy / Single Table Inheritance pattern (please see Entity Framework Code First Inheritance for more information) is used for mapping a hierarchy of classes to a single table, as part of “soft delete” solutions, or, less known, for differentiating between multiple tenants. This column is called a discriminator.

In order to configure an entity to use a discriminator column, there is no out of the box attribute, so we must resort to code configuration:

   1: protected override void OnModelCreating(DbModelBuilder modelBuilder)
   2: {
   3:     modelBuilder.Entity<MyMultiTenantEntity>().Map(m => m.Requires("tenant_id").HasValue("first_tenant"));
   4:  
   5:     base.OnModelCreating(modelBuilder);
   6: }

Because there’s really no need to keep repeating this code, let’s implement an attribute for indicating a discriminator column in an entity:

   1: [Serializable]
   2: [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
   3: public sealed class DiscriminatorAttribute : Attribute
   4: {
   5:     public DiscriminatorAttribute(String columnName, Object discriminatorValue)
   6:     {
   7:         this.ColumnName = columnName;
   8:         this.DiscriminatorValue = discriminatorValue;
   9:     }
  10:  
  11:     public String ColumnName { get; private set; }
  12:  
  13:     public Object DiscriminatorValue { get; private set; }
  14:  
  15:     public override Boolean Equals(Object obj)
  16:     {
  17:         var other = obj as DiscriminatorAttribute;
  18:  
  19:         if (other == null)
  20:         {
  21:             return (false);
  22:         }
  23:  
  24:         return ((this.ColumnName == other.ColumnName) && (Object.Equals(this.DiscriminatorValue, other.DiscriminatorValue) == true));
  25:     }
  26:  
  27:     public override Int32 GetHashCode()
  28:     {
  29:         return (String.Concat(this.ColumnName, ":", this.DiscriminatorValue).GetHashCode());
  30:     }
  31: }

As you can see, the DiscriminatorAttribute attribute can only be applied to a class, at most once. This makes sense, because most likely you will only have a single discriminator column per entity:

   1: [Discriminator("tenant_id", "first_tenant")]
   2: public class MyMultiTenantEntity
   3: {
   4:     //...
   5: }

You need to specify both a column name and a discriminator value, which can be of any type, usually, a string or an integer.

Now, let’s write a custom convention that knows how to handle our custom attribute and perform the mapping:

WARNING! DYNAMICS AND REFLECTION AHEAD!

PROCEED WITH CAUTION!

   1: public sealed class DiscriminatorConvention : TypeAttributeConfigurationConvention<DiscriminatorAttribute>
   2: {
   3:     private static readonly MethodInfo entityMethod = typeof(DbModelBuilder).GetMethod("Entity");
   4:     private static readonly MethodInfo hasValueMethod = typeof(ValueConditionConfiguration).GetMethods().Single(m => (m.Name == "HasValue") && (m.IsGenericMethod == false));
   5:  
   6:     private readonly DbModelBuilder modelBuilder;
   7:     private readonly ISet<Type> types = new HashSet<Type>();
   8:  
   9:     public DiscriminatorConvention(DbModelBuilder modelBuilder)
  10:     {
  11:         this.modelBuilder = modelBuilder;
  12:     }
  13:  
  14:     public override void Apply(ConventionTypeConfiguration configuration, DiscriminatorAttribute attribute)
  15:     {
  16:         if (this.types.Contains(configuration.ClrType) == true)
  17:         {
  18:             //if the type has already been processed, bail out
  19:             return;
  20:         }
  21:  
  22:         //add the type to the list of processed types
  23:         this.types.Add(configuration.ClrType);
  24:  
  25:         dynamic entity = entityMethod.MakeGenericMethod(configuration.ClrType).Invoke(modelBuilder, null);
  26:  
  27:         Action<dynamic> action = arg =>
  28:         {
  29:             var valueConditionConfiguration = arg.Requires(attribute.ColumnName);
  30:             hasValueMethod.Invoke(valueConditionConfiguration, new Object[] { attribute.DiscriminatorValue });
  31:         };
  32:  
  33:         entity.Map(action);
  34:     }
  35: }

This class uses a bit of dynamics and reflection because types are not known at compile time, and hence we cannot use generics directly. Because the Apply method will be called multiple times, we need to keep track of which entities have already been processed by this convention, so as to avoid reprocessing them. We need to pass it the instance of DbModelBuilder, because otherwise our custom convention would have no way to apply the mapping, but I think it is a reasonable trade off.

Et voilà! In order to make use of it, we need to register the convention in OnModelCreating:

   1: protected override void OnModelCreating(DbModelBuilder modelBuilder)
   2: {
   3:     modelBuilder.Conventions.Add(new DiscriminatorConvention(modelBuilder));
   4:  
   5:     base.OnModelCreating(modelBuilder);
   6: }

And that’s it! Happy conventions! Winking smile




                             

No Comments

Add a Comment

As it will appear on the website

Not displayed

Your website