EF Feature CTP5: Pluggable Conventions

 


The information in this post is out of date.

Visit msdn.com/data/ef for the latest information on current and past releases of EF.

The pluggable conventions feature was removed from the EF4.1 release. You can track this feature at https://entityframework.codeplex.com/workitem/61.


 

We have released Entity Framework Feature Community Technology Preview 5 (CTP5) . Feature CTP5 contains a preview of new features that we are planning to release as a stand-alone package in Q1 of 2011 and would like to get your feedback on. Feature CTP5 builds on top of the existing Entity Framework 4 (EF4) functionality that shipped with .NET Framework 4.0 and Visual Studio 2010 and is an evolution of our previous CTPs.

If you aren’t familiar with Code First then you should read the Code First Walkthrough before tackling this post.

Code First includes a set of simple, model-wide behaviors that provide sensible configuration defaults for the parts of your model that have not been explicitly configured using Data Annotations or the Fluent API. These default behaviors are referred to as Conventions. One of the most commonly requested features is the ability to add custom conventions or switch off some of the default conventions. Based on your feedback we’ve bumped up the priority of this feature and CTP5 includes an early preview of “Pluggable Conventions”.

 

Limitations (Please Read)

The functionality included in CTP5 is an early preview that was included to get your feedback. There are a number of rough edges and the API surface is likely to change for the RTM release.

The Pluggable Conventions feature we have implemented so far allows you to perform configuration based on information obtained by reflection from the types and members in your model. It does not allow you to read information about the shape of your model. This does impose some restrictions; for example you can-not read the model to find out if a given property is a foreign key. These restrictions are something we will remove in the future but not before the initial RTM in 2011.

 

The Model

Here is a simple console application that uses Code First to perform data access. If we run the application we’ll get an exception because Code First can’t find a property to use as the primary key for the two entities. This is because the primary key properties don’t conform to the default conventions.

 using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Database;
 namespace ConventionSample
{
    class Program
    {
        static void Main(string[] args)
        {
            DbDatabase.SetInitializer(
                new DropCreateDatabaseIfModelChanges<BlogContext>());

            using (var ctx = new BlogContext())
            {
                ctx.Blogs.Add(new Blog {  Name = "Test Blog" });
                ctx.SaveChanges();

                foreach (var blog in ctx.Blogs)
                {
                    System.Console.WriteLine("{0}: {1}", blog.BlogKey, blog.Name);
                }
            }
        }
    }

    public class BlogContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    }

    public class Blog
    {
        public int BlogKey { get; set; }
        public string Name { get; set; }

        public virtual ICollection<Post> Posts { get; set; }
    }

    public class Post
    {
        public int PostKey { get; set; }
        public string Title { get; set; }
        public string Abstract { get; set; }
        public string Content { get; set; }

        public int BlogKey { get; set; }
        public virtual Blog Blog { get; set; }
    }
}
  

Our First Custom Convention

We could just use DataAnnotations or the Fluent API to configure the primary key for each of these entities but if we have a larger model this is going to get very repetitive. It would be better if we could just tell Code First that any property named ‘<TypeName>Key’ is the primary key. We can now do this by creating a custom convention:

 using System;
using System.Data.Entity.ModelConfiguration.Configuration.Types;
using System.Data.Entity.ModelConfiguration.Conventions.Configuration;
 public class MyKeyConvention :
IConfigurationConvention<Type, EntityTypeConfiguration>
{
    public void Apply(Type typeInfo, Func<EntityTypeConfiguration> configuration)
    {
        var pk = typeInfo.GetProperty(typeInfo.Name + "Key");
        if (pk != null)
        {
            configuration().Key(pk);
        }
    }
}
  
 We then register our convention by overriding OnModelCreating in our derived context:
 public class BlogContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Add<MyKeyConvention>();
    }
}
  

A Closer Look at IConfigurationConvention

IConfigurationConvention allows us to implement a convention that is passed some reflection information and a configuration object.

Why Not Just IConvention?

You may be wondering why it’s not just IConvention? As mentioned earlier this first part of pluggable conventions allows you to write simple conventions using reflection information but does not allow you to read any information about the model. Later on (after our first RTM) we intend to expose the other half of our pluggable convention story that allows you to read from the model and perform much more granular changes.

Why Func<EntityTypeConfiguration>?

We want to avoid needlessly creating configurations for every type and property in your model, the configuration is lazily created when you decide to use it. We know the use of Func isn’t ideal and we’ll have something cleaner ready for RTM.

Am I restricted to Type and EntityTypeConfiguration?

No, the generic parameters are pretty flexible and Code First will call your convention with any combination of MemberInfo/Configuration that it finds in the model. For example you could specify PropertyInfo and StringPropertyConfiguration and Code First will call your convention for all string properties in your model:

 using System;
using System.Data.Entity.ModelConfiguration.Configuration.Properties.Primitive;
using System.Data.Entity.ModelConfiguration.Conventions.Configuration;
using System.Reflection;

public class MakeAllStringsShort :
    IConfigurationConvention<PropertyInfo, StringPropertyConfiguration>
{
    public void Apply(PropertyInfo propertyInfo, 
        Func<StringPropertyConfiguration> configuration)
    {
        configuration().MaxLength = 200;
    }
}

The first generic can be either of the following:

  • Type (System)
  • PropertyInfo (System.Reflection)

The second generic can be any of the following:

  • ModelConfiguration (System.Data.Entity.ModelConfiguration.Configuration)

  • EntityTypeConfiguration (System.Data.Entity.ModelConfiguration.Configuration.Types)

  • PropertyConfiguration (System.Data.Entity.ModelConfiguration.Configuration.Properties)

    • PrimitivePropertyConfiguration (System.Data.Entity.ModelConfiguration.Configuration.Properties.Primitive)

      • DateTimePropertyConfiguration (System.Data.Entity.ModelConfiguration.Configuration.Properties.Primitive)

      • DecimalPropertyConfiguration (System.Data.Entity.ModelConfiguration.Configuration.Properties.Primitive)

      • LengthPropertyConfiguration (System.Data.Entity.ModelConfiguration.Configuration.Properties.Primitive)

        • StringPropertyConfiguration (System.Data.Entity.ModelConfiguration.Configuration.Properties.Primitive)
        • BinaryPropertyConfiguration (System.Data.Entity.ModelConfiguration.Configuration.Properties.Primitive)

Important: Take note of the namespace included after each type. The Code First API currently includes types with the same names in other namespaces. Using these types will result in a convention that compiles but is never called. If the namespace ends in .Api then you have the wrong namespace. This is a rough edge that we will address before we RTM.

Most combinations are valid, however specifying Type and PropertyConfiguration (or any type derived from PropertyConfiguration) will result in the convention never being called since there is never a PropertyConfiguration for a Type. If you were to specify PropertyInfo and EntityTypeConfiguration then you will be passed the configuration for the type that the property is defined on. For example we could re-write our primary key convention as follows:

 using System;
using System.Data.Entity.ModelConfiguration.Configuration.Types;
using System.Data.Entity.ModelConfiguration.Conventions.Configuration;
using System.Reflection;

public class MyKeyConvention :
    IConfigurationConvention<PropertyInfo, EntityTypeConfiguration>
{
    public void Apply(PropertyInfo propertyInfo, Func<EntityTypeConfiguration> configuration)
    {
        if (propertyInfo.Name == propertyInfo.DeclaringType.Name + "Key")
        {
            configuration().Key(propertyInfo);
        }
    }
}
 

More Examples

Make all string properties in the model non-unicode:

 using System;
using System.Data.Entity.ModelConfiguration.Configuration.Properties.Primitive;
using System.Data.Entity.ModelConfiguration.Conventions.Configuration;
using System.Reflection;

public class MakeAllStringsNonUnicode :
    IConfigurationConvention<PropertyInfo, StringPropertyConfiguration>
{
    public void Apply(PropertyInfo propertyInfo, 
        Func<StringPropertyConfiguration> configuration)
    {
        configuration().IsUnicode = false;
    }
}
  
  
 
 

Ignore properties that conform to a common pattern:

 
 using System;
using System.Data.Entity.ModelConfiguration.Configuration.Types;
using System.Data.Entity.ModelConfiguration.Conventions.Configuration;
using System.Reflection;

public class IgnoreCommonTransientProperties :
    IConfigurationConvention<PropertyInfo, EntityTypeConfiguration>
{
    public void Apply(PropertyInfo propertyInfo, 
        Func<EntityTypeConfiguration> configuration)
    {
        if (propertyInfo.Name == "LastRefreshedFromDatabase"
            && propertyInfo.PropertyType == typeof(DateTime))
        {
            configuration().Ignore(propertyInfo);
        }
    }
}
  
  
 Ignore all types in a given namespace. 
 using System;
using System.Data.Entity.ModelConfiguration.Configuration;
using System.Data.Entity.ModelConfiguration.Conventions.Configuration;

public class IgnoreAllTypesInNamespace :
    IConfigurationConvention<Type, ModelConfiguration>
{
    private string _namespace;

    public IgnoreAllTypesInNamespace(string ns)
    {
        _namespace = ns;
    }

    public void Apply(Type typeInfo, 
        Func<ModelConfiguration> configuration)
    {
        if (typeInfo.Namespace == _namespace)
        {
            configuration().Ignore(typeInfo);
        }
    }
}

Because this convention does not have a default constructor we need to use a different overload of ModelBuilder.Conventions.Add.

 public class BlogContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Add(
            new IgnoreAllTypesInNamespace("ConventionSample.UnMappedTypes"));
    }
}
  
  
 Use TPT mapping for inheritance hierarchies by default:
 using System;
using System.Data.Entity.ModelConfiguration.Configuration.Types;
using System.Data.Entity.ModelConfiguration.Conventions.Configuration;

public class TptByDefault :
    IConfigurationConvention<Type, EntityTypeConfiguration>
{
    public void Apply(Type typeInfo, 
        Func<EntityTypeConfiguration> configuration)
    {
        configuration().ToTable(typeInfo.Name);
    }
}
  
  
 

Use a custom [NonUnicode] attribute to identify properties that should be non-unicode, note the use of the AttributeConfigurationConvention helper class that is included in CTP5.

 using System;
using System.Data.Entity.ModelConfiguration.Configuration.Properties.Primitive;
using System.Data.Entity.ModelConfiguration.Conventions.Configuration;
using System.Reflection;

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class NonUnicodeAttribute : Attribute
{ }

public class ApplyNonUnicodeAttribute
    : AttributeConfigurationConvention<PropertyInfo, StringPropertyConfiguration, NonUnicodeAttribute>
{

    public override void Apply(PropertyInfo propertyInfo, StringPropertyConfiguration configuration, NonUnicodeAttribute attribute)
    {
        configuration.IsUnicode = false;
    }
}

Removing Existing Conventions

As well as adding in your own conventions you can also disable any of the default conventions. For example you may wish to disable the convention that configures integer primary keys to be identity by default:

 public class BlogContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<StoreGeneratedIdentityKeyConvention>();
    }
}

The conventions that can be removed are:

Namespace: System.Data.Entity.ModelConfiguration.Conventions.Edm

  • AssociationInverseDiscoveryConvention

    Looks for navigation properties on classes that reference each other and configures them as inverse properties of the same relationship.

  • ComplexTypeDiscoveryConvention

    Looks for types that have no primary key and configures them as complex types.

  • DeclaredPropertyOrderingConvention

    Ensures the primary key properties of each entity precede other properties.

  • ForeignKeyAssociationMultiplicityConvention

    Configures relationships to be required or optional based on the nullability of the foreign key property, if included in the class definition.

  • IdKeyDiscoveryConvention

    Looks for properties named Id or <TypeName>Id and configures them as the primary key.

  • NavigationPropertyNameForeignKeyDiscoveryConvention

    Looks for a property to use as the foreign key for a relationship, using the <NavigationProperty><PrimaryKeyProperty> pattern.

  • OneToManyCascadeDeleteConvention

    Switches cascade delete on for required relationships.

  • OneToOneConstraintIntroductionConvention

    Configures the primary key as the foreign key for one:one relationships.

  • PluralizingEntitySetNameConvention

    Configures the entity set name in the Entity Data Model to be the pluralized type name.

  • PrimaryKeyNameForeignKeyDiscoveryConvention

    Looks for a property to use as the foreign key for a relationship, using the <PrimaryKeyProperty> pattern.

  • PropertyMaxLengthConvention

    Configures all String and byte[] properties to have max length by default.

  • StoreGeneratedIdentityKeyConvention

    Configure all integer primary keys to be Identity by default.

  • TypeNameForeignKeyDiscoveryConvention

    Looks for a property to use as the foreign key for a relationship, using the <PrincipalTypeName><PrimaryKeyProperty> pattern.

Namespace: System.Data.Entity.ModelConfiguration.Conventions.Edm.Db

  • ColumnOrderingConvention

    Applies any ordering specified in [Column] annotations.

  • ColumnTypeCasingConvention

    Converts any store types that were explicitly configured to be lowercase. Some providers, including MS SQL Server and SQL Compact, require store types to be specified in lower case.

  • PluralizingTableNameConvention

    Configures table names to be the pluralized type name.

Namespace: System.Data.Entity.ModelConfiguration.Conventions.Configuration

These conventions process the data annotations that can be specified in classes. For example if you wanted to stop Code First from taking notice of the StringLength data annotation you could remove the StringLengthAttributeConvention.

  • ColumnAttributeConvention
  • ComplexTypeAttributeConvention
  • ConcurrencyCheckAttributeConvention
  • DatabaseGeneratedAttributeConvention
  • ForeignKeyAttributeConvention
  • InversePropertyAttributeConvention
  • KeyAttributeConvention
  • MaxLengthAttributeConvention
  • NotMappedPropertyAttributeConvention
  • NotMappedTypeAttributeConvention
  • RequiredNavigationPropertyAttributeConvention
  • StringLengthAttributeConvention
  • TableAttributeConvention
  • TimestampAttributeConvention

Namespace: System.Data.Entity.Database

  • IncludeMetadataInModel

    Adds the EdmMetadata table, used by DbContext to check if the database schema matches the current model.

 

Summary

In this post we covered the Pluggable Conventions feature that is included in EF Feature CTP5. This is an early preview and still has a number of rough edges but we included it because we really want your feedback.

As always we would love to hear any feedback you have by commenting on this blog post.

For support please use the Entity Framework Pre-Release Forum.

Rowan Miller

Program Manager

ADO.NET Entity Framework