Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Entity Framework Code First : how to annotate a foreign key for a "Default" value?

I have 2 classes: Client and Survey.

Each Client can have many surveys - but only one default survey.

I have defined the classes like this:

public class Client
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ID { get; set; }

    public string ClientName { get; set; }

    public Nullable<int> DefaultSurveyID { get; set; }

    [ForeignKey("DefaultSurveyID")]
    public virtual Survey DefaultSurvey { get; set; }

    public virtual ICollection<Survey> Surveys { get; set; }
}

public class Survey
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ID { get; set; }

    public string SurveyName { get; set; }

    [Required]
    public int ClientID { get; set; }

    [ForeignKey("ClientID")]
    public virtual Client Client { get; set; }
}

This creates the Client table as I expect:

[dbo].[Clients]
(
[ID] [int] IDENTITY(1,1) NOT NULL,
[ClientName] [nvarchar](max) NULL,
[DefaultSurveyID] [int] NULL
)

But the Survey table has an extra foreign key:

[dbo].[Surveys]
(
[ID] [int] IDENTITY(1,1) NOT NULL,
[SurveyName] [nvarchar](max) NULL,
[ClientID] [int] NOT NULL,
[Client_ID] [int] NULL
)

Why is Code First generating this relationship and how to I tell it not to?

like image 810
Colin Avatar asked Mar 18 '13 17:03

Colin


People also ask

How do you mention foreign key in code first approach?

To create Foreign Key, you need to use ForeignKey attribute with specifying the name of the property as parameter. You also need to specify the name of the table which is going to participate in relationship. I mean to say, define the foreign key table. Thanks for reading this article, hope you enjoyed it.

Can a foreign key have a default value?

Yes, you can define a column with a default value of 0 as a Foreign Key. However, for the constraint to work, you would need to have a row in the source table with a value of 0 as well.

How do you set a foreign key in EF?

The [ForeignKey(name)] attribute can be applied in three ways: [ForeignKey(NavigationPropertyName)] on the foreign key scalar property in the dependent entity. [ForeignKey(ForeignKeyPropertyName)] on the related reference navigation property in the dependent entity.


3 Answers

The problem is that when you have multiple relationships between two entities, EF Code First isn't able to find out which navigation properties match up, unless, you tell it how, here is the code:

public class Client
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ID { get; set; }

    public string ClientName { get; set; }

    /****Change Nullable<int> by int?, looks better****/
    public int? DefaultSurveyID { get; set; }

    /****You need to add this attribute****/
    [InverseProperty("ID")]
    [ForeignKey("DefaultSurveyID")]
    public virtual Survey DefaultSurvey { get; set; }

    public virtual ICollection<Survey> Surveys { get; set; }
}

With your previous version, EF was creating that extra relationship because it didn't know that the DefaultSurvey property was referencing the ID of the Survey class, but you can let it know that, adding the attribute InverseProperty whose parameter is the name of the property in Survey you need DefaultSurvey to match with.

like image 102
ecampver Avatar answered Oct 13 '22 00:10

ecampver


You can do it using code-first, but not being a code first expert I cheated :-)

1) I created the tables and relationships (as above without the extra Client_ID) in the database using SMS

2) I used Reverse Engineer Code First to create the required classes and mappings

3) I dropped the database and recreated it using context.Database.Create()

Original table defs:

CREATE TABLE [dbo].[Client](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [Name] [nvarchar](50) NULL,
    [DefaultSurveyId] [int] NULL,
     CONSTRAINT [PK_dbo.Client] PRIMARY KEY NONCLUSTERED 
    (
        [Id] ASC
    )
)

CREATE TABLE [dbo].[Survey](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [Name] [nvarchar](50) NULL,
    [ClientId] [int] NULL,
     CONSTRAINT [PK_dbo.Survey] PRIMARY KEY NONCLUSTERED 
    (
        [Id] ASC
    )
)

Plus foreign keys

ALTER TABLE [dbo].[Survey]  WITH CHECK 
    ADD CONSTRAINT [FK_dbo.Survey_dbo.Client_ClientId] FOREIGN KEY([ClientId])
    REFERENCES [dbo].[Client] ([Id])

ALTER TABLE [dbo].[Client]  WITH CHECK 
    ADD CONSTRAINT [FK_dbo.Client_dbo.Survey_DefaultSurveyId] 
    FOREIGN KEY([DefaultSurveyId]) REFERENCES [dbo].[Survey] ([Id])

Code generated by reverse engineering:

public partial class Client
{
    public Client()
    {
        this.Surveys = new List<Survey>();
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public int? DefaultSurveyId { get; set; }
    public virtual Survey DefaultSurvey { get; set; }
    public virtual ICollection<Survey> Surveys { get; set; }
}

public partial class Survey
{
    public Survey()
    {
        this.Clients = new List<Client>();
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public int? ClientId { get; set; }
    public virtual ICollection<Client> Clients { get; set; }
    public virtual Client Client { get; set; }
}

public class ClientMap : EntityTypeConfiguration<Client>
{
    #region Constructors and Destructors

    public ClientMap()
    {
        // Primary Key
        this.HasKey(t => t.Id);

        // Properties
        this.Property(t => t.Name).HasMaxLength(50);

        // Table & Column Mappings
        this.ToTable("Client");
        this.Property(t => t.Id).HasColumnName("Id");
        this.Property(t => t.Name).HasColumnName("Name");
        this.Property(t => t.DefaultSurveyId).HasColumnName("DefaultSurveyId");

        // Relationships
        this.HasOptional(t => t.DefaultSurvey)
            .WithMany(t => t.Clients).HasForeignKey(d => d.DefaultSurveyId);
    }

    #endregion
}

public class SurveyMap : EntityTypeConfiguration<Survey>
{
    #region Constructors and Destructors

    public SurveyMap()
    {
        // Primary Key
        this.HasKey(t => t.Id);

        // Properties
        this.Property(t => t.Name).HasMaxLength(50);

        // Table & Column Mappings
        this.ToTable("Survey");
        this.Property(t => t.Id).HasColumnName("Id");
        this.Property(t => t.Name).HasColumnName("Name");
        this.Property(t => t.ClientId).HasColumnName("ClientId");

        // Relationships
        this.HasOptional(t => t.Client)
            .WithMany(t => t.Surveys).HasForeignKey(d => d.ClientId);
    }

    #endregion
}
like image 11
Phil Avatar answered Oct 12 '22 23:10

Phil


Entity Framework does exactly what it's told to do. What you've told it is that there is both a one-to-many and a one-to-one relationship between Clients and Surveys. It generated both FKs in the Survey table in order to map both of the relationships that you've requested. It has no idea that you're trying to connect the two relationships together, nor do I think does it have the ability to deal with that.

As an alternative you might want to consider adding a IsDefaultSurvey field on the Survey object so that you can query for the default survey through the Surveys collection that you have on the Client object. You could even go one step further and put it in as a NotMapped property on the Client object so that you could still use Client.DefaultSurvey to get the correct survey, and not have to change any of your other code, as follows:

[NotMapped]
public Survey DefaultSurvey
{
  get { return this.Surveys.First(s => s.IsDefaultSurvey); }
}
like image 2
Corey Adler Avatar answered Oct 13 '22 00:10

Corey Adler