My First Encounter with Custom Conventions in Entity Framework 6

This post was written before EF6 RTM was released and is out-of-date. For up-to-date information see the article on model based conventions on msdn.

I was not really involved in the work related to custom conventions in EF6 and had not had a chance to try this feature so when I saw this question on stackoverflow I thought it would be a good opportunity to try it out and learn something new. The question was – how do I configure foreign keys using custom conventions? Entity Framework has a built-in convention which configures a property whose name is equal to a corresponding navigation property name suffixed with “Id” as a foreign key property. However if the model uses a different naming convention the built-in convention will not work. In the question from stackoverflow the convention for foreign key properties was “Id” + {NavigationPropertyName}. Since it was used consistently across the model it really did make sense to use custom conventions instead of configuring each foreign key property manually (i.e. using ForeignKey attribute or .HasForeignKey() method). So, I looked at the custom conventions specification and started reading about lightweight conventions. It looked simple – one line of code should be enough. Unfortunately I could not find a good way to configure foreign keys with this approach – .Properties() allows only configuring primitive properties and foreign keys need to be configured on navigation properties. .Entities() is great for configuring entity level settings but does not really allow to configure a property if you don’t know its name (and it did not seem possible to configure navigation properties this way either). Therefore I started looking at the Configuration-based Conventions. They are no longer a single line of code as the lightweight conventions and require creating a class implementing the IConfigurationConvention interface containing the logic for the convention. In return they allow for much more flexibility. The IConfigurationConvention interface is generic and requires two types – TMemberInfo and TConfiguration. TMemberInfo can be either Type or PropertyInfo and defines whether the IConfigurationConvention.Apply() method should be invoked for types or properties. The second parameter (TConfiguration) describes what we would like to configure and can be for instance a PropertyConfiguration, StructuralTypeConfiguration, PrimitivePropertyConfiguration etc. (the full list can be found in the feature specification). One of the configurations on the list is NavigationPropertyConfiguration. This seemed to be exactly what I needed to be able to configure foreign keys. Now the plan was simple – create a convention that for each navigation property will try finding a property named “Id” + {NavigationPropertyName} and if such a property exists configure it as a foreign key property. This is the convention I came up with:

public class NavigationPropertyConfigurationConvention
    : IConfigurationConvention<PropertyInfo, NavigationPropertyConfiguration>
{
    public void Apply(
                 PropertyInfo propertyInfo, 
                 Func<NavigationPropertyConfiguration> configuration)
    {
        var foreignKeyProperty = 
            propertyInfo.DeclaringType.GetProperty("Id" + propertyInfo.Name);

        if (foreignKeyProperty != null && configuration().Constraint == null)
        {
            var fkConstraint = new ForeignKeyConstraintConfiguration();
            fkConstraint.AddColumn(foreignKeyProperty);

            configuration().Constraint = fkConstraint;
        }           
    }
}

While not a single liner the convention is short and easy to follow. I created a very simple model to test it out:

public class Entity
{
    public int Id { get; set; }

    public ICollection<RelatedEntity> RelatedEntities { get; set; }
}

public class RelatedEntity
{
    public int Id { get; set; }

    public int IdPrincipal { get; set; }

    public Entity Principal { get; set; }
}

public class MyContext : DbContext
{
    public DbSet<Entity> Entities { get; set; }
    public DbSet<RelatedEntity> RelatedEntities { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Add<NavigationPropertyConfigurationConvention>();
    }
}

Note that the convention had to be registered in the OnModelCreating() method.
To see that the convention was applied I set a break point in the convention an started my test app. Also to verify it worked I just dumped the edmx using edmx writer and checked my AssociationType (notice the PropertyRef Name under the Dependent element):

<Association Name="RelatedEntity_Principal">
  <End Role="Entity" Type="Self.Entity" Multiplicity="1">
    <OnDelete Action="Cascade" />
  </End>
  <End Role="RelatedEntity" Type="Self.RelatedEntity" Multiplicity="*" />
  <ReferentialConstraint>
    <Principal Role="Entity">
      <PropertyRef Name="Id" />
    </Principal>
    <Dependent Role="RelatedEntity">
      <PropertyRef Name="IdPrincipal" />
    </Dependent>
  </ReferentialConstraint>
</Association>

You may also want to check this interesting post on conventions from Rowan.

Advertisements

11 thoughts on “My First Encounter with Custom Conventions in Entity Framework 6

  1. Andreas Geier says:

    Hi Pawel,
    in my Solution I only have the navigation properties in pocos using the virtual object itself without using an additional Id-Property. It works but the naming convention of EF creates a column with a name I want to change.
    I want to use custom conventions (using EF6 alpha 2) to change the name of this generated “columns”.
    Example:
    public class BaseBob { public Guid GlobalKey { get; set; } }
    [Table(“Persons”)]
    public class Person : BaseBob
    {
    public string Name { get; set; }
    public Address Address { get; set; }
    }
    public class Address : BaseBob
    {
    public String StreetName { get; set; }
    }

    this will create a column in Db-Table Persons called “Address_GlobalKey”.
    I only want to change this columnname to “AddressGlobalKey”.

    Can you tell me, if this is possible using custom conventions? Its because all of my business-objects using the BaseBob-Class and the reference-key is always a GUID called GlobalKey.

    Thanks in advance.
    Andreas

    Like

    • moozzyk says:

      Hi Andreas,
      I am missing some information here. The model you showed is invalid because none of the classes are entities since none of them have any key property. GlobalKey seems to act as a key property but is not configured as such and does not match convention for keys so effectively it is not a key property. Person seems to be an entity type since it has Table attribute but again since it does not have any key property it is not an entity. I actually tried your model and the above was confirmed by an exception saying: “System.Data.Entity.Edm.EdmEntityType: : EntityType ‘Person’ has no key defined Define the key for this EntityType.” Now Address is even more confusing since it might be an entity type or a complex type (depending on the configuration I am not seeing). You said Address is a navigation property but Address_GlobalKey indicates it is a complex property since the convention for naming complex properties is : {ComplexTypeName}_{PropertyName}. If this is the case you should not look at conventions for navigation properties but just for properties. You might be able to use lightweight conventions to do that. I may try to write a post about that actually…

      Like

      • Andreas Geier says:

        Sorry about that: I didn’t write it correct in the post. Of course I put the correct DataAnnotations to [Key] to GlobalKey in BaseClass and [Table] to Address. In addition please notice that BaseClass is abstract. I used TPT and have much more classes in there. Here you get a top down line from Base to Person:

        [Table(“Objects”)]
        public abstract class BaseClass { … }

        [Table(“Bobs”)]
        public abstract class BusinessObject : BaseClass { … }

        [Table(“Contacts”)]
        public class Contact : BusinessObject { … }

        [Table(“Persons”)]
        public class Persons : Contact { … }

        I dont use Complex Types in my solution. First I used DataAnnotations and NavigationProperties this way i.e. in the Address-Class:


        [ForeignKey(“PersonGlobalKey”)]
        public Person Person { get; set; }
        public Guid? PersonGlobalKey { get; set; }

        just to realize, that the column-name in the database is PersonGlobalKey instead of Person_GlobalKey.

        Thank you.
        Andreas

        Like

    • moozzyk says:

      I don’t have reply button in your last post so replying here. For what you need you should be able to use lightweight conventions. I simplified your model and came up with something like this:

      public class BaseBob
      {
      [Key]
      public Guid GlobalKey { get; set; }
      }

      [Table(“Addresses”)]
      public class Address : BaseBob
      {
      public String StreetName { get; set; }
      }

      [Table(“Persons”)]
      public class Person : BaseBob
      {
      [ForeignKey(“AddressGlobalKey”)]
      public Address Address { get; set; }

      public Guid? AddressGlobalKey { get; set; }
      }

      public class MyContext : DbContext
      {
      DbSet

      Addresses { get; set; }
      DbSet Persons { get; set; }

      protected override void OnModelCreating(DbModelBuilder modelBuilder)
      {
      modelBuilder
      .Properties()
      .Where(p => p.Name.EndsWith(“GlobalKey”) && p.Name != “GlobalKey”)
      .Configure(
      p => p.HasColumnName(
      p.ClrPropertyInfo.Name.Insert(
      p.ClrPropertyInfo.Name.Length – “GlobalKey”.Length, “_”)));
      }
      }

      Like

      • Andreas Geier says:

        Hi, thank you very much. This will work, if I provide a GlobalKey-Property for ALL my Reference-Properties and attach a [ForeignKey]Attribute to them (I’ve done this allready without using your code in OnModelCreating and it worked well before).

        But now I don’t have any Global-Key Properties in my Business-Object Classes anymore, because I don’t want other developers using this GlobalKey-Properties directly and to avoid fixup-Problems when using DetectChanges on DbContext.

        So I’m still searching for a convention to do this.
        At the moment there are no fixup-Problems, because of I’m NOT USING Global-Key-Properties with [Foreign-Key]-Attributes.
        If there is no way to do this, I will try to use this:

        modelBuilder.Entity()
        .HasOptional(x => x.Person)
        .WithMany()
        .Map(a => a.MapKey(“PersonGlobalKey”));

        But this is very much work, for all classes. And the model is still growing.

        Thanks again
        Best regards
        Andreas

        Like

    • moozzyk says:

      There has been quite a few changes to conventions for the Beta release and also post-Beta. Sorry, I can’t post more details at the moment – I have not been following conventions recently. I took a brief look and I can see that IConfigurationConvention is now internal. However there is IModelConvention that is public so it probably is your starting point.

      Like

    • moozzyk says:

      Btw. the post you gave the link for is about the Beta of EF5 and not EF6. Indeed in EF5 conventions were public at some point before the release (I think until EF5 CTP5) but then the feature was made internal since it was not ready for the prime time. In EF6 custom/pluggable conventions is one of the bigger features but there is still quite a lot of churn in the area (including changes to the public API).

      Like

    • moozzyk says:

      You are correct. When I wrote this post the latest version of EF6 available was alpha. A lot of things changed in this area for RTM. I will add a note at the top of the article with the link to the documentation you posted.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: