Skip to content

In-Memory Corruption of Nested Owned Entities After SaveChanges with Record 'with' Expression #37581

@gustavlarson

Description

@gustavlarson

Bug description

When using C# record's with expression on an owned entity stored as JSON, EF Core corrupts the in-memory representation of nested owned entities after calling SaveChanges(). The nested properties become null in memory, causing NullReferenceException when accessed, even though the data is correctly persisted to the database.

Steps to Reproduce

  1. Create an entity with an owned entity (C# record) stored as JSON that contains nested owned entities
  2. Add the entity to the context and call SaveChanges() (first save)
  3. Use the with expression to create a modified copy of the owned entity
  4. Call SaveChanges() again (second save)
  5. Access nested properties of the owned entity

Expected Behavior

The in-memory entity graph should remain intact after SaveChanges(). All nested properties of the owned entity should be accessible with their correct values.

In my reproduction code, product.Metadata.Weight.Value should remain 5 after the second SaveChanges().

Actual Behavior

After the second SaveChanges(), nested owned entities become null in memory. Accessing product.Metadata.Weight.Value throws NullReferenceException because Weight is now null.

Important: The data IS correctly persisted to the database. This is purely an in-memory corruption of the tracked entity.

Your code

Full reproduction code including failing test can be found here: https://github.com/gustavlarson/efcore-record-bug

using Microsoft.EntityFrameworkCore;

// Entity model
public class Product
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public ProductMetadata? Metadata { get; set; }
}

public record ProductMetadata
{
    public Weight? Weight { get; init; }
    public string? Color { get; init; }
}

public record Weight
{
    public int Value { get; init; }
    public string Unit { get; init; }
}

// DbContext configuration
public class TestDbContext : DbContext
{
    public TestDbContext(DbContextOptions<TestDbContext> options) : base(options) { }
    
    public DbSet<Product> Products => Set<Product>();
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>(entity =>
        {
            entity.HasKey(e => e.Id);
            entity.Property(e => e.Name).IsRequired().HasMaxLength(200);
            entity.OwnsOne(e => e.Metadata, metadata =>
            {
                metadata.ToJson();  // Key: stored as JSON
                metadata.OwnsOne(e => e.Weight);
            });
        });
    }
}

// Reproduction
var product = new Product 
{ 
    Name = "Test Product", 
    Metadata = new() 
    { 
        Weight = new() { Value = 5 } 
    }
};

context.Products.Add(product);
await context.SaveChangesAsync();  // First save - works fine

// Use 'with' expression to create modified copy
product.Metadata = product.Metadata with { Color = product.Metadata.Color };

// At this point, product.Metadata.Weight.Value is still 5
Console.WriteLine($"Before SaveChanges: {product.Metadata.Weight.Value}"); // Prints: 5

await context.SaveChangesAsync();  // Second save - triggers bug

// BUG: Weight is now null!
Console.WriteLine($"After SaveChanges: {product.Metadata.Weight.Value}"); // NullReferenceException!

Stack traces


Verbose output


EF Core version

10.0.2

Database provider

No response

Target framework

.NET 10

Operating system

Windows 11

IDE

No response

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions