You have a combination of two settings which are causing this:
entity.Navigation(e => e.Country).AutoInclude();
This causes the
Country
navigation property to always be included in any query that loads a
Customer
.
_context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
This causes every query that uses your context to use non-tracking behaviour when loading entities.
Tracking vs. No-Tracking Queries - EF Core | Microsoft Learn[
^]
That means your
dbSet.FindAsync
call loads the
Customer
and
Country
entities as disconnected entities.
Disconnected Entities - EF Core | Microsoft Learn[
^]
When you pass that entity to
Remove
, it attaches
the entire graph to the context. The
Customer
entity ends up in the
Deleted
state. But since the country is not already tracked, it ends up in the
Added
state.
Since you can't add two countries with the same ID, you get an error at this point.
There are several possible solutions:
1. Remove the
AutoInclude
for the
Country
navigation property, and only include it when it's required.
2. Remove the
QueryTrackingBehavior.NoTracking
setting.
(A repository changing global settings on a context it doesn't own is already a code-smell.)
3. Don't load the entity from the database before deleting it:
public virtual async Task<bool> RemoveAsync(T entityToRemove)
{
try
{
var entry = dbSet.Attach(entityToRemove);
entry.State = EntityState.Deleted;
await _context.SaveChangesAsync();
return true;
}
catch (DbUpdateConcurrencyException)
{
return false;
}
}
...
await customerRepository.RemoveAsync(new Customer { Id = 42 });
4. Use EF Core 7's
new bulk operations[
^] to delete the record without loading it:
public async Task<bool> RemoveAsync(Expression<Func<T, bool>> filter)
{
int recordsAffected = await dbSet.Where(filter).ExecuteDeleteAsync();
return recordsAffected != 0;
}
...
await customerRepository.RemoveAsync(c => c.Id == 42);