InvalidOperationException imprevisto durante il tentativo di modificare la relazione tramite il valore predefinito della proprietà


10

Nel seguente codice di esempio ottengo la seguente eccezione db.Entry(a).Collection(x => x.S).IsModified = true:

System.InvalidOperationException: 'Impossibile rilevare l'istanza del tipo di entità' B 'perché è già stata tracciata un'altra istanza con il valore chiave' {Id: 0} '. Quando si collegano entità esistenti, assicurarsi che sia allegata solo un'istanza di entità con un determinato valore chiave.

Perché non aggiunge invece di collegare le istanze di B?

Stranamente la documentazione per IsModifiednon specifica InvalidOperationExceptioncome possibile eccezione. Documentazione non valida o bug?

So che questo codice è strano, ma l'ho scritto solo per capire come funziona ef core in alcuni casi egde strani. Quello che voglio è una spiegazione, non una soluzione.

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    public class A
    {
        public int Id { get; set; }
        public ICollection<B> S { get; set; } = new List<B>() { new B {}, new B {} };
    }

    public class B
    {
        public int Id { get; set; }
    }

    public class Db : DbContext {
        private const string connectionString = @"Server=(localdb)\mssqllocaldb;Database=Apa;Trusted_Connection=True";

        protected override void OnConfiguring(DbContextOptionsBuilder o)
        {
            o.UseSqlServer(connectionString);
            o.EnableSensitiveDataLogging();
        }

        protected override void OnModelCreating(ModelBuilder m)
        {
            m.Entity<A>();
            m.Entity<B>();
        }
    }

    static void Main(string[] args)
    {
        using (var db = new Db()) {
            db.Database.EnsureDeleted();
            db.Database.EnsureCreated();

            db.Add(new A { });
            db.SaveChanges();
        }

        using (var db = new Db()) {
            var a = db.Set<A>().Single();
            db.Entry(a).Collection(x => x.S).IsModified = true;
            db.SaveChanges();
        }
    }
}

Come sono collegati A e B? significa qual è la proprietà della relazione?
sam

Risposte:


8

La ragione dell'errore nel codice fornito è la seguente.

Quando si ottiene un'entità creata Adal database, la sua proprietà Sviene inizializzata con una raccolta che contiene due nuovi record B. Iddi ciascuna di queste nuove Bentità è uguale a 0.

// This line of code reads entity from the database
// and creates new instance of object A from it.
var a = db.Set<A>().Single();

// When new entity A is created its field S initialized
// by a collection that contains two new instances of entity B.
// Property Id of each of these two B entities is equal to 0.
public ICollection<B> S { get; set; } = new List<B>() { new B {}, new B {} };

Dopo aver eseguito la riga di var a = db.Set<A>().Single()raccolta codice Sdell'entità Anon contiene Bentità dal database, poiché DbContext Dbnon utilizza il caricamento lento e non viene caricato esplicitamente la raccolta S. L'entità Acontiene solo nuove Bentità create durante l'inizializzazione della raccolta S.

Quando si chiama il framework di entità di IsModifed = trueraccolta S, si tenta di aggiungere queste due nuove entità Bal rilevamento delle modifiche. Ma fallisce perché entrambe le nuove Bentità hanno lo stesso Id = 0:

// This line tries to add to change tracking two new B entities with the same Id = 0.
// As a result it fails.
db.Entry(a).Collection(x => x.S).IsModified = true;

Dalla traccia dello stack è possibile vedere che il framework di entità tenta di aggiungere Bentità in IdentityMap:

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.SetPropertyModified(IProperty property, Boolean changeState, Boolean isModified, Boolean isConceptualNull, Boolean acceptChanges)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.SetFkPropertiesModified(InternalEntityEntry internalEntityEntry, Boolean modified)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.SetFkPropertiesModified(Object relatedEntity, Boolean modified)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.set_IsModified(Boolean value)

E il messaggio di errore dice anche che non è possibile tracciare Bun'entità con Id = 0perché un'altra Bentità con la stessa Idè già tracciata.


Come risolvere questo problema.

Per risolvere questo problema è necessario eliminare il codice che crea Bentità durante l'inizializzazione della Sraccolta:

public ICollection<B> S { get; set; } = new List<B>();

Dovresti invece riempire la Sraccolta nel luogo in cui Aè stata creata. Per esempio:

db.Add(new A {S = {new B(), new B()}});

Se non si utilizza il caricamento lento, è necessario caricare esplicitamente la Sraccolta per aggiungere i relativi elementi nel rilevamento delle modifiche:

// Use eager loading, for example.
A a = db.Set<A>().Include(x => x.S).Single();
db.Entry(a).Collection(x => x.S).IsModified = true;

Perché non aggiunge invece di collegare le istanze di B?

In breve , sono associati all'istanza di essere aggiunti perché hanno Detachedstato.

Dopo aver eseguito la riga di codice

var a = db.Set<A>().Single();

le istanze create dell'entità Bhanno stato Detached. Può essere verificato utilizzando il codice seguente:

Console.WriteLine(db.Entry(a.S[0]).State);
Console.WriteLine(db.Entry(a.S[1]).State);

Quindi quando si imposta

db.Entry(a).Collection(x => x.S).IsModified = true;

EF cerca di aggiungere Bentità per cambiare tracciamento. Dal codice sorgente di EFCore puoi vedere che questo ci porta al metodo InternalEntityEntry.SetPropertyModified con i seguenti valori dell'argomento:

  • property- una delle nostre Bentità,
  • changeState = true,
  • isModified = true,
  • isConceptualNull = false,
  • acceptChanges = true.

Questo metodo con tali argomenti modifica lo stato delle Detached Bentità in Modified, quindi tenta di avviare il tracciamento per esse (vedere le righe 490 - 506). Poiché le Bentità ora hanno uno stato, Modifiedciò li porta ad essere collegati (non aggiunti).


Dov'è la risposta per "Perché non aggiunge invece di allegare le istanze di B?" Stai dicendo "fallisce perché entrambe le nuove entità B hanno lo stesso Id = 0". Penso che sia sbagliato perché ef core salva entrambi con 1 e 2 ID. Non credo sia la risposta corretta a questa domanda
DIlshod K

@DIlshod K Grazie per il commento. Nella sezione "Come risolvere questo problema" ho scritto che la raccolta Sdeve essere caricata in modo esplicito, poiché il codice fornito non utilizza il caricamento lento. Naturalmente, EF ha salvato Bentità precedentemente create nel database. Ma la riga di codice A a = db.Set<A>().Single()carica solo entità Asenza entità nella raccolta S. Per caricare il Scaricatore desideroso di raccolta dovrebbe essere utilizzato. Cambierò il mio asnwer per includere esplicitamente la risposta alla domanda "Perché non aggiunge invece di allegare le istanze di B?".
Iliar Turdushev,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.