Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
608 views
in Technique[技术] by (71.8m points)

entity framework core - Delete loaded and unloaded objects by ID in EntityFrameworkCore

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

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
}

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.

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.

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
}

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

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?

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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.

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.

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

or

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...