Search code examples
c#delete-rowef-core-3.1

EF Core > Delete entity (Soft Delete) > Entity State remains Unchanged


In my generic base repository I have the following simple method for removing an entity from database:

    public async Task<bool> DeleteAsync(TKey id)
    {
        var item = await Context.Set<TDb>().FindAsync(id).ConfigureAwait(false);
        if (item == null)
            return null;

        var result = Context.Set<TDb>().Remove(item);
        await Context.SaveChangesAsync().ConfigureAwait(false);

        return result.State == EntityState.Modified || result.State == EntityState.Deleted;
    }

Then in my DB Context I do set shadow properties in my save changes async in a following way (just like Microsoft suggests) (code simplified for better clarity):

public class EfCoreDbContext : DbContext, IUnitOfWork
{
    public EfCoreDbContext(
        DbContextOptions options
        IConfiguration configuration) : base(options)
    {
    }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        ChangeTracker.SetShadowISoftDeletableProperties();
        ChangeTracker.SetShadowIUserOwnableProperties(UserResolver);
        ChangeTracker.SetShadowIAuditableProperties(UserResolver);
        return await base.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
    }
}

As visible from SaveChangesAsync method, I already use shadow properties for other tracking items, such as audits, or user ownership which works without a problem.

Finally, here is the Change Tracker code responsible for setting up soft delete

    public static void SetShadowISoftDeletableProperties(this ChangeTracker changeTracker)
    {
        changeTracker.DetectChanges();
        foreach (var entry in changeTracker.Entries())
        {
            if (typeof(ISoftDeletable).IsAssignableFrom(entry.Entity.GetType()))
            {
                if (entry.State == EntityState.Deleted)
                {
                    entry.State = EntityState.Modified;
                    entry.Property("IsDeleted").CurrentValue = true;
                }
                else if (entry.State == EntityState.Added)
                {
                    entry.Property("IsDeleted").CurrentValue = false;
                }
                else
                {
                    entry.Property("IsDeleted").CurrentValue = entry.Property("IsDeleted").OriginalValue;
                }
            }
        }
    }

Everything works fine, IsDeleted property is being set successfully in the database; however when I inspect my result.State, it tells me that entity state is Unchanged (which causes that my DeleteAsync returns false), which I don't quite understand. Even in code it is visible, that I change state from Deleted to Modified.

I can surely ignore this and instead, return plain true after saving changes (in case of error, it would fail before); yet I am just trying to understand why I am getting this unexpected state result. Any help in respect to this matter would be highly appreciated.


Solution

  • Added, Modified and Deleted are pending states, indicating what EF Core will do with that entity when SaveChanges{Async} is called. ChangeTracker.HasChanges() returns true when there is any entity entry with such state.

    By default (acceptAllChangesOnSuccess=true parameter of SaveChanges{Async}), after successful SaveChanges these states are updated to reflect the permanent (database) state of these entities. Added and Modified become Unchanged, Deleted become Detached (and entries are removed from the change tracker) and ChangeTracker.HasChanges() returns false.

    You can change that by passing acceptAllChangesOnSuccess=false to SaveChanges{Async}, in which case you'll see the pending states, but then you should not forget to call ChangeTracker.AcceptAllChanges(), otherwise the next SaveChanges{Async} will try to reapply them in the database (and most likely will fail).