Skip to content

Problem masking & unhelpful exception when loading entity with inconsistent data in owned entity columns #38223

@fschmied

Description

@fschmied

Bug description

Consider a scenario with:

  • a root entity,
  • Outer: an outer owned entity owned by the root entity, and
  • Inner: an inner (nested) owned entity owned by Outer.

Further consider that a row exists in the database with a NULL value in a column that maps to a non-nullable property of Outer. At the same time, columns that map to properties of Inner are present.

This is an inconsistent state in the database, Outer is "partially present" in the underlying data. Still, EF Core will apparently successfully load the root entity, with Outer being null.

When you subsequently try to replace Outer with a (valid) new instance, SaveChanges throws an InvalidOperationException with the message

The instance of entity type 'Inner' cannot be tracked because another instance with the same key value … is already being tracked.

It's very hard to find the root cause of the problem from this exception. But if you don't trigger the exception (e.g., in a read-only code path), it's even more problematic because EF Core will pretend that Outer is null even though there is some data for it in the database. In my opinion, EF Core should throw an exception with a clear error message upon encountering such an inconsistency while hydrating an entity.

This is aggravated by EF Core scaffolding migrations that will produce this state by default (see #25359 ).

Your code

// Add: <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.7" />

using System.Diagnostics;
using Microsoft.EntityFrameworkCore;

try
{
    await using var context = new TestDbContext();

    // Create database, recreating it if it already exists
    context.Database.EnsureDeleted();
    context.Database.EnsureCreated();

    // Insert a root entity with inconsistent data
    var rootId = Guid.NewGuid();
                
    context.Database.ExecuteSql($@"
                    INSERT INTO RootEntities (Id, Outer_RequiredProperty, Outer_Inner_InnerProperty)
                    VALUES ({rootId}, NULL, 42)
                ");

    Console.WriteLine("Database seeded with inconsistent state.");

    // Now try to load and modify - this should trigger the bug
    Console.WriteLine("Attempting to load and modify the entity...\n");
                
    var root = await context.RootEntities.FindAsync(rootId);
    Trace.Assert(root != null);

    Console.WriteLine($"Loaded root entity with Id: {root.Id}");
    Console.WriteLine($"Outer: {root.Outer?.ToString() ?? "<null>"}");
    Console.WriteLine();

    // Try to replace the owned entity with a new valid instance
    Console.WriteLine("Attempting to replace the outer owned entity with a valid instance...");
    root.Outer = new Outer
        { 
            RequiredProperty = 1,

            Inner = new Inner
            {
                InnerProperty = 2
            }
        };

    context.SaveChanges();
    Console.WriteLine("✓ Successfully saved the changes.");
}
catch (Exception ex)
{
    Console.WriteLine();
    Console.WriteLine($"Exception caught: {ex}");
}

// Entity models
public class RootEntity
{
    public Guid Id { get; set; }
    public Outer? Outer { get; set; }
}

public record Outer
{
    public required int RequiredProperty { get; set; }

    public required Inner? Inner { get; set; }
}

public record Inner
{
    public required int InnerProperty { get; set; }
}

// DbContext
public class TestDbContext : DbContext
{
    public DbSet<RootEntity> RootEntities { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(
            "Server=(localdb)\\mssqllocaldb;Database=EFCoreBugRepro;Integrated Security=true;");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<RootEntity>(entity =>
        {
            entity.HasKey(e => e.Id);
            entity.OwnsOne(e => e.Outer, outer =>
            {
                outer.OwnsOne(o => o.Inner);
            });
        });
    }
}

Stack traces

System.InvalidOperationException: The instance of entity type 'Inner' cannot be tracked because another instance with the same key value for {'OuterRootEntityId'} is already being tracked. When replacing owned entities, modify the properties without changing the instance or detach the previous owned entity entry first. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.ThrowIdentityConflict(InternalEntityEntry entry)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry, Boolean updateDuplicate)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(InternalEntityEntry entry)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.OnStateChanging(EntityState newState)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntryBase.SetEntityState(EntityState oldState, EntityState newState, Boolean acceptChanges, Boolean modifyProperties)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState entityState, Boolean acceptChanges, Boolean modifyProperties, Nullable`1 forceStateWhenUnknownKey, Nullable`1 fallbackState)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.PaintAction(EntityEntryGraphNode`1 node)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1 node, Func`2 handleNode)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1 node, Func`2 handleNode)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.AttachGraph(InternalEntityEntry rootEntry, EntityState targetState, EntityState storeGeneratedWithKeySetTargetState, Boolean forceStateWhenUnknownKey)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.NavigationReferenceChanged(InternalEntityEntry entry, INavigationBase navigationBase, Object oldValue, Object newValue)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntryNotifier.NavigationReferenceChanged(InternalEntityEntry entry, INavigation navigation, Object oldValue, Object newValue)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectNavigationChange(InternalEntityEntry entry, INavigationBase navigationBase)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.LocalDetectChanges(InternalEntityEntry entry)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectChanges(IStateManager stateManager)
   at Microsoft.EntityFrameworkCore.ChangeTracking.ChangeTracker.DetectChanges()
   at Microsoft.EntityFrameworkCore.DbContext.TryDetectChanges()
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges()
   at Program.<Main>$(String[] args) in C:\Users\fschmied\...\ReproSample\Program.cs:line 44
   at Program.<Main>$(String[] args) in C:\Users\fschmied\...\ReproSample\Program.cs:line 45

Verbose output


EF Core version

10.0.7

Database provider

Microsoft.EntityFrameworkCore.SqlServer

Target framework

.NET 10

Operating system

No response

IDE

not relevant

Metadata

Metadata

Assignees

Type

No fields configured for Bug.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions