为什么我的 Attach() 方法会触发 "Entity already exists" InvalidOperationException?

Why does my Attach() method trigger a "Entity already exists" InvalidOperationException?

这让我困惑了几个小时。我正在重写 Winforms 桌面应用程序以支持 ASP.NET 核心网站。该应用程序将一些表本地存储在 LiteDB 缓存中,并调用“使用”DBContext 来获取数据。

桌面应用程序使用 TaxAccount 抽象 class,由 HouseholdBusiness 继承。

在客户端搜索中,应用程序调用 GetAccount() 以显示单个用户帐户。由于数据库可能很慢,缓存在后台更新。方法在这里。


        /// <summary>
        /// Retrieve a single account from cache. Later, replace the account object with object from server.
        /// </summary>
        /// <param name="accountID"></param>
        /// <returns></returns>
        public TaxAccount GetAccount(int accountID)
        {
            var accounts = Cache.GetCollection<TaxAccount>();
            var account = accounts.FindById(accountID);

            if (GetSingleAccountTask == null || GetSingleAccountTask.IsCompleted)
            {
                GetSingleAccountTask = Task.Run(() => UpdateAccount(account));
            }

            return account;

            void UpdateAccount(TaxAccount account)
            {
                using (var serverContext = new ApplicationDbContext(ServerOptions))
                {
                    var found = serverContext.Accounts
                        .Include(X => X.Users)
                        .FirstOrDefault(X => X.Id == account.Id);

                    account = found;

                    if (found != null)
                    {
                        accounts.Update(found);
                    }
                    else
                    {
                        accounts.Delete(account.Id);
                    }

                }
            }
        }

我想更新 TaxAccount 实体的单个属性。为此,我使用 Attach(taxAccount),理想情况下应该只更新我想要的 属性。

        public void UpdatePrivateLink(TaxAccount taxAccount, string link)
        {
            // Retrieve collection from cache.
            var accounts = Cache.GetCollection<TaxAccount>();
            using (var serverContext = new ApplicationDbContext(ServerOptions))
            {
                // Attach taxAccount to server context.
                serverContext.Attach(taxAccount);
                taxAccount.PrivateFolderLink = link;
                // Update server.
                serverContext.SaveChanges();
                // Update cache.
                accounts.Update(taxAccount);
            }    
        }

这行不通。它创建了一个 System.InvalidOperationException : The instance of entity type 'Household' cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked。但我找不到实体。

这是我尝试过的事情的列表:

更糟糕的是,如果我创建一个具有相同 ID 的 new Household(),那么突然之间它就可以工作了!

var account = new Household() { Id = taxAccount.Id };
serverContext.Attach(account);
account.PrivateFolderLink = link;
serverContext.SaveChanges();

// Then we have to save in cache.
taxAccount.PrivateFolderLink = link;
accounts.Update(taxAccount);

这个解决方法对我来说毫无意义。为什么 EF 认为 taxAccount 是在全新的 DbContext 上跟踪的?为什么我不能在不创建新对象的情况下摆脱这种跟踪?

希望得到建议。

编辑:

编辑: 此测试是最简单的实现,但仍然失败。


        public void AttachTest(int accountID, string link)
        {
            var accounts = Cache.GetCollection<TaxAccount>();
            var acct = accounts.FindById(accountID);

            using (var serverContext = new ApplicationDbContext(ServerOptions))
            {
                serverContext.Attach(acct);
                acct.PrivateFolderLink = link;
                serverContext.SaveChanges();
            }
        }

对于完整调试:我正在 .NET 5.0 控制台应用程序上进行测试,EF 版本是 5.0.13,托管在 .NET Standard 2.1 库上。

这是我正在使用的 TaxAccount 模型。

    public abstract class TaxAccount
    {
        [Key]
        public int Id { get; set; }

        [MaxLength(200)]
        public string Name { get; set; }

        public bool Archived { get; set; } = false;
        public string PrivateFolderLink { get; set; }

        public List<AppUser> Users { get; set; }
    }

    public class Household : TaxAccount
    {
    }

    public class Business : TaxAccount
    {
        [EmailAddress, MaxLength(500)]
        public string Email { get; set; }
        [MaxLength(500)]
        public string Phone { get; set; }
        [MaxLength(1000)]
        public string Address { get; set; }
    }

在我的 ApplicationDbContext 中,唯一的 fluent 逻辑是标记鉴别器。

            // Tax Account abstract class.
            builder.Entity<TaxAccount>().HasDiscriminator()
                .HasValue<Household>(nameof(Household))
                .HasValue<Business>(nameof(Business))
                .IsComplete(true);

            builder.Entity<TaxAccount>()
                .Property("Discriminator")
                .HasMaxLength(50);

来自Microsoft Wiki:

Begins tracking the given entity and entries reachable from the given entity using >the Modified state by default, but see below for cases when a different state will >be used.

Generally, no database interaction will be performed until SaveChanges() is called.

我的猜测是,在保存更改之前跟踪帐户。

经过今天早上的一些试验,我明白了!幸运的是,它与缓存或其他 DbContexts 无关!

classTaxAccount有一个List<AppUser>,有一个属性Accounts,有一个List<TaxAccount>。这种多对多关系在 Attach() 方法中创建了一个循环,而 EF Core 无法很好地处理它。为了证明这一点,我写了两个测试,都有效!

public void AttachTest(int accountID, string link)
        {
            var accounts = Cache.GetCollection<TaxAccount>();
            var acct = accounts.FindById(accountID);
            // We set the Users relation to be null.
            acct.Users = null;

            using (var serverContext = new ApplicationDbContext(ServerOptions))
            {
                serverContext.Attach(acct);
                acct.PrivateFolderLink = link;
                serverContext.SaveChanges();
            }
        }
public void AttachTestNullUsers(int accountID, string link)
        {
            var accounts = Cache.GetCollection<TaxAccount>();
            var acct = accounts.FindById(accountID);
            // For each user, the Accounts is null, this also breaks the relationship.
            acct.Users.ForEach(X => X.Accounts = null);

            using (var serverContext ...
        }

现在,有两个后续问题:

  1. 这比创建 TaxAccount 的新实例并附加它更好吗?
  2. 如何处理多对多关系的 INSERTUPDATE 操作?

  1. 可能不会。设置 acct.Users = null 是一个意外的结果,一旦命令完成,很容易忘记恢复该关系。 OTOH,初始化 new TaxAccount(taxAccount.Id) 是一个轻操作,对基础对象没有影响。

  2. 很糟糕,经过一些测试,如果您想添加或删除多对多对象,Attach() 不是一个好主意。查找然后更新是您最好的选择。