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.
Pingback: Global Conventions in Entity Framework Code First v 4.1 « Sergey Barskiy's Blog
Excellent work – just what we need here. EF 4.1. is certainly powerful but this helps really complete what I need
@Liam Grossmann – I just put up anothre post that does global conventions without attributes. Cuts down on fluent API even more.
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?
You probably do not have Entity Framework CodeFirst (EntityFramework.dll) installed. You can install it by searching Microsoft Downloads or using NuGet package.
Pingback: Entity Framework 4.1 Code First learning path (a)
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
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.
Update 10/16/2011:
I formalized my ideas on conventions into a CodePlex project – http://efcodefirstextras.codeplex.com/. The project also includes repository and migrations.
@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!
Anyone please vote here: http://data.uservoice.com/forums/72025-ado-net-entity-framework-ef-feature-suggestions/suggestions/1155529-ability-to-plug-in-custom-conventions-for-schema-g
Anyway to do this in EF 5.0?
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
@Katerina
Conventions are coming back in EF 6.0. You can already pull the latest from Code Plex if you ‘d like.
@Sergey Barskiy
That I saw on the EF 6 roadmap. Unfortunately I also use DevForce witch is not yet compatible with EF 6.
Would be great to get this in 5 though.
-Kat