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
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.
using TestEFContext context = new(options);
Main main = new();
Sub sub = new();
main.Subs.Add(sub);Result:

EntityState:
Main State:Detached
Sub State:DetachedExample 2: Only Parent is Tracked
When main is added to tracking, it automatically tracks sub, and sub.Main is updated to main.
using TestEFContext context = new(options);
Main main = new();
Sub sub = new();
main.Subs.Add(sub);
context.Mains.Add(main);Result:

EntityState:
Main State:Added
Sub State:AddedExample 3: Only Child is Tracked
If only sub is tracked and main is not, sub.Main will not be updated.
using TestEFContext context = new(options);
Main main = new();
Sub sub = new();
main.Subs.Add(sub);
context.Subs.Add(sub);Result:

EntityState:
Main State:Detached
Sub State:AddedExample 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().
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():

Result after calling SaveChanges():

EntityState:
Before SaveChanges:
Main State:Added
Sub State:Added
After SaveChanges:
Main State:Unchanged
Sub State:UnchangedTIP
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.
using TestEFContext context = new(options);
Main main = new();
Sub sub = new();
sub.Main = main;Result:

EntityState:
Main State:Detached
Sub State:DetachedExample 6: Parent is Tracked
When main is tracked but sub is not, main.Subs remains an empty collection.
using TestEFContext context = new(options);
Main main = new();
Sub sub = new();
sub.Main = main;
context.Mains.Add(main);Result:

EntityState:
Main State:Added
Sub State:DetachedExample 7: Only Child is Tracked
When only the child is tracked, it will automatically track main, and main.Subs will include sub.
using TestEFContext context = new(options);
Main main = new();
Sub sub = new();
sub.Main = main;
context.Subs.Add(sub);Result:

EntityState:
Main State:Added
Sub State:AddedSetting 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.
using TestEFContext context = new(options);
Main main = new() {
Id = 1L
};
Sub sub = new (){
Id = 2L,
MainId = 1L
};
context.Subs.Add(sub);Result:

EntityState:
Main State:Detached
Sub State:AddedExample 9: Both Parent and Child are Tracked
When both main and sub are tracked, the navigation properties are automatically synchronized.
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:

EntityState:
Main State:Added
Sub State:AddedExample 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().
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():

Result after calling SaveChanges():

EntityState:
Before SaveChanges:
Main State:Added
Sub State:Added
After SaveChanges:
Main State:Unchanged
Sub State:UnchangedExample 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.
using TestEFContext context = new(options);
Sub sub = new() {
Id = 3L
};
context.Subs.Add(sub);
sub.MainId = 1L;
Main main = context.Mains.Find(1L);Result:

EntityState:
Main State:Unchanged
Sub State:AddedExample 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.
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:

EntityState:
Main1 State:Unchanged
Main2 State:Added
Sub State:AddedOther Operations
Example 13: SaveChanges() Failure
Even if SaveChanges() fails, the navigation properties are still synchronized.
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:

EntityState:
Before SaveChanges:
Main State:Added
Sub State:Added
After SaveChanges:
Main State:Added
Sub State:AddedExample 14: Using Entry() to Get EntityEntry
Executing Entry() also synchronizes the navigation properties of tracked entities.
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:

Conclusion
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.
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.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.
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,subis 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, usingmain.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 tonullwhen the association is broken, rather than deleting the associated child table data.
Change Log
- 2024-08-12 Initial version created.
