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