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

10
在下面的示例代码中,当执行db.Entry(a).Collection(x => x.S).IsModified = true时,会出现以下异常:

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

为什么它不是添加而是附加B的实例呢?
奇怪的是,IsModified的文档没有将InvalidOperationException作为可能的异常之一进行说明。是文档有误还是存在bug?
我知道这段代码很奇怪,但我只是为了理解ef core在某些奇怪情况下的工作原理而编写的。我想要一个解释,而不是一个解决方法。
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();
        }
    }
}

A和B有什么关系?也就是说,它们之间存在什么关联属性? - sam
1个回答

9
提供的代码出现错误的原因如下。
当你从数据库获取创建的实体A时,它的属性S会被初始化为一个包含两个新记录B的集合。这些新的B实体的Id都等于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() 后,实体 A 的集合 S 不包含数据库中的实体 B,因为 DbContext Db 没有使用延迟加载,并且没有显式加载集合 S。实体 A 仅包含在初始化集合 S 期间创建的新实体 B
当您为集合 S 调用 IsModifed = true 时,实体框架尝试将这两个新实体 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)

错误信息还表明,它无法跟踪Id = 0B实体,因为已经有另一个具有相同IdB实体被跟踪了。


如何解决这个问题。

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

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

相反,您应该在创建A的位置填充S集合。例如:

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的实例,而是附加了它们?

简而言之,它们被附加而不是添加,因为它们处于“分离”的状态。

执行代码行后:

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 - 我们的B实体之一,
  • changeState = true,
  • isModified = true,
  • isConceptualNull = false,
  • acceptChanges = true
该方法使用这些参数来更改Detached B实体的状态为Modified,然后尝试开始跟踪它们(参见490-506行)。因为B实体现在具有Modified状态,所以它们被附加(而不是添加)。

“为什么它没有添加B的实例,而是附加了它们?”这个问题的答案在哪里?你说“失败是因为两个新的B实体具有相同的Id = 0”。我认为这是错误的,因为EF Core将它们都保存为1和2的ID。我不认为这是正确的答案。 - Dilshod K
@DIlshod K 感谢您的评论。在“如何解决这个问题”部分,我写道集合S应该被显式加载,因为提供的代码不使用延迟加载。当然,EF会将之前创建的B实体保存在数据库中。但是,代码行A a = db.Set<A>().Single()仅加载实体A而不加载集合S中的实体。要加载集合S,应该使用急切加载。我将更改我的答案,明确回答“为什么它不添加而是附加B实例”的问题。 - Iliar Turdushev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接