Global Conventions in Entity Framework Code First v 4.1

In my previous post I showed how to implement custom conventions in absence of the same feature in Entity Framework 4.1.  Today I am going to expand on the same topic and try to create some global contentions.

Let me elaborate a bit on a problem I am trying to resolve.  For example, if I create a property of type decimal on a class that is used to create a set of entities in DbContext, it would result in the decimal(18,2) column definition in the database.  What if I would like my default to be (8,4) unless specified otherwise?  You can see my problem.  If I have 100 tables in my database all with 10 decimal fields, I would have to type in 1000 decimal column definitions using fluent API.   This is way too much typing to my taste. 

Here is the solution I would like to purse.  I am going to expand on my previous attribute based conventions and create a global, attribute-less convention.

First I am going to refactor my previous code and convert attribute specific conventions to generic conventions based on IConvention

Here is my new code to process all conventions and add one convention:

        /// <summary>

        /// Add one convention

        /// </summary>

        /// <param name="convention">Convention to add</param>

        protected void AddConvention(IConvention convention)

        {

            conventions.Add(convention);

        }

 

        /// <summary>

        /// Process conventions

        /// </summary>

        /// <param name="modelBuilder">Model builder</param>

        protected virtual void ProcessAddedConventions(DbModelBuilder modelBuilder)

        {

            if (conventions.Count > 0)

            {

                // poulate reflection data

                PopulateSetMetadata();

                // run through all added conventions

                conventions.ForEach(convention =>

                {

                    if (convention is IAttributeConvention)

                    {

                        ProcessAttributeBasedConvention(modelBuilder, convention as IAttributeConvention);

                    }

                   

                });

            }

        }

 

Now, I am going to write a new method to process global conventions.  I am going to extensively comment the code below:

    /// <summary>

    /// Process global conventions

    /// </summary>

    /// <param name="modelBuilder">Model builder</param>

    /// <param name="convention">One global convention to process</param>

    private void ProcessGlobalConvention(DbModelBuilder modelBuilder, IGlobalConvention convention)

    {

      var setMetadata = dbSetMetadata[this.GetType().AssemblyQualifiedName];

      // run through DbSets in current context

      setMetadata.ForEach(set =>

      {

        //run through properties in each DbSet<T> for class of type T

        set.DbSetItemProperties.ToList().ForEach(prop =>

        {

          // get type of property that matches current convention

          Type targetType = GetMatchingTypeForConfiguration(convention.PropertyConfigurationType);

          // make sure this type matched property type

          if (prop.PropertyInfo.PropertyType == targetType)

          {

            // Get entity method in ModuleBuilder

            // we are trying to get to the point of expressing the following

            //modelBuilder.Entity<Person>().Property(a => a.Name).IsMaxLength() for example

            var setMethod = modelBuilder.GetType()

                .GetMethod("Entity", BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);

            // one we have Entity method, we have to add generic parameters to get to Entity<T>

            var genericSetMethod = setMethod.MakeGenericMethod(new Type[] { set.ItemType });

            // Get an instance of EntityTypeConfiguration<T>

            var entityInstance = genericSetMethod.Invoke(modelBuilder, null);

 

            //Get methods of EntityTypeConfiguration<T>

            var propertyAccessors = entityInstance.GetType().GetMethods(

                BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy).ToList();

 

            // we are looking for Property method that returns PropertyConfiguration

            // that is used in current convention

            var propertyMethod =

                propertyAccessors.Where(oneProperty =>

                    oneProperty.ReturnType == convention.PropertyConfigurationType).FirstOrDefault();

 

            //Get method handle in order to build the expression

            // example: (a => a.Name)

            var expressionGetMethod = GetPropertyExpressionMethodHandle();

 

            //Create lamda expression by making expression method that takes two generic parameters

            // one for class, the other for property type

            var genericExpressionMethod = expressionGetMethod

                .MakeGenericMethod(new Type[] { prop.PropertyInfo.DeclaringType, prop.PropertyInfo.PropertyType });

 

            //FInally, get lamda expression it self

            // example: (a => a.Name)

            var propertyExpression = genericExpressionMethod.Invoke(null, new object[] { prop.PropertyInfo });

 

            //Not get an instance of PrimitivePropertyConfiguration by 
            // infoking EntityTypeConfiguration<T>’s

            // Property() method

            var config = propertyMethod

                .Invoke(entityInstance, new object[] { propertyExpression }) as PrimitivePropertyConfiguration;

 

            //Finally, pass this configuration and attribute into the convention

            convention.ApplyConfiguration(prop.PropertyInfo, config);

          }

        });

      });

    }

 

Just like in attribute based convention method, I am running through all the properties for all the entities.  However, first step I am doing is making sure that the convention I am applying is matching the property type.  I am using the following method to get property type based on convention.

    /// <summary>

    /// Determine what property type should be used for a specific convention

    /// </summary>

    /// <param name="propertyConfigurationType">

    /// Type of PrimitivePropertyConfiguration to process

    /// </param>

    /// <returns>

    /// Property type that should be used with current convention

    /// </returns>

    private Type GetMatchingTypeForConfiguration(Type propertyConfigurationType)

    {

      if (propertyConfigurationType == typeof(DecimalPropertyConfiguration))

      {

        return typeof(decimal);

      }

      if (propertyConfigurationType == typeof(StringPropertyConfiguration))

      {

        return typeof(string);

      }

      if (propertyConfigurationType == typeof(DateTimePropertyConfiguration))

      {

        return typeof(DateTime);

      }

      if (propertyConfigurationType == typeof(BinaryPropertyConfiguration))

      {

        return typeof(byte[]);

      }

      else

      {

        return typeof(object);

      }

    }

 

For example, I only want to apply string based configuration to string properties.  The rest of the code on apply global conventions methods is just reflection code aimed to obtain property configuration and call Apply method of the global convention.  Here is a sample implementation for this convention, making all decimal properties decimal(8,4):

using System;

using System.Data.Entity.ModelConfiguration.Configuration;

using System.Reflection;

using EFCodeFirstConventions;

 

namespace EFCodeFirstConventionsConsole

{

  public class GenericDecimalConvention :

    GlobalConfigurationConvention<MemberInfo, DecimalPropertyConfiguration>

  {

    protected override void Apply(

      MemberInfo memberInfo,

      DecimalPropertyConfiguration propertyConfiguration)

    {

      propertyConfiguration.HasPrecision(8, 4);

    }

  }

}

 

If I add this convention to my extended context, I will make my decimals size (8,4) unless specified otherwise.  In order to do so, I am processing global conventions prior to attribute based conventions:

    /// <summary>

    /// Process conventions

    /// </summary>

    /// <param name="modelBuilder">Model builder</param>

    protected virtual void ProcessAddedConventions(DbModelBuilder modelBuilder)

    {

      if (conventions.Count > 0)

      {

        // poulate reflection data

        PopulateSetMetadata();

        // run through all global added conventions

        conventions.ForEach(convention =>

        {

          if (convention is IGlobalConvention)

          {

            ProcessGlobalConvention(modelBuilder, convention as IGlobalConvention);

          }

        });

        // run through attribute based conventions

        conventions.ForEach(convention =>

        {

          if (convention is IAttributeConvention)

          {

            ProcessAttributeBasedConvention(modelBuilder, convention as IAttributeConvention);

          }

        });

      }

    }

 

Now, let’s do another convention that is based on property names.  For example, I want to have all properties that have word Percent in them to be set as decimal(4,2).  I am just going to write a global convention for that:

using System.Data.Entity.ModelConfiguration.Configuration;

using System.Reflection;

using EFCodeFirstConventions;

 

namespace EFCodeFirstConventionsConsole

{

  public class PercentConvention :

    GlobalConfigurationConvention<MemberInfo, DecimalPropertyConfiguration>

  {

    protected override void Apply(

      MemberInfo memberInfo,

      DecimalPropertyConfiguration propertyConfiguration)

    {

      if (memberInfo.Name.ToUpper().Contains("PERCENT"))

      {

        propertyConfiguration.HasPrecision(4, 2);

      }

    }

  }

}

 

 

Super easy, now that I have my framework setup.  In this convention I am using the fact that I have MethodInfo as a parameter, so I can apply name based global conventions!  This cuts down on amount of fluent API code I have to write tremendously.  One thing to remember, I have to add conventions to my extended context in certain order to ensure they do not overwrite each other.

 

    public class CustomContext : ExtendedDbContext

    {

      public IDbSet<Person> Perosns { get; set; }

 

      protected override void AddConventions()

      {

        AddConvention(new EtendedStringConvention());

        AddConvention(new GenericDecimalConvention());

        AddConvention(new PercentConvention());

      }

 

    }

 

Here is my test Person class I am using:

using System.ComponentModel.DataAnnotations;

 

namespace EFCodeFirstConventionsConsole

{

  public class Person

  {

    [Key]

    public int PersonID { get; set; }

 

    [ExtendedString(10, 200, false)]

    public string Name { get; set; }

 

    public bool IsActive { get; set; }

 

    public decimal GenericDecimal { get; set; }

 

    public decimal Percent { get; set; }

  }

}

 

You can download updated project here.

 

Thank you.

9 Comments

  1. This is fantastic!

    I did have a problem using this code on nullable fields (ex. decimal? Percent), since the expression return types don’t match when creating the PrimitivePropertyConfiguration inside Process*Convention methods which throws an exception.

    To work around this, instead of selecting the first property accessor method…

    var propertyMethod = propertyAccessors.Where(oneProperty => oneProperty.ReturnType == convention.PropertyConfigurationType).FirstOrDefault();

    …I loop through all of those that match the PropertyConfigurationType and try/catch the propertyMethod.Invoke and convention.ApplyConfiguration until one works. Not very elegant, but I wasn’t sure how to identify the nullable version of the propertyMethod in the propertyAccessors collection.

  2. first of all thanks for this work, its great but I am having a problem. In my Poco classes there is always a base abstract class like PersonBase, and the final Poco class just extends on it (the reason for this is because I have PersonView which will derive from PersonBase), I am getting an exception related to type conversion from base class to the entity class itself, I don’t have a fix for it yet though, I am not so good with reflection:), so any help is appreciated.

    public abstract class PersonBase
    {
    [Key]
    public int PersonID { get; set; }

    [ExtendedString(10, 200, false)]
    public string Name { get; set; }

    public bool IsActive { get; set; }

    public decimal GenericDecimal { get; set; }

    public decimal? GenericNullableDecimal { get; set; }

    public decimal Percent { get; set; }

    [ExtendedDecimal(8,2)]
    public decimal AttrbuteBasedDecimal { get; set; }

    [ExtendedDecimal(6, 4)]
    public decimal? AttrbuteBasedNullableDecimal { get; set; }
    }
    public class Person : PersonBase
    { }

  3. var genericExpressionMethod = expressionGetMethod
    .MakeGenericMethod(new Type[] { prop.PropertyInfo.DeclaringType, prop.PropertyInfo.PropertyType });

    Change “DeclaringType” to “ReflectedType” as below, that should work.

    var genericExpressionMethod = expressionGetMethod
    .MakeGenericMethod(new Type[] { prop.PropertyInfo.ReflectedType, prop.PropertyInfo.PropertyType });

  4. Hello, Hazim,
    I have not tried inherited classes, I am glad you took a stab at the code. It would make sense that Declaring type would be your base class. Thanks for participating and helping other folks.

Leave a Reply to Sergey Barskiy Cancel reply

Your email address will not be published. Required fields are marked *