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

Aperta Adatta ai principianti
#5,521 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
1/5
Tempo stimato
1-3 ore
Idoneità per principianti
74/100
Tipo di issue
Documentazione
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
csharp
Ambito
documentation

Direzione di ricerca

Individua la pagina delle modifiche che introducono breaking change di EF Core 11 e leggi il formato delle voci esistenti. Aggiungi una voce a basso impatto che descriva il comportamento modificato e la mitigazione indicata nell’issue. Poi controlla i link e la formattazione della pagina. Il lavoro è terminato quando la voce spiega chiaramente l’eccezione in fase di esecuzione e consiglia istanze separate o tipi complessi.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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
Lingua principale
Mermaid
Stelle
1.7k
Fork
2k
Merge medio
1g 3h
PR unite (30g)
33

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di dotnet/EntityFramework.Docs

Tutte le issue di dotnet/EntityFramework.Docs

Issue simili

Altre issue su Documentation

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.