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.