InvalidOperationException inesperado al intentar cambiar la relación a través del valor predeterminado de la propiedad

10

En el código de ejemplo a continuación, obtengo la siguiente excepción al hacer db.Entry(a).Collection(x => x.S).IsModified = true:

System.InvalidOperationException: 'La instancia del tipo de entidad' B 'no se puede rastrear porque ya se está rastreando otra instancia con el valor clave' {Id: 0} '. Al adjuntar entidades existentes, asegúrese de que solo se adjunte una instancia de entidad con un valor clave dado.

¿Por qué no agrega en lugar de adjuntar las instancias de B?

Curiosamente, la documentación de IsModifiedno especifica InvalidOperationExceptioncomo una posible excepción. Documentación inválida o un error?

Sé que este código es extraño, pero lo escribí solo para comprender cómo funciona ef core en algunos casos extraños de egde. Lo que quiero es una explicación, no una solución.

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();
        }
    }
}
Supremum
fuente
¿Cómo se relacionan A y B? ¿Qué significa la relación de propiedad?
Sam

Respuestas:

8

La razón del error en el código proporcionado es la siguiente.

Cuando obtiene una entidad creada Ade la base de datos, su propiedad Sse inicializa con una colección que contiene dos nuevos registros B. Idde cada una de estas nuevas Bentidades es igual 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 {} };

Después de ejecutar la línea de var a = db.Set<A>().Single()colección de código Sde entidad A, no contiene Bentidades de la base de datos, porque DbContext Dbno utiliza carga diferida y no hay carga explícita de la colección S. La entidad Asolo contiene nuevas Bentidades que se crearon durante la inicialización de la recopilación S.

Cuando solicita IsModifed = trueun Smarco de entidad de recopilación , intenta agregar esas dos nuevas entidades Bal seguimiento de cambios. Pero falla porque ambas nuevas Bentidades tienen lo mismo 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;

Puede ver en el seguimiento de la pila que el marco de la entidad intenta agregar Bentidades en 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)

Y el mensaje de error también dice que no puede rastrear la Bentidad Id = 0porque otra Bentidad con la misma Idya está rastreada.


Cómo resolver este problema.

Para resolver este problema, debe eliminar el código que crea Bentidades al inicializar la Srecopilación:

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

En su lugar, debe llenar la Scolección en el lugar donde Ase crea. Por ejemplo:

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

Si no utiliza la carga diferida, debe cargar explícitamente la Scolección para agregar sus elementos al seguimiento de cambios:

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

¿Por qué no agrega en lugar de adjuntar las instancias de B?

En resumen , se adjuntan en lugar de ser agregados porque tienen Detachedestado.

Después de ejecutar la línea de código

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

Las instancias creadas de entidad Btienen estado Detached. Se puede verificar usando el siguiente código:

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

Entonces cuando configuras

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

EF intenta agregar B entidades para cambiar el seguimiento. Desde el código fuente de EFCore , puede ver que esto nos lleva al método InternalEntityEntry.SetPropertyModified con los siguientes valores de argumento:

  • property- una de nuestras Bentidades,
  • changeState = true,
  • isModified = true,
  • isConceptualNull = false,
  • acceptChanges = true.

Este método con tales argumentos cambia el estado de las Detached Bentidades a Modified, y luego intenta comenzar a rastrearlas (véanse las líneas 490 - 506). Debido a que las Bentidades ahora tienen estado, Modifiedesto los lleva a estar unidos (no agregados).

Iliar Turdushev
fuente
¿Dónde está la respuesta para "¿Por qué no agrega en lugar de adjuntar las instancias de B?" Está diciendo "falla porque ambas nuevas entidades B tienen el mismo Id = 0". Creo que está mal porque ef core guarda tanto con 1 como con 2 identificadores. No creo que sea la respuesta correcta para esta pregunta
DIlshod K
@DIlshod K Gracias por el comentario. En la sección "Cómo resolver este problema" escribí que la colección Sdebe cargarse explícitamente, porque el código provisto no usa carga diferida. Por supuesto, EF guardó Bentidades creadas previamente en la base de datos. Pero la línea de código A a = db.Set<A>().Single()solo carga entidades Asin entidades en la colección S. Para cargar la colección, se Sdebe utilizar una carga ansiosa. Cambiaré mi respuesta para incluir explícitamente la respuesta a la pregunta "¿Por qué no agrega en lugar de adjuntar las instancias de B?".
Iliar Turdushev