InvalidOperationException inattendue lors de la tentative de modification de la relation via la valeur par défaut de la propriété

10

Dans l'exemple de code ci-dessous, j'obtiens l'exception suivante lors de l'exécution db.Entry(a).Collection(x => x.S).IsModified = true:

System.InvalidOperationException: 'L'instance de type d'entité' B 'ne peut pas être suivie car une autre instance avec la valeur de clé' {Id: 0} 'est déjà en cours de suivi. Lorsque vous attachez des entités existantes, assurez-vous qu'une seule instance d'entité avec une valeur de clé donnée est attachée.

Pourquoi n'ajoute-t-il pas au lieu de joindre les instances de B?

Étrangement, la documentation de IsModifiedne spécifie pas d' InvalidOperationExceptionexception possible. Documentation invalide ou bug?

Je sais que ce code est étrange, mais je l'ai écrit uniquement pour comprendre comment le noyau ef fonctionne dans certains cas bizarres. Ce que je veux, c'est une explication, pas une solution.

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
la source
Comment sont liés A et B? ce qui signifie quelle est la propriété de la relation?
sam

Réponses:

8

La raison de l'erreur dans le code fourni est la suivante.

Lorsque vous obtenez une entité créée à Apartir de la base de données, sa propriété Sest initialisée avec une collection qui contient deux nouveaux enregistrements B. Idde chacune de ces nouvelles Bentités est égal à 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 {} };

Après avoir exécuté la ligne de code, la var a = db.Set<A>().Single()collection Sd'entité Ane contient pas d' Bentités de la base de données, car DbContext Dbn'utilise pas de chargement différé et il n'y a pas de chargement explicite de la collection S. L'entité Acontient uniquement les nouvelles Bentités créées lors de l'initialisation de la collection S.

Lorsque vous appelez le framework d'entité de IsModifed = truecollecte, essayez Sd'ajouter ces deux nouvelles entités Bau suivi des modifications. Mais cela échoue car les deux nouvelles Bentités ont la même 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;

Vous pouvez voir dans la trace de la pile que le framework d'entité essaie d'ajouter des Bentités dans 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)

Et le message d'erreur indique également qu'il ne peut pas suivre l' Bentité avec Id = 0car une autre Bentité avec la même chose Idest déjà suivie.


Comment résoudre ce problème.

Pour résoudre ce problème, vous devez supprimer le code qui crée des Bentités lors de l'initialisation de la Scollection:

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

Au lieu de cela, vous devez remplir la Scollection à l'endroit où Aest créé. Par exemple:

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

Si vous n'utilisez pas le chargement différé, vous devez explicitement charger la Scollection pour ajouter ses éléments dans le suivi des modifications:

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

Pourquoi n'ajoute-t-il pas au lieu de joindre les instances de B?

Bref , ils sont attachés au lieu d'être ajoutés car ils ont de l' Detachedétat.

Après avoir exécuté la ligne de code

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

les instances d'entité créées Bont un état Detached. Il peut être vérifié à l'aide du code suivant:

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

Ensuite, lorsque vous définissez

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

EF essaie d'ajouter des Bentités pour modifier le suivi. Du code source d' EFCore, vous pouvez voir que cela nous conduit à la méthode InternalEntityEntry.SetPropertyModified avec les valeurs d'argument suivantes:

  • property- une de nos Bentités,
  • changeState = true,
  • isModified = true,
  • isConceptualNull = false,
  • acceptChanges = true.

Cette méthode avec de tels arguments change l'état des Detached Bentites en Modified, puis essaie de commencer leur suivi (voir lignes 490 - 506). Parce que les Bentités ont maintenant un état, Modifiedcela les conduit à être attachées (non ajoutées).

Iliar Turdushev
la source
Où est la réponse pour "Pourquoi n'ajoute-t-il pas au lieu de joindre les instances de B?" Vous dites "il échoue parce que les deux nouvelles entités B ont le même Id = 0". Je pense que c'est faux parce que le noyau ef enregistre à la fois avec 1 et 2 identifiants. Je ne pense pas que ce soit la bonne réponse à cette question
DIlshod K
@DIlshod K Merci pour le commentaire. Dans la section "Comment résoudre ce problème", j'ai écrit que la collection Sdevrait être chargée explicitement, car le code fourni n'utilise pas le chargement paresseux. Bien sûr, EF a enregistré les Bentités précédemment créées dans la base de données. Mais la ligne de code A a = db.Set<A>().Single()ne charge que l'entité Asans entité dans la collection S. Pour charger la collecte, un Schargement plus rapide doit être utilisé. Je vais changer ma réponse pour inclure explicitement la réponse à la question "Pourquoi n'ajoute-t-elle pas au lieu de joindre les instances de B?".
Iliar Turdushev