On this page

Skip to content

A Brief Discussion on Synchronizing Navigation Properties and Foreign Keys in Entity Framework

Recently, I had to handle some requirements that involved these concepts. To avoid any mistakes or misinformation, I decided to verify the behavior myself.

This article uses "Microsoft.EntityFrameworkCore 8" to test the association behavior between parent and child tables. Unless otherwise specified, the results below represent the state before calling SaveChanges(). Please note that results may vary slightly across different versions of Entity Framework.

Entity Structure Definition

csharp
public partial class Main {
    public long Id { get; set; }

    public virtual ICollection<Sub> Subs { get; set; } = new List<Sub>();
}

public partial class Sub {
    public long Id { get; set; }

    public long MainId { get; set; }

    public virtual Main Main { get; set; }
}

public partial class TestEFContext : DbContext {
    public TestEFContext(DbContextOptions<TestEFContext> options)
        : base(options) {
    }

    public virtual DbSet<Main> Mains { get; set; }

    public virtual DbSet<Sub> Subs { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder) {
        modelBuilder.Entity<Main>(entity => {
            entity.ToTable("Main");

            entity.Property(e => e.Id).ValueGeneratedNever();
        });

        modelBuilder.Entity<Sub>(entity => {
            entity.ToTable("Sub");

            entity.Property(e => e.Id).ValueGeneratedNever();

            entity.HasOne(d => d.Main).WithMany(p => p.Subs)
                .HasForeignKey(d => d.MainId)
                .OnDelete(DeleteBehavior.ClientSetNull)
                .HasConstraintName("FK_Sub_Main");
        });

        OnModelCreatingPartial(modelBuilder);
    }

    partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

Associating Child Tables via Parent Navigation Properties

Example 1: Parent and Child are Untracked

If both main and sub are not tracked, sub.Main remains null.

csharp
using TestEFContext context = new(options);
Main main = new();
Sub sub = new();
main.Subs.Add(sub);

Result:

ef sync result 1

EntityState:

text
Main State:Detached
Sub State:Detached

Example 2: Only Parent is Tracked

When main is added to tracking, it automatically tracks sub, and sub.Main is updated to main.

csharp
using TestEFContext context = new(options);
Main main = new();
Sub sub = new();
main.Subs.Add(sub);
context.Mains.Add(main);

Result:

ef sync result 2

EntityState:

text
Main State:Added
Sub State:Added

Example 3: Only Child is Tracked

If only sub is tracked and main is not, sub.Main will not be updated.

csharp
using TestEFContext context = new(options);
Main main = new();
Sub sub = new();
main.Subs.Add(sub);
context.Subs.Add(sub);

Result:

ef sync result 3

EntityState:

text
Main State:Detached
Sub State:Added

Example 4: Tracking Parent First, Then Setting Navigation Property

Track main first, then execute main.Subs.Add(sub). sub.Main is null initially, but it synchronizes after calling SaveChanges().

csharp
using TestEFContext context = new(options);
Main main = new();
Sub sub = new();
context.Mains.Add(main);
main.Subs.Add(sub);

context.SaveChanges();

Result before calling SaveChanges():

ef sync before save 1

Result after calling SaveChanges():

ef sync after save 1

EntityState:

text
Before SaveChanges:
Main State:Added
Sub State:Added
After SaveChanges:
Main State:Unchanged
Sub State:Unchanged

TIP

The reason Sub State is Added is likely because checking the Sub State via context.Entry(sub).State triggers the change tracking for the navigation property.

Associating Parent Tables via Child Navigation Properties

Testing different scenarios for setting navigation properties on the child table:

Example 5: Parent and Child are Untracked

If you set sub.Main = main directly while both are untracked, main.Subs remains an empty collection.

csharp
using TestEFContext context = new(options);
Main main = new();
Sub sub = new();
sub.Main = main;

Result:

ef sync result 4

EntityState:

text
Main State:Detached
Sub State:Detached

Example 6: Parent is Tracked

When main is tracked but sub is not, main.Subs remains an empty collection.

csharp
using TestEFContext context = new(options);
Main main = new();
Sub sub = new();
sub.Main = main;
context.Mains.Add(main);

Result:

ef sync result 5

EntityState:

text
Main State:Added
Sub State:Detached

Example 7: Only Child is Tracked

When only the child is tracked, it will automatically track main, and main.Subs will include sub.

csharp
using TestEFContext context = new(options);
Main main = new();
Sub sub = new();
sub.Main = main;
context.Subs.Add(sub);

Result:

ef sync result 6

EntityState:

text
Main State:Added
Sub State:Added

Setting Associations via Foreign Key Properties

Example 8: Only Child is Tracked

If only sub is tracked and the foreign key MainId is set on sub, neither the main nor the sub navigation properties will be synchronized.

csharp
using TestEFContext context = new(options);
Main main = new() {
    Id = 1L
};
Sub sub = new (){
    Id = 2L,
    MainId = 1L
};
context.Subs.Add(sub);

Result:

ef sync result 7

EntityState:

text
Main State:Detached
Sub State:Added

Example 9: Both Parent and Child are Tracked

When both main and sub are tracked, the navigation properties are automatically synchronized.

csharp
using TestEFContext context = new(options);
Main main = new() {
    Id = 1L
};
Sub sub = new () {
    Id = 2L,
    MainId = 1L
};
context.Mains.Add(main);
context.Subs.Add(sub);

Result:

ef sync result 8

EntityState:

text
Main State:Added
Sub State:Added

Example 10: Setting Foreign Key After Tracking

If the foreign key is set after adding to tracking, the navigation property will not synchronize automatically, but it will update after calling SaveChanges().

csharp
using TestEFContext context = new(options);
Main main = new() {
    Id = 1L
};
Sub sub = new () {
    Id = 2L
};

context.Mains.Add(main);
context.Subs.Add(sub);
sub.MainId = 1L;

context.SaveChanges();

Result before calling SaveChanges():

ef sync before save 2

Result after calling SaveChanges():

ef sync after save 2

EntityState:

text
Before SaveChanges:
Main State:Added
Sub State:Added
After SaveChanges:
Main State:Unchanged
Sub State:Unchanged

Example 11: Using Find() to Retrieve Tracked Parent

Create and track sub first, then use Find() to retrieve the associated Main data. main.Subs will include sub.

csharp
using TestEFContext context = new(options);
Sub sub = new() {
    Id = 3L
};

context.Subs.Add(sub);
sub.MainId = 1L;

Main main = context.Mains.Find(1L);

Result:

ef sync result 9

EntityState:

text
Main State:Unchanged
Sub State:Added

Example 12: Using Find() to Retrieve Untracked Parent

If you track sub first, then use Find() to retrieve Main data that is unrelated to the locally tracked entity, the navigation property will not synchronize automatically.

csharp
using TestEFContext context = new(options);
Main main2 = new() {
    Id = 2L
};
Sub sub = new() {
    Id = 4L
};

context.Mains.Add(main2);
context.Subs.Add(sub);
sub.MainId = 2L;

Main main1 = context.Mains.Find(1L);

Result:

ef sync result 10

EntityState:

text
Main1 State:Unchanged
Main2 State:Added
Sub State:Added

Other Operations

Example 13: SaveChanges() Failure

Even if SaveChanges() fails, the navigation properties are still synchronized.

csharp
using TestEFContext context = new(options);
// Intentionally write data with an existing ID
Main main = new() {
    Id = 1L
};
Sub sub = new() {
    Id = 2L
};

try {
    context.Mains.Add(main);
    context.Subs.Add(sub);
    sub.MainId = 1L;
    context.SaveChanges();
} catch {
}
Console.ReadLine();

Result:

ef sync result 11

EntityState:

text
Before SaveChanges:
Main State:Added
Sub State:Added
After SaveChanges:
Main State:Added
Sub State:Added

Example 14: Using Entry() to Get EntityEntry

Executing Entry() also synchronizes the navigation properties of tracked entities.

csharp
using TestEFContext context = new(options);
Main main = new() {
    Id = 1L
};
Sub sub = new() {
    Id = 2L
};

context.Mains.Add(main);
context.Subs.Add(sub);
sub.MainId = 1L;

context.Entry(main);
context.Entry(sub);

Result:

ef sync result 12

Conclusion

  1. Tracking and Navigation Property Synchronization:

    The prerequisite for navigation property synchronization is that both entities must be in a tracked state. Any operation that causes an entity state change, such as adding, deleting, or manually setting the entity state, triggers a tracking state check, which in turn automatically synchronizes the navigation properties.

  2. Database Updates and Navigation Properties:

    Whether or not navigation properties are synchronized does not affect the actual database update. Even if navigation properties are not synchronized, the system performs an entity change tracking check when SaveChanges() is executed, which automatically triggers the synchronization of navigation properties.

  3. Foreign Key Properties and Synchronization:

    When an entity triggers a change tracking check, not only are navigation properties synchronized, but foreign key properties also participate in the synchronization process. Therefore, you can use foreign key properties to influence the values of navigation properties.

  4. Impact of Retrieving Data from the Database:

    When data is read from the database and added to tracking, the associated local entity navigation properties are automatically synchronized.

Supplementary Notes

  • Adding data using navigation properties: When using main.Subs.Add(sub) to set a navigation property, sub is automatically tracked. This method is intended to allow adding associated child table data when adding parent table data.
  • Deleting data: If you need to delete child table data, you should use context.Subs.Remove(sub) to ensure the data is removed from the database. Conversely, using main.Subs.Remove(sub) only breaks the association between the parent and child, and does not delete the child table data; the child data remains in the database.
  • Deleting associations: You should use main.Subs.Remove(sub) in the following scenarios:
    • Many-to-many associations: In many-to-many associations, the relationship between two entities is implemented via a join table. When you use main.Subs.Remove(sub) to break the association, it only removes the association record from the join table and does not affect the data in the parent or child tables.

    • Foreign key property allows null:

      If the foreign key property allows null, the system will set the foreign key property to null when the association is broken, rather than deleting the associated child table data.

Change Log

  • 2024-08-12 Initial version created.