通过EntityFrameworkCore中的ID删除已加载和已卸载的对象

问题描述:

我有一种方法,该方法接收要删除的对象的ID的 IEnumerable< Guid> 。一种建议的方法如下:

I have a method that receives an IEnumerable<Guid> of IDs to objects I want to delete. One suggested method is as follows

foreach(Guid id in ids)
{
  var tempInstance = new MyEntity { Id = id };
  DataContext.Attach(tempInstance); // Exception here
  DataContext.Remove(tempInstance);
}

如果对象尚未加载到内存中,则此方法很好。但是我的问题是,当它们已经加载时, Attach 方法会抛出一个 InvalidOperationException-无法跟踪实体类型'MyEntity'的实例,因为另一个键值 Id:...的实例已经被跟踪。如果我使用 DataContext.Remove 而不调用 Attach ,也会发生同样的情况。

This works fine if the objects aren't already loaded into memory. But my problem is that when they are already loaded then the Attach method throws an InvalidOperationException - The instance of entity type 'MyEntity' cannot be tracked because another instance with the key value 'Id:...' is already being tracked. The same happens if I use DataContext.Remove without calling Attach.

foreach(Guid id in ids)
{
  var tempInstance = new MyEntity { Id = id };
  DataContext.Remove(tempInstance); // Exception here
}

我不想使用 DataContext.Find 来获取已加载对象的实例,因为如果尚未加载该对象,则会将该对象加载到内存中。

I don't want to use DataContext.Find to grab the instance of an already loaded object because that will load the object into memory if it isn't already loaded.

我无法使用 DataContext.ChangeTracker 查找已加载的对象,因为只有状态已修改的对象才会出现在其中,并且我的对象可能会被加载和未修改。

I cannot use DataContext.ChangeTracker to find already loaded objects because only objects with modified state appear in there and my objects might be loaded and unmodified.

以下方法在设置 EntityEntry.State 时也会抛出相同的 InvalidOperationException MyEntity 上覆盖 GetHashCode Equals 以确保字典查找

The following approach throws the same InvalidOperationException when setting EntityEntry.State, even when I override GetHashCode and Equals on MyEntity to ensure dictionary lookups see them as the same object.

foreach(Guid id in ids)
{
  var tempInstance = new MyEntity { Id = id };
  EntityEntry entry = DataContext.Entry(tempInstance);
  entry.State == EntityState.Deleted; // Exception here
}

到目前为止,我唯一能找到的方法就是通过ID删除对象,而不知道对象是否为以下对象:

The only way so far I have found that I can achieve deleting objects by ID without knowing if the object is the following:

foreach(Guid id in ids)
{
  var tempInstance = new MyEntity { Id = id };
  try
  {
    DataContext.Attach(tempInstance); // Exception here
  }
  catch (InvalidOperationException)
  {
  }
  DataContext.Remove(tempInstance);
}

奇怪的是,我能够调用 DataContext。尝试尝试附加的异常后,无错误地删除(tempInstance),但此时它确实可以正常工作,并且删除了正确的在执行 DataContext.SaveChanges 时从数据库中获取行。

It's odd that I am able to call DataContext.Remove(tempInstance) without error after experiencing an exception trying to Attach it, but at this point it does work without an exception and also deletes the correct rows from the database when DataContext.SaveChanges is executed.

我不喜欢捕获异常。是否有实现我想要的良好方法?

I don't like catching the exception. Is there a "good" way of achieving what I want?

注意:如果类具有自引用,则需要将对象加载到内存中,以便EntityFrameworkCore可以确定删除顺序

Note: If the class has a self-reference then you need to load the objects into memory so EntityFrameworkCore can determine in which order to delete the objects.

奇怪的是,尽管这在EF6和EF Core中是一个很常见的例外,但它们都没有公开公开方法用于以编程方式检测具有相同密钥的已跟踪实体实例。请注意,覆盖 GetHashCode 等于并没有帮助,因为EF使用引用相等性来跟踪实体实例。

Strangely, although this is a quite common exception in EF6 and EF Core, neither of them expose publicly a method for programmatically detecting the already tracked entity instance with the same key. Note that overriding GetHashCode and Equals doesn't help since EF is using reference equality for tracking entity instances.

当然可以从 DbSet< T> .Local 属性获得它,但不会像作为 Find 所使用的内部EF机制以及引发上述异常的方法非常有效。我们需要的只是 Find 方法的第一部分,如果找不到则返回 null 而不是从数据库中加载。

Of course it can be obtained from the DbSet<T>.Local property, but it would not be as efficient as the internal EF mechanism used by Find and the methods throwing the aforementioned exception. All we need is the first part of the Find method and returning null when not found instead of loading from the database.

幸运的是,对于EF Core,可以通过使用一些EF Core内部组件(在标准下,此API支持实体)相对容易地实现我们所需的方法。 Framework Core基础结构,不能直接在您的代码中使用。此API可能会更改或在将来的版本中删除。策略)。这是在EF Core 2.0.1上测试的示例实现:

Luckily, for EF Core the method that we need can be implemented relatively easily by using some of the EF Core internals (under the standard This API supports the Entity Framework Core infrastructure and is not intended to be used directly from your code. This API may change or be removed in future releases. policy). Here is the sample implementation, tested on EF Core 2.0.1:

using Microsoft.EntityFrameworkCore.Internal;

namespace Microsoft.EntityFrameworkCore
{
    public static partial class CustomExtensions
    {
        public static TEntity FindTracked<TEntity>(this DbContext context, params object[] keyValues)
            where TEntity : class
        {
            var entityType = context.Model.FindEntityType(typeof(TEntity));
            var key = entityType.FindPrimaryKey();
            var stateManager = context.GetDependencies().StateManager;
            var entry = stateManager.TryGetEntry(key, keyValues);
            return entry?.Entity as TEntity;
        }
    }
}

现在,您可以简单地使用:

Now you can use simply:

foreach (var id in ids)
    DataContext.Remove(DataContext.FindTracked<MyEntity>(id) ?? new MyEntity { Id = id }));

DataContext.RemoveRange(ids.Select(id => 
    DataContext.FindTracked<MyEntity>(id) ?? new MyEntity { Id = id }));