EF Core 11 breaking changes: assigning the same owned instance to more than one navigation now throws

Open Beginner friendly
#5,521 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
1/5
Estimated time
1-3 hours
Newbie friendliness
74/100
Issue type
Documentation
Clarity
Clearly specified
Activity status
Active
Tech stack
csharp
Domain
documentation

Research direction

Find the EF Core 11 breaking changes page and read its existing entry format. Add a low impact entry describing the changed behavior and mitigation from the issue, then check the page’s links and formatting. Done when the entry clearly explains the runtime exception and recommends separate instances or complex types.

Written by the indexing model from the issue text.

Description

Issue or suggestion

dotnet/efcore#38446 (11.0 preview 6) made EF throw when the same owned instance is referenced from more than one ownership navigation. The change isn't listed on the 11.0 breaking changes page, and it caught us during the upgrade because one of these patterns saved fine on 10.0:

var customer = db.Customers.Single(c => c.Id == 1);
customer.Billing = customer.Shipping;
db.SaveChanges();

10.0.15 saves it: both column sets get the Shipping values. 11.0.0 throws:

InvalidOperationException: The same object instance of type 'Address' is being referenced from the navigation 'Shipping' on 'Customer' and from the navigation 'Billing' on 'Customer'. Owned entity types cannot have multiple owners pointing to the same instance. Consider creating a copy of the object to use for each navigation.

The PR description says the old behavior was a confusing FK error at SaveChanges. That's what happens in some cases, but not in this one. There's also a second case that didn't throw on 10.0: when two new owners share an optional owned instance, 10.0 saves both, and the first owner silently ends up with null for that navigation (repro below). So the new check is a good change. It's just that code that ran on 10 now fails at runtime after the upgrade, and nothing at build time points to it.

Could this get a low impact entry on the breaking changes page? Roughly:

  • Old behavior: sharing an owned instance either failed at SaveChanges with an unrelated error, or saved. When it saved, two navigations of a tracked owner both got the same values, or one of two new owners lost the value.
  • New behavior: InvalidOperationException when the entity is tracked or on DetectChanges.
  • Mitigation: give each navigation its own instance (copy it), or map the type as a complex type, which has value semantics.

I can open a PR for it if that helps.

Repro (SQLite):

#:package [email protected]
#:property PublishAot=false

using Microsoft.EntityFrameworkCore;

using (var db = new AppDb())
{
    db.Database.EnsureDeleted();
    db.Database.EnsureCreated();
    db.Add(new Customer { Id = 1, Shipping = new Address { City = "Paris" }, Billing = new Address { City = "Lyon" } });
    db.SaveChanges();
}

Run("Existing customer: Billing = Shipping", db =>
{
    var customer = db.Customers.Single(c => c.Id == 1);
    customer.Billing = customer.Shipping;
    db.SaveChanges();
});

Run("Two new customers share one optional Billing instance", db =>
{
    var billing = new Address { City = "Berlin" };
    db.Add(new Customer { Id = 2, Shipping = new Address { City = "Rome" }, Billing = billing });
    db.Add(new Customer { Id = 3, Shipping = new Address { City = "Oslo" }, Billing = billing });
    db.SaveChanges();
});

using (var db = new AppDb())
{
    foreach (var c in db.Customers.AsNoTracking().OrderBy(c => c.Id))
    {
        Console.WriteLine($"Customer {c.Id}: Shipping={c.Shipping.City}, Billing={c.Billing?.City ?? "<null>"}");
    }
}

static void Run(string name, Action<AppDb> action)
{
    Console.WriteLine(name);
    try
    {
        using var db = new AppDb();
        action(db);
        Console.WriteLine("  saved");
    }
    catch (Exception e)
    {
        Console.WriteLine($"  {e.GetType().Name}: {e.Message}");
    }
}

public class Customer
{
    public int Id { get; set; }
    public Address Shipping { get; set; } = null!;
    public Address? Billing { get; set; }
}

public class Address
{
    public string City { get; set; } = "";
}

public class AppDb : DbContext
{
    public DbSet<Customer> Customers => Set<Customer>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite("Data Source=repro.db");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Customer>(b =>
        {
            b.Property(c => c.Id).ValueGeneratedNever();
            b.OwnsOne(c => c.Shipping);
            b.OwnsOne(c => c.Billing);
        });
    }
}

Output with 11.0.0:

Existing customer: Billing = Shipping
  InvalidOperationException: The same object instance of type 'Address' is being referenced from the navigation 'Shipping' on 'Customer' and from the navigation 'Billing' on 'Customer'. Owned entity types cannot have multiple owners pointing to the same instance. Consider creating a copy of the object to use for each navigation.
Two new customers share one optional Billing instance
  InvalidOperationException: The same object instance of type 'Address' is being referenced from the navigation 'Billing' on 'Customer' and from the navigation 'Billing' on 'Customer'. Owned entity types cannot have multiple owners pointing to the same instance. Consider creating a copy of the object to use for each navigation.
Customer 1: Shipping=Paris, Billing=Lyon

Output with 10.0.15:

Existing customer: Billing = Shipping
  saved
Two new customers share one optional Billing instance
  saved
Customer 1: Shipping=Paris, Billing=Paris
Customer 2: Shipping=Rome, Billing=<null>
Customer 3: Shipping=Oslo, Billing=Berlin
Dominant language
Mermaid
Stars
1.7k
Forks
2k
Avg merge
1d 3h
Merged PRs (30d)
33

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/EntityFramework.Docs

All issues in dotnet/EntityFramework.Docs

Similar issues

More Documentation issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.