Неожиданное исключение InvalidOperationException при попытке изменить отношение через значение свойства по умолчанию - PullRequest
10 голосов
/ 26 февраля 2020

В приведенном ниже примере кода я получаю следующее исключение при выполнении db.Entry(a).Collection(x => x.S).IsModified = true:

System.InvalidOperationException: «Экземпляр типа сущности« B »не может быть отслежен, поскольку другой экземпляр с ключом значение '{Id: 0}' уже отслеживается. При присоединении существующих сущностей убедитесь, что присоединен только один экземпляр сущности с данным значением ключа.

Почему он не добавляет вместо присоединения экземпляры B?

Странно, но документация для IsModified не определяет InvalidOperationException в качестве возможного исключения. Неверная документация или ошибка?

Я знаю, что этот код странный, но я написал его только для того, чтобы понять, как работает ядро ​​ef в некоторых странных случаях egde. То, что я хочу, это объяснение, а не обходной путь.

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();
        }
    }
}

1 Ответ

8 голосов
/ 11 марта 2020

Причина ошибки в предоставленном коде следующая.

Когда вы получаете созданный объект A из базы данных, его свойство S инициализируется с коллекцией, содержащей две новые записи B. Id каждой из этих новых B сущностей равно 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 {} };

После выполнения строки кода var a = db.Set<A>().Single() collection S сущности A не содержит B сущности из базы данных, потому что DbContext Db не использует отложенную загрузку и нет явной загрузки коллекции S. Сущность A содержит только новые B сущности, которые были созданы во время инициализации коллекции S.

При вызове IsModifed = true для коллекции S платформа сущностей пытается добавить эти два новых объекта B в отслеживание изменений. Но происходит сбой, потому что оба новых объекта B имеют одинаковые 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;

Из трассировки стека видно, что инфраструктура объектов пытается добавить B сущностей в 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)

И в сообщении об ошибке также говорится, что он не может отслеживать B объект с Id = 0, потому что другой объект B с таким же Id уже отслежен.


Как решить эту проблему.

Чтобы решить эту проблему, вы должны удалить код, который создает B сущностей при инициализации S collection:

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

Вместо Вы должны заполнить коллекцию S в том месте, где создается A. Например:

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

Если вы не используете отложенную загрузку, вам следует явно загрузить коллекцию S, чтобы добавить ее элементы в отслеживание изменений:

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

Почему он не добавляет, а не прикрепляет экземпляры B?

Короче , они прикрепляются вместо добавления, потому что имеют Detached состояние.

После выполнения строки кода

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

созданные экземпляры объекта B имеют состояние Detached. Это можно проверить с помощью следующего кода:

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

Затем, когда вы устанавливаете

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

EF пытается добавить B сущностей для изменения отслеживания. Из исходного кода EFCore вы можете видеть, что это приводит нас к методу InternalEntityEntry.SetPropertyModified со следующими значениями аргумента:

  • property - один
  • changeState = true,
  • isModified = true,
  • isConceptualNull = false,
  • acceptChanges = true.

Этот метод с такими аргументами изменяет состояние элементов Detached B на Modified, а затем пытается начать их отслеживание (см. Строки 490 - 506). Поскольку B сущности теперь имеют состояние Modified, это приводит к их присоединению (не добавляется).

...