Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Entity Framework 4.1 Database First does not add a primary key to the DbContext T4 generated class

I am just getting started with Entity Framework 4.1, trying out the "database first" mode. When EF generates a Model class with the "ADO.Net DbContext Generator," shouldn't it identify the primary key for the class with a [Key] attribute? Without this, it appears incompatible with the T4 MVCScaffolding.

Here are the details:

Using the Entity Data Model Designer GUI, I have added a simple "country" table to the model from my existing database. The GUI correctly identifies a single integer identity key field named "PK" as my primary key. (Alas! I'm a new user so I can't add a screenshot. I've included the CSDL instead below.) However, when EF generates code using the "ADO.Net DbContext Generator", it does not identify the PK field as the key field in the generated class (see code excerpt below).

The CSDL for the "country" table:

<edmx:ConceptualModels>
  <Schema Namespace="EpiDataModel" Alias="Self" xmlns:annotation="http://schemas.microsoft.com/ado/2009/02/edm/annotation" xmlns="http://schemas.microsoft.com/ado/2008/09/edm">
    <EntityContainer Name="EpiModelEntities" annotation:LazyLoadingEnabled="true">
      <EntitySet Name="countries" EntityType="EpiDataModel.country" />
    </EntityContainer>
    <EntityType Name="country">
      <Key>
        <PropertyRef Name="PK" />
      </Key>
      <Property Name="PK" Type="Int32" Nullable="false" annotation:StoreGeneratedPattern="Identity" />
      <Property Name="Abbreviation" Type="String" Nullable="false" MaxLength="200" Unicode="false" FixedLength="false" />
      <Property Name="Name" Type="String" MaxLength="1024" Unicode="false" FixedLength="false" />
      <Property Name="Description" Type="String" MaxLength="1024" Unicode="false" FixedLength="false" />
      <Property Name="Sequence" Type="Int32" />
    </EntityType>
  </Schema>
</edmx:ConceptualModels>

Here's the autogenerated code:

//------------------------------------------------------------------------------
// <auto-generated>
//    This code was generated from a template.
//
//    Manual changes to this file may cause unexpected behavior in your application.
//    Manual changes to this file will be overwritten if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

using System;
using System.Collections.Generic;

namespace MvcApplication1.Areas.Epi.Models
{
    public partial class country
    {
        public int PK { get; set; }
        public string Abbreviation { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public Nullable<int> Sequence { get; set; }
    }
}

This causes a problem when I try to scaffold a controller using the MVCScaffolding T4 template. I get an error "No properties appear to be primary keys." The command and output from the NuGet Package Manager Console is below:

PM> scaffold controller MvcApplication1.Areas.Epi.Models.country -Area Epi -NoChildItems -DbContextType MvcApplication1.Areas.Epi.Models.EpiModelEntities -Force
Scaffolding countriesController...
Get-PrimaryKey : Cannot find primary key property for type 'MvcApplication1.Areas.Epi.Models.country'. No properties appear to be primary keys.
At C:\work\EPI\EPIC_MVC3\sandbox\MvcApplication1\packages\MvcScaffolding.1.0.6\tools\Controller\MvcScaffolding.Controller.ps1:74 char:29
+ $primaryKey = Get-PrimaryKey <<<<  $foundModelType.FullName -Project $Project -ErrorIfNotFound
    + CategoryInfo          : NotSpecified: (:) [Get-PrimaryKey], Exception
    + FullyQualifiedErrorId : T4Scaffolding.Cmdlets.GetPrimaryKeyCmdlet

However, if I manually change the generated class to add a [Key] attribute to the field, then the exact same scaffolding command shown above works fine:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; // manually added

namespace MvcApplication1.Areas.Epi.Models
{
    public partial class country
    {
        [Key]                        // manually added
        public int PK { get; set; }
        public string Abbreviation { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public Nullable<int> Sequence { get; set; }
    }
}

So why aren't EF Database First and the T4 MVCScaffolding playing nice together? And even without the scaffolding issue, don't the EF classes need to know what the key field(s) are?

like image 627
Martin_W Avatar asked Dec 07 '11 18:12

Martin_W


2 Answers

T4 Templates doesn't use data annotations because classes generated from templates don't need them. EF also don't need them because mapping is defined in XML files not in code. If you need data annotations you must either:

  • Modify T4 template to use them (this requires understanding of EF metadata model)
  • Don't use templates and use code first instead
  • Use buddy classes to manually add data annotations and hope that scaffolding will recognize them
like image 115
Ladislav Mrnka Avatar answered Sep 29 '22 08:09

Ladislav Mrnka


If someone does want to do this, I found some good interesting templates on james mannings github Those templates have more functionality, but the bit I pulled out of these was:
1) Replace the usings at the top of Entity.tt with

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
<#
    if (efHost.EntityFrameworkVersion >= new Version(4, 4))
    {
       WriteLine("using System.ComponentModel.DataAnnotations.Schema;");
    }
#>

2) Then find this line (that prints out the properties)

    <#= Accessibility.ForProperty(property) #> <#= typeUsage #> <#= code.Escape(property) #> { get; set; }

3) and prepend this template code

    var attributes = new List<string>();
    var isPartOfPrimaryKey = efHost.EntityType.KeyMembers.Contains(property);
    var primaryKeyHasMultipleColumns = efHost.EntityType.KeyMembers.Count > 1;

    if (isPartOfPrimaryKey)
    {
        if (primaryKeyHasMultipleColumns)
        {
            var columnNumber = efHost.EntityType.KeyMembers.IndexOf(property);
            attributes.Add(String.Format("[Key, Column(Order = {0})]", columnNumber));
        }
        else
        {
            attributes.Add("[Key]");
        }
    }
    PushIndent(new string(' ', 8));
    foreach (var attribute in attributes)
    {
        WriteLine(attribute);
    }
    ClearIndent();
like image 21
Mike Walsh Avatar answered Sep 29 '22 07:09

Mike Walsh