EF Code First Navigation Properties and Foreign Keys

An Entity Framework Code First class corresponding to a table with a foreign key typically has two fields for the foreign key. The foreign key as represented in the database and a C# reference.

[ForeignKey("BrandId")]
public Brand Brand { get; set; }
 
[ForeignKey("Brand")]
public int BrandId { get; set; }

This is a violation of the DRY principle. In C# there is nothing keeping the two properties in sync. We can set BrandId to the Id of Saab and the Brand reference to the Volvo object. If I do that, what will Entity Framework do when saving to the database? In this post I’ll investigate how Entity Framework handles the situation.

Updating the Foreign Key

For my first example I’ll use the Car entity.

public class Car
{
    public int CarId { get; private set; }
 
    [ForeignKey("BrandId")]
    public Brand Brand { get; set; }
 
    [ForeignKey("Brand")]
    public int BrandId { get; set; }
 
    [Required]
    [StringLength(6)]
    public string RegistrationNumber { get; set; }
 
    [Column("BodyStyle", TypeName = "int")]
    public CarBodyStyle BodyStyle { get; set; }
 
    public int? TopSpeed { get; set; }
 
    [Required]
    [StringLength(20)]
    public string Color { get; set; }
}

My first test is to update the foreign key. I’ve added debug outputs to the code to show exactly what happens.

The car has BrandId 7 pointing to Brand "Volvo"
Setting BrandId to 8
The car has BrandId 8 pointing to Brand "Volvo"
Saving Changes...
The car has BrandId 8 pointing to Brand "Saab"

As expected, setting the BrandId won’t change the Brand property. Before saving the item, the state is inconsistent. When saving, Entity Framework updates the database according to the changed key and also updates the Brand property accordingly.

Updating the Navigation Property

My second test is to go the other way around, updating the navigation property. Will the unchanged BrandId overrule the changed navigation property, or will Entity Framework let the property that was updated decide?

The car has BrandId 7 pointing to Brand "Volvo"
Setting Brand to Saab
The car has BrandId 7 pointing to Brand "Saab"
Saving Changes...
The car has BrandId 8 pointing to Brand "Saab"

Entity Framework figures out that this time it is the navigation property that has changed and updates accordingly. That’s great.

Making a conflicting Update

My third test is a bit more cruel. I’ll update both to new values, but to point to different objects. How will Entity Framework handle the inconsistency?

The car has BrandId 7 pointing to Brand "Volvo"
Setting Brand to Saab
The car has BrandId 7 pointing to Brand "Saab"
Setting BrandId to 9
The car has BrandId 9 pointing to Brand "Saab"
Saving Changes...
A first chance exception of type 'System.InvalidOperationException' occurred in System.Data.Entity.dll

When saving the changes, an exception is thrown.

Conflicting changes to the role ‘Car_Brand_Target’ of the relationship ‘TestLib.Entities.Car_Brand’ have been detected.

That’s also great, aborting is the only reasonable alternative in this place. Having tested three cases so far, this post could end here with Entity Framework having passed the tests. However there is yet another alternative that I want to show. I’ve saved the best for last.

Entity Framework Dynamic Proxy in Action

I’ve previously shown how an Entity Framework Dynamic Proxy is automatically created and how it can improve change tracking. When running the tests above, I had to be careful to run them on a test class that does not meet the requirements for dynamic proxy objects. Let’s rerun the first test with another class, that do fulfil the requirements for dynamic proxies.

The person has GenderId 0 pointing to Gender "UnSpecified"
Setting GenderId to 1
The person has GenderId 1 pointing to Gender "Male"
Saving Changes...
The person has GenderId 1 pointing to Gender "Male"

With the proxy wrapped around our Gender object the setting of GenderId is intercepted and the Gender navigation property is automatically updated. No more inconsistencies. The other way around works too, so whenever one of the properties are updated, the other one is updated too. I think that this feature makes a very clear case for always trying to conform to the proxy creation rules when working with code first. The proxies have been designed to intercept calls to the entity objects, providing extra services and added safety. Without them we are on our own, having to constantly be on guard against nasty bugs caused by inconsistencies. It’s not worth it. It’s better to make the entities compatible with the proxy requirements.

This post is part of the EF Migrations series.<< EF Migrations Command ReferenceUpdate-Database MSI Custom Action >>

7 comments

  1. I’ve been with this problem since the day you made this post. Actually, that is the day when I started to work with EF.

    I’ve made those test of yours but with diferent results. Changing the ID would change the navigation property but not the other way around. Is there a chance that you send me you codes via e-mail.

    tks…

    1. I just double checked that I indeed had tested both ways, and it works for me:

      The person has GenderId 0 pointing to Gender "UnSpecified"
      Setting GenderId to 1
      The person has GenderId 1 pointing to Gender "Male"
       
      The person has GenderId 0 pointing to Gender "UnSpecified"
      Setting Gender to Male
      The person has GenderId 1 pointing to Gender "Male"

      Are your properties virtual? Without that the EF proxy won’t be able to intercept the changes. These are my properties:

      [ForeignKey("Gender")]
      public virtual byte GenderId { get; set; }
       
      [ForeignKey("GenderId")]
      public virtual Gender Gender { get; set; }
  2. I tried version 4.3.1 and EF5 Beta2. no go.
    Would really help if you can post the complete working program if you have one.

    1. This is a complete listing of the code involved in the test.

      [Table("People")] 
      public class Person 
      {
        public virtual int PersonId { get; set; }
       
        private int birthYear;
       
        [Required] 
        public virtual int BirthYear 
        { 
          get 
          { 
            Debug.WriteLine(string.Format("Getting BirthYear of person {0}", PersonId)); 
            return birthYear; 
          } 
          set 
          { 
            Debug.WriteLine(string.Format("Setting BirthYear of person {0} to {1}", 
            PersonId, value)); 
            birthYear = value; 
          } 
        }
       
        [ForeignKey("Gender")]  public virtual byte GenderId { get; set; }
       
        [ForeignKey("GenderId")] 
        public virtual Gender Gender { get; set; } 
      }
       
      public class CarsContext : DbContext 
      { 
        public CarsContext(string connectionString) 
        : base(connectionString) 
        {}
       
        public CarsContext() { }
       
        static CarsContext() 
        {
          // See http://coding.abel.nu/2012/03/prevent-ef-migrations-from-creating-or-changing-the-database/ 
          Database.SetInitializer(new ValidateDatabase<CarsContext>()); 
        }
       
        public DbSet<Person> People { get; set; } 
        public DbSet<Gender> Genders { get; set; }
      }
       
      [TestMethod] 
      public void TestNavigationAndForeignKeySetKeyWithProxy() 
      { 
        using (TransactionScope ts = new TransactionScope()) 
        using (CarsContext context = new CarsContext()) 
        { 
          var genders = context.Genders.Include(g => g.People).OrderBy(g => g.GenderId); 
          var person = genders.First().People.First();
       
          Debug.WriteLine("The person has GenderId {0} pointing to Gender "{1}"", 
            person.GenderId, person.Gender.Description); 
          Gender newGender = genders.Skip(1).First(); 
          Debug.WriteLine(string.Format("Setting GenderId to {0}", newGender.GenderId)); 
            person.GenderId = newGender.GenderId; 
          Debug.WriteLine("The person has GenderId {0} pointing to Gender "{1}"", 
            person.GenderId, person.Gender.Description);
       
          Debug.WriteLine("Saving Changes..."); 
          context.SaveChanges(); 
          Debug.WriteLine("The person has GenderId {0} pointing to Gender "{1}"", 
            person.GenderId, person.Gender.Description); 
        }
      } 
       
      [TestMethod] 
      public void TestNavigationAndForeignKeySetNavigationPropertyWithProxy() 
      { 
        using (TransactionScope ts = new TransactionScope()) 
        using (CarsContext context = new CarsContext()) 
        { 
          var genders = context.Genders.Include(g => g.People).OrderBy(g => g.GenderId); 
          var person = genders.First().People.First();
       
          Debug.WriteLine("The person has GenderId {0} pointing to Gender "{1}"", 
            person.GenderId, person.Gender.Description); 
          Gender newGender = genders.Skip(1).First(); 
          Debug.WriteLine(string.Format("Setting Gender to {0}", newGender.Description)); 
          person.Gender = newGender; 
          Debug.WriteLine("The person has GenderId {0} pointing to Gender "{1}"", 
            person.GenderId, person.Gender.Description);
       
          Debug.WriteLine("Saving Changes..."); 
          context.SaveChanges(); 
          Debug.WriteLine("The person has GenderId {0} pointing to Gender "{1}"", 
            person.GenderId, person.Gender.Description); 
        } 
      }

      If this helps, please point out what information was missing in the original post so that I can add it.

  3. Could you post the complete working solution please..
    I just can’t get it to work the way you have described.

  4. How did you manage to let the foreign key property immediately update the navigation property, before SaveChanges? I’ve seen this in my code, too, but only if ALL mapped properties were virtual, not just the two involved in this relationship. Also, what do you mean with “proxy”? When I call ToString on my code first entity classes, I get a lenghty string with the word “proxy” in it. Does that mean I have such proxies? Why are they not always working as expected? I haven’t disabled the change tracker or anything.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.