I’ve noticed a weird occurrence with EF and the way it determines which fields it will use when evaluating a .Include(x=x.T) statement.
In our project, all our database tables (and thus the POCOs for EF) are prefixed with DB, and the two tables in question are DBTemplates and DBItems. Each Item has an (optional) template associated with it.
The relevant lines in our DbContext are this:
public IDbSet<DBTemplate> Templates {get;set;}
public IDbSet<DBItem> Items {get;set;}
For simplicity, assume both tables contain an Id and Name property as well as the DBItems table having a foreign key back to DBTemplates. This is (right now) called Template_Id (I’ll explain why in a bit), but generally speaking we would have named it DBTemplateId.
Generally speaking, we have a variable called db that is an instance of our DbContext.
The developer who initially created the DBItem definition added the virtual property linking to the related DBTemplate, but omitted the foreign key. The properties look like this:
public class DBItem
{
public System.Guid Id { get; set; }
public string Name { get; set; }
public virtual DBTemplate Template { get; set; }
}
public class DBTemplate
{
public System.Int32 Id { get; set; }
public string Name { get; set; }
}
When querying for Items, the following statement is hit
db.Items.Include(i=>i.Template)
and the following SQL is generated
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
[Extent2].[Id] AS [Id1],
[Extent2].[Name] AS [Name1],
FROM [dbo].[DBItems] AS [Extent1]
LEFT OUTER JOIN [dbo].[DBTemplates] AS [Extent2] ON [Extent1].[Template_Id] = [Extent2].[Id]
So far, so good. However, I now need (and should have) the Template_Id on the DBItem object. When I add it to the DBItem class so it looks like this:
public class DBItem
{
public System.Guid Id { get; set; }
public string Name { get; set; }
public Nullable<System.Int32> Template_Id { get; set; }
public virtual DBTemplate Template { get; set; }
}
the same line is hit, but the SQL generated looks like this now
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
[Extent1].[Template_Id] AS [Template_Id],
[Extent2].[Id] AS [Id1],
[Extent2].[Name] AS [Name1],
FROM [dbo].[DBItems] AS [Extent1]
LEFT OUTER JOIN [dbo].[DBTemplates] AS [Extent2] ON [Extent1].[Template_Id1] = [Extent2].[Id]
(Note the Template_Id1 for the join)
Now, this is simple to fix, as I can just tell the model builder to look for a foreign key named Template_Id, and everything is fine.
builder.Entity<DBItem>().HasOptional(x => x.Template).WithMany().HasForeignKey(x => x.Template_Id);
However, if the convention was followed, and the field named DBTemplateId, and the DBItem class looked like this:
public class DBItem
{
public System.Guid Id { get; set; }
public string Name { get; set; }
public Nullable<System.Int32> DBTemplateId { get; set; }
public virtual DBTemplate Template { get; set; }
}
Without modifying the ModelBuilder properties (letting EF take over purely on convention), it correctly takes the DBTemplateId property as the foreign key, and generates SQL like this:
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
[Extent1].[DBTemplateId] AS [DBTemplateId],
[Extent2].[Id] AS [Id1],
[Extent2].[Name] AS [Name1],
FROM [dbo].[DBItems] AS [Extent1]
LEFT OUTER JOIN [dbo].[DBTemplates] AS [Extent2] ON [Extent1].[DBTemplateId] = [Extent2].[Id]
and the query works as intended (with less error-prone C# code too).
Obviously the lesson here is to follow the pattern with field naming, but why does EF automatically find and use the property name as the join key when named DBTemplateId but looks for a duplicate Template_Id1 when Template_Id exists on the POCO?
When Entity Framework Code First detects a relationship, it tries to discover the property that should be used as the foreign key by following some conventions.
It will look for a property with a name matching one of the following 3 rules:
[Targettype key property name][Targettype name]+[Targettype key property name][Foreignkey navigation property name]+[Targettype key property name]So when you add a property called
DBTemplateIdthe second rule will be matched, and Code First will correctly use that property as the foreign key and generate a foreign key column in the database with that name.If you however call the property
Template_Idinstead, none of the rules will be matched, and Code First will simply generate a name for the foreign key automatically.By convention the generated name will be
[Navigation property name]_[Targettype key property name], which in your case results inTemplate_Id. But because there already exists a property with that name, it will append the number 1.It seems to me like it would make sense for Code First to have a 4th rule that would match the same name it itself creates automatically. If it did that,
Template_Idwould have been a legal name, and you wouldn’t have to configure it using theHasForeignKeymethod.