尝试通过属性默认值更改关系时出现意外的 InvalidOperationException

Sup*_*mum 10 c# language-lawyer entity-framework-core-3.1

在下面的示例代码中,我在执行以下操作时遇到以下异常db.Entry(a).Collection(x => x.S).IsModified = true

System.InvalidOperationException: '无法跟踪实体类型 'B' 的实例,因为另一个具有键值 '{Id: 0}' 的实例已被跟踪。附加现有实体时,请确保仅附加一个具有给定键值的实体实例。

为什么不添加而不是附加 B 的实例?

奇怪的是,文档IsModified没有指定InvalidOperationException为可能的例外。无效的文档或错误?

我知道这段代码很奇怪,但我写它只是为了了解 ef core 在一些奇怪的 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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Ili*_*hev 8

提供的代码中的错误原因如下。

当您A从数据库中创建实体时,它的属性S将使用包含两个新记录的集合进行初始化BId每个新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 {} };
Run Code Online (Sandbox Code Playgroud)

执行代码行后实体var a = db.Set<A>().Single()集合不包含来自数据库的实体,因为不使用延迟加载并且没有显式加载集合。实体仅包含在集合初始化期间创建的新实体。SABDbContext DbSABS

当您调用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;
Run Code Online (Sandbox Code Playgroud)

您可以从堆栈跟踪中看到实体框架尝试将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)
Run Code Online (Sandbox Code Playgroud)

并且错误消息还告诉它无法跟踪B实体,Id = 0因为已经跟踪了另一个B具有相同实体的实体Id


如何解决这个问题。

要解决此问题,您应该删除B初始化S集合时创建实体的代码:

public ICollection<B> S { get; set; } = new List<B>();
Run Code Online (Sandbox Code Playgroud)

相反,您应该SA创建的地方填充集合。例如:

db.Add(new A {S = {new B(), new B()}});
Run Code Online (Sandbox Code Playgroud)

如果您不使用延迟加载,则应显式加载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;
Run Code Online (Sandbox Code Playgroud)

为什么不添加而不是附加 B 的实例?

简而言之,它们是附加的,因为它们具有Detached状态,因此不会被添加。

执行一行代码后

var a = db.Set<A>().Single();
Run Code Online (Sandbox Code Playgroud)

创建的实体实例B具有 state Detached。可以使用下一个代码进行验证:

Console.WriteLine(db.Entry(a.S[0]).State);
Console.WriteLine(db.Entry(a.S[1]).State);
Run Code Online (Sandbox Code Playgroud)

然后当你设置

db.Entry(a).Collection(x => x.S).IsModified = true;
Run Code Online (Sandbox Code Playgroud)

EF 尝试添加B实体以更改跟踪。从EFCore 的源代码中,您可以看到这将我们引向具有下一个参数值的InternalEntityEntry.SetPropertyModified方法:

  • property- 我们的B实体之一,
  • changeState = true,
  • isModified = true,
  • isConceptualNull = false,
  • acceptChanges = true.

带有此类参数的此方法将Detached B实体的状态更改为Modified,然后尝试开始跟踪它们(参见第490 - 506 行)。因为B实体现在有状态,Modified这导致它们被附加(未添加)。