Required dependent of optional dependent is not materialized/updated properly
- Dominant language
- C#
- Stars
- 14.8k
- Forks
- 3.4k
- PR merge metrics
- PR metrics pending
Description
An aggregate root has a 1:0..1 optional dependent with some required properties, using table splitting. This dependent entity has a 1:1 required dependent where all properties are nullable (that's why this nav is required). After the optional dependent has saved values, changes on the required dependent entity are not saved.
### Steps to reproduce
Build & run.
Observe the last issued SQL command and "State before save:" line on the console output.
If the required dependent is replaced: "State before save: Added" and an update command.
If the required dependent is reused: "State before save: Detached" and no update command.
(note that both City and Street columns are updated in the former case despite the Street not changing)
```c#
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
// Prep the db & seed
using( var db = new AppDb() )
{
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
db.Parents.Add(new() { Id = "x", Child = new() { Name = "asd" } });
db.SaveChanges();
}
// reproduce the issue
using( var db = new AppDb() )
{
var parent = db.Parents.Single();
// This will update
//parent.Child!.Address = new() { City = "asd" };
// This will not
parent.Child!.Address.City = "asd";
db.ChangeTracker.DetectChanges();
Console.WriteLine($"State before save: {db.Entry(parent.Child.Address).State}");
db.SaveChanges();
}
// Model
class AppDb : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
builder
.UseSqlServer("Data Source=localhost;Initial Catalog=Test;Integrated Security=true")
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging()
;
}
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity().OwnsOne(e => e.Child).OwnsOne(e => e.Address);
}
public DbSet Parents => this.Set();
}
class Parent
{
public required string Id { get; set; }
public Child? Child { get; set; }
}
class Child
{
public required string Name { get; set; }
public Address Address { get; set; } = new();
}
class Address
{
// All optional, hence the navigation to this entity is required
public string? City { get; set; }
public string? Street { get; set; }
}
```
First I thought this has to do with owned entities and thus part of #24581 and #1985 however it repros with plain old navigations. Replace OnModelCreating with the following (basically maps to the same table structure), steps and outcome is the same
```c#
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity().ToTable("Parents");
builder.Entity().HasOne(e => e.Child).WithOne().HasForeignKey();
builder.Entity().Navigation(e => e.Child).AutoInclude();
builder.Entity().Property("Id");
builder.Entity().HasKey("Id");
builder.Entity().ToTable("Parents");
builder.Entity().HasOne(e => e.Address).WithOne().HasForeignKey
builder.Entity().Navigation(e => e.Address).IsRequired().AutoInclude();
builder.Entity().Property("Id");
builder.Entity().HasKey("Id");
builder.Entity().ToTable("Parents");
}
```
Some additional observations:
* the navigation is required but warns OptionalDependentWithAllNullPropertiesWarning[20704] and neither the Address nor the City and Street setters are hit on materialization
* the Address is Detached on materialization, it should be Unchanged
* even though Detached the change tracker still maintains some knowledge of the Address or else the replace/reuse behavior could not be possibly different. It's just an unknown object.
### Include provider and version information
EF Core version: 8.0.8
Database provider: Microsoft.EntityFrameworkCore.SqlServer (probably others)
Target framework: .NET 8.0
Contributor guide
Assessment
This issue has not been assessed yet.