Custom Conventions in Entity Framework Code First v 4.1

As you know, in preview version of Entity Framework code first existed concept of custom pluggable conventions that would allow the developer to avoid using large amounts of code in OnModelCreating method.  Typically, if one would like to keep entity classes free of entity framework references and possibly of Data Annotations references, fluent API available in ModelBuilder class can be used to configure entire model.  However, if you have reasonably large model, you will end up with thousands of lined of code in OnModelCreating  method of you custom DbContext.

Custom conventions would allow you to avoid this situation.  You would combine custom attributes with pluggable conventions that would pass the attribute value into your convention along with appropriate instance PropertyConfiguration, such as StringPropertyConfiguration.

Unfortunately, this functionality was removed from release candidate build and also will not be available in final build of EF 4.1 sometimes later this year.  I heard some complaints regarding this issue, and decided to write something compatible.

In this post I will describe how I went about this process, illustrating how to support primitive properties, such as string properties. 

I borrowed the concept from CTP 5.  My base class of custom convention is very similar to Microsoft one.

I started with the following interface:

using System;

using System.Data.Entity.ModelConfiguration.Configuration;

using System.Data.Entity.ModelConfiguration.Conventions;

using System.Reflection;

 

namespace EFCodeFirstConventions

{

    public interface IAttributeConvention : IConvention

    {

        void ApplyConfiguration(

            MemberInfo memberInfo,

            PrimitivePropertyConfiguration propertyConfiguration,

            Attribute attrribute);

 

        Type PropertyConfigurationType { get; }

        Type AttributeType { get; }

    }

}

 

 

IConvention class still exists in release candidate.  The idea behind the interface is as following.  I will call ApplyConfiguration method, passing the instance of attribute that my property inside entity class is decorated with along with PropertyInfo object (to provide additional information to the convention class).  Here is my implementation in an abstract class:

using System;

using System.Data.Entity.ModelConfiguration.Configuration;

using System.Reflection;

 

namespace EFCodeFirstConventions

{

    public abstract class AttributeConfigurationConvention<TMemberInfo, TPropertyConfiguration, TAttribute>

        : IAttributeConvention

        where TMemberInfo : MemberInfo

        where TPropertyConfiguration : PrimitivePropertyConfiguration

        where TAttribute : Attribute

    {

 

        public void ApplyConfiguration(

            MemberInfo memberInfo,

            PrimitivePropertyConfiguration propertyConfiguration,

            Attribute attribute)

        {

            Apply((TMemberInfo)memberInfo, (TPropertyConfiguration)propertyConfiguration, (TAttribute)attribute);

        }

 

        protected abstract void Apply(

            TMemberInfo memberInfo,

            TPropertyConfiguration propertyConfiguration,

            TAttribute attrribute);

 

 

        public Type PropertyConfigurationType

        {

            get { return typeof(TPropertyConfiguration); }

        }

 

        public Type AttributeType

        {

            get { return typeof(TAttribute); }

        }

 

    }

 

 

 

}

 

I introduced Apply method in my base convention class in order to make cleaner, strongly typed API in derived classes.  I also make convention strongly typed via generics, again in order to easy the pain of implementation.

Here is a sample implementation:

using System.Data.Entity.ModelConfiguration.Configuration;

using System.Reflection;

using EFCodeFirstConventions;

 

namespace EFCodeFirstConventionsConsole

{

    public class EtendedStringConvention :

        AttributeConfigurationConvention<MemberInfo, StringPropertyConfiguration, ExtendedStringAttribute>

    {

 

        protected override void Apply(

            MemberInfo memberInfo,

            StringPropertyConfiguration propertyConfiguration,

            ExtendedStringAttribute attrribute)

        {

            propertyConfiguration.IsUnicode(attrribute.IsUnicode);

            if (attrribute.MaxLength == int.MaxValue || attrribute.MaxLength == -1)

            {

                propertyConfiguration.IsMaxLength();

            }

            else if (attrribute.MaxLength == attrribute.MinLength && attrribute.MinLength > 0)

            {

                propertyConfiguration.IsMaxLength();

                propertyConfiguration.IsFixedLength();

                propertyConfiguration.HasMaxLength(attrribute.MaxLength);

            }

            else

            {

                propertyConfiguration.HasMaxLength(attrribute.MaxLength);

            }

        }

    }

}

 

 

As you can see, once you are in your Apply method if you have clean code that allows you to configure StringPropertyConfiguration based on ExtendedStringAttribute value.

using System;

 

namespace EFCodeFirstConventionsConsole

{

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]

    public class ExtendedStringAttribute : Attribute

    {

        public ExtendedStringAttribute()

            : this(isUnicode: true)

        {

 

        }

 

        public ExtendedStringAttribute(

            int minLength = 0, int maxLength = int.MaxValue, bool isUnicode = true)

        {

            MinLength = minLength;

            MaxLength = maxLength;

            IsUnicode = isUnicode;

        }

 

        public int MinLength { get; private set; }

        public int MaxLength { get; private set; }

        public bool IsUnicode { get; private set; }

    }

}

 

Code above is my attribute.  I am using it as follows:

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; }

 

    }

}

 

Easy as pie.  Now, here is the ugly code with plenty of reflection that supports this nice feature.  I documented the class itself very thoroughly to ensure that one can understand what I wrote.

using System;

using System.Collections.Generic;

using System.Data.Entity;

using System.Data.Entity.ModelConfiguration.Configuration;

using System.Linq;

using System.Linq.Expressions;

using System.Reflection;

using EFCodeFirstConventions.Reflection;

 

namespace EFCodeFirstConventions

{

    public abstract class ExtendedDbContext : DbContext

    {

        protected override void OnModelCreating(DbModelBuilder modelBuilder)

        {

            base.OnModelCreating(modelBuilder);

            // call derived class to add conventions

            AddConventions();

            // now process conventions

            ProcessAddedConventions(modelBuilder);

        }

 

        /// <summary>

        /// Force implementation via astract class

        /// </summary>

        protected abstract void AddConventions();

 

        // conventsions saved here

        private List<IAttributeConvention> conventions = new List<IAttributeConvention>();

 

        //reflrecion data about DbContext, its sets, properties and attributes

        private static Dictionary<string, List<DbSetMetadata>> dbSetMetadata =

            new Dictionary<string, List<DbSetMetadata>>();

 

        private static object locker = new object();

 

        /// <summary>

        /// Add one convention

        /// </summary>

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

        protected void AddConvention(IAttributeConvention 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 =>

                {

                    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 attribute that matches convention

                                    var data = prop.DbSetItemAttributes

                                        .Where(attr => attr.Attribute.GetType() == convention.AttributeType).FirstOrDefault();

 

                                    // this class’s property has the attribute

                                    if (data != null)

                                    {

                                        // 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 == 
.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, data.Attribute);

                                    }

 

                                });

                        });

                });

            }

        }

 

        /// <summary>

        /// Locate member info handle for GetPropertyExpression method by iterating through

        /// class hierarchy

        /// </summary>

        /// <returns>MemberInfo handle for GetPropertyExpression method</returns>

        private MethodInfo GetPropertyExpressionMethodHandle()

        {

            MethodInfo returnValue = null;

            Type currentType = this.GetType();

            while (returnValue == null)

            {

                returnValue = currentType

                                .GetMethod("GetPropertyExpression",

                                BindingFlags.NonPublic | BindingFlags.FlattenHierarchy 
|
BindingFlags
.Static);

                if (returnValue == null)

                {

                    currentType = currentType.BaseType;

                    if (currentType == null)

                    {

                        break;

                    }

                }

            }

            return returnValue;

        }

 

        /// <summary>

        /// Create Expression that can access property on a class.  You would typically write it as

        /// (p=>p.Name)

        /// In our case we are using Expression to build the same expression

        /// </summary>

        /// <typeparam name="TClass">Class type that is owning the property in question</typeparam>

        /// <typeparam name="TProperty">Property type</typeparam>

        /// <param name="property">PropertyInfo object for property in question</param>

        /// <returns>Expression that returns the property, such as (p=>p.Name)</returns>

        private static Expression<Func<TClass, TProperty>> GetPropertyExpression<TClass, TProperty>(PropertyInfo property)

        {

            //  Create {p=> portion of the Epxression in example (p=>p.Name)

            var objectExpression = Expression.Parameter(property.DeclaringType, "param");

            // create property expression – .Name for example

            var propertyExpression = Expression.Property(objectExpression, property);

            //Create lambda expression from two parts

            var returnValue = Expression.Lambda<Func<TClass, TProperty>>(propertyExpression, objectExpression);

            return returnValue;

        }

 

        /// <summary>

        /// RUn through DbContnxt sets and save reflection data in a dictionary

        /// </summary>

        private void PopulateSetMetadata()

        {

            if (!dbSetMetadata.ContainsKey(this.GetType().AssemblyQualifiedName))

            {

                lock (locker)

                {

                    if (!dbSetMetadata.ContainsKey(this.GetType().AssemblyQualifiedName))

                    {

                        var props = this.GetType().GetProperties(

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

                        List<DbSetMetadata> sets = new List<DbSetMetadata>();

                        props.ForEach(one =>

                        {

                            //Filter out db sets

                            if (one.PropertyType.IsGenericType &&

                                (one.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>) ||

                                one.PropertyType.GetGenericTypeDefinition() == typeof(IDbSet<>)))

                            {

                                sets.Add(new DbSetMetadata(one.PropertyType.GetGenericArguments().First(), one));

                            }

                        });

                        // add this context to diutionary

                        dbSetMetadata.Add(this.GetType().AssemblyQualifiedName, sets);

                    }

                }

            }

        }

    }

}

 

That is all.  Feel free to use the code.  Please contact me (use Contact page) if you would like me to create CodePlex project for this sample of extend it further.

Here is what my inherited sample DbContext looks like:

        public class CustomContext: ExtendedDbContext

        {

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

 

            protected override void AddConventions()

            {

                AddConvention(new EtendedStringConvention());

            }

 

        }

 

 

You can download full project here.

Thanks.

15 Comments

  1. Pingback: Global Conventions in Entity Framework Code First v 4.1 « Sergey Barskiy's Blog

  2. Sir i have been using the same using statement like :
    using System.Data.Entity.ModelConfiguration.Conventions;
    i am getting the error that this type or namespace ‘Model Configuration’ does not exist in System.Data.Entity even after i have installed the Entity framework ,and added it to the references of my application.May i know the reason why i am getting this error?

  3. Pingback: Entity Framework 4.1 Code First learning path (a)

  4. Hello Sergey,

    I have few base codes used EF 4.0 and I have implemented AttributeConfigurationConvention for Decimal Precision and Scale. when I install EF 4.1 I project was crushed. Build error message as “In accessible due to its protection level etc…” Does any namespace changes?

    regards

  5. Not that I am aware of. This message typically means that some method or property is expected to be public, but is private or protected. If you email me a sample project, I will take a look. My email address is on the contact page.

  6. @Sergey, can you make your DbContext available via NuGet?
    It’s a lot of code to keep track of.

    I don’t like the fluent API apprach at all. It makes my code real nasty. I prefer attributes a lot better.
    I wish they would make all the conventions available in either atts or fluent by choice.

    Thank you!

  7. With EF now Open source. Has anyone simply turned this feature back on? If I had alot of spare time I might go investigate this, but alas, its only tuesday and I am almost to 30 hours already.

    Thanks,
    Kat

Leave a Reply to Katerina Cancel reply

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