Entity Framework Core Navigation Property vs Foreign Key Property — FIGHT!

9/30/2019 4:27:40 PM UPDATE: Apparently, things changed since .NET Core 3 Preview 5 which is when I wrote this post. I’ll update the post as soon as I make sense of all my unit tests that are failing since updating to .NET Core 3 RTM.

Suppose we have an entity A that has a direct, 1:1, relationship with entity B. So…

class A {
    public int Id { get; set; }
    public BId { get; set; }
    public B B { get; set; }
}

class B {
    public int Id { get; set; }
}

What happens if we try to assign two different Bs to an A like this?

var b1 = new B();
var b2 = new B();

var a = new A { 
    BId = b1,
    B = b2
};

context.A.Add(a);
context.SaveChanges();

b1 wins. A.B is set to b1 and A.BId is set to b1.Id.

How did I know that? I wrong a bunch of unit tests to find out. I couldn’t find it documented anywhere and my lack of understanding what seriously hindering my ability to use EF properly.

Here are the rules I came up with:

  1. In general, setting the foreign key property always trumps setting the navigation property. The only exception to the rule is when you set the foreign key property to null and the navigation property to a valid entity when the parent entity is already associated with a child entity. See ForeignKeySmarts11 below.
  2. When editing an entity, you only need to set the foreign key property or the navigation property. When you call SaveChanges, the property you didn’t set will be synchronized with the property that you did set. Setting both properties is fine, and in some cases may be the preference if you need to interrogate the properties before calling SaveChanges.

Given the strange quirk in rule #1, I recommend always setting the foreign key property and only the foreign key property if possible.

At this point, I’ll shut up and just show you the tests. Some details within the test have been removed to make them easier to read.

[Test]
public async Task ForeignKeySmarts1()
{
    var b = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", B = b.Entity });

    Assert.That(a.Entity.B, Is.Not.Null);
    Assert.That(a.Entity.B?.Id, Is.EqualTo(b.Entity.Id));
}


[Test]
public async Task ForeignKeySmarts3()
{
    var b = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b.Entity.Id });

    Assert.That(a.Entity.B, Is.Not.Null);
    Assert.That(a.Entity.B?.Id, Is.EqualTo(b.Entity.Id));
}

[Test]
public async Task ForeignKeySmarts4()
{
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id });

    var b2 = await AddB("Another");

    a.Entity.B = b2.Entity;
    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B.Id, Is.EqualTo(b2.Entity.Id));
    Assert.That(a.Entity.B.Name, Is.EqualTo(b2.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts5()
{
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id });

    var b2 = await AddB("Another");

    a.Entity.BId = b2.Entity.Id;
    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B?.Id, Is.EqualTo(b2.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b2.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts10()
{
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id });

    var b2 = await AddB("Another");

    a.Entity.BId = b2.Entity.Id;
    a.Entity.B = null;

    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B?.Id, Is.EqualTo(b2.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b2.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts11()
{
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id });

    var b2 = await AddB("Another");

    a.Entity.BId = null;
    a.Entity.B = b2.Entity;

    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B?.Id, Is.EqualTo(b2.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b2.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts6()
{
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id });

    var b2 = await AddB("Another");
    var b3 = await AddB("Dawg");

    a.Entity.BId = b2.Entity.Id;
    a.Entity.B = b3.Entity;

    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B?.Id, Is.EqualTo(b2.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b2.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts7()
{
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id });

    var b2 = await AddB("Another");
    var b3 = await AddB("Dawg");

    a.Entity.BId = b3.Entity.Id;
    a.Entity.B = b2.Entity;

    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B?.Id, Is.EqualTo(b3.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b3.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts2()
{
    var b1 = await AddB("Something");
    var b2 = await AddB("Another");

    var a = await AddA(new A { Name = "Yum",
        B = b1.Entity,
        BId = b2.Entity.Id
    });

    Assert.That(a.Entity.B, Is.Not.Null);
    Assert.That(a.Entity.B?.Id, Is.GreaterThan(0));
    Assert.That(a.Entity.B?.Id, Is.EqualTo(b2.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b2.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts8()
{
    var b1 = await AddB("Something");
    var b2 = await AddB("Another");

    var a = await AddA(new A { Name = "Yum",
        B = b2.Entity,
        BId = b1.Entity.Id
    });

    Assert.That(a.Entity.B, Is.Not.Null);
    Assert.That(a.Entity.B?.Id, Is.GreaterThan(0));
    Assert.That(a.Entity.B?.Id, Is.EqualTo(b1.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b1.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts9()
{
    var b2 = await AddB("Another");
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum",
        B = b2.Entity,
        BId = b1.Entity.Id
    });

    Assert.That(a.Entity.B, Is.Not.Null);
    Assert.That(a.Entity.B?.Id, Is.GreaterThan(0));
    Assert.That(a.Entity.B?.Id, Is.EqualTo(b1.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b1.Entity.Name));
}

/// <param name="intelligencePoint"></param>
private async Task<EntityEntry<A>> AddA(A intelligencePoint)
{
    var a = await _context.As.AddAsync(intelligencePoint);
    await _context.SaveChangesAsync();
    return a;
}

/// <param name="name"></param>
private async Task<EntityEntry<B>> AddB(string name)
{
    var b = await _context.Bs.AddAsync(new B { Name = name });
    await _context.SaveChangesAsync();
    return b;
}

[Test]
public async Task ForeignKeySmarts12()
{
    var b1 = await AddB("Something");
    var b2 = await AddB("Another");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id, B = b2.Entity});

    Assert.That(a.Entity.B?.Id, Is.EqualTo(b1.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b1.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts13()
{
    var b1 = await AddB("Something");
    var b2 = await AddB("Another");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id});

    a.Entity.B = b2.Entity;
    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B?.Id, Is.EqualTo(b2.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.EqualTo(b2.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts14()
{
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id});

    a.Entity.B = new B
    {
        Name = "cow"
    };

    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B?.Id, Is.Not.EqualTo(b1.Entity.Id));
    Assert.That(a.Entity.B?.Name, Is.Not.EqualTo(b1.Entity.Name));
}

[Test]
public async Task ForeignKeySmarts15()
{
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id});

    a.Entity.BId = null;

    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B?.Id, Is.Null);
    Assert.That(a.Entity.B?.Name, Is.Null);
}

[Test]
public async Task ForeignKeySmarts16()
{
    var b1 = await AddB("Something");

    var a = await AddA(new A { Name = "Yum", BId = b1.Entity.Id});

    a.Entity.B = null;

    await _context.SaveChangesAsync();

    Assert.That(a.Entity.B?.Id, Is.Null);
    Assert.That(a.Entity.B?.Name, Is.Null);
}

Published by alexdresko

To learn more about me, check the "About Me" page on this site.

Scroll Up