How can I implement "Soft Deletes" with "Entity Framework Core" (aka EF7)?

c# entity-framework-core

Question

I'm trying to implement a "Soft Delete" using EF7. My Item table has a field named IsDeleted of type bit. All of the examples that I see around SO and elsewhere are using something like this:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Item>().Map(m => m.Requires("IsDeleted").HasValue(false));
}

but Map() is no longer a method of ModelBuilder.

EDIT: Let me clarify. I'm mostly only interested in reading the data right now. I want EF to automatically filter out all records in my Item table where IsDeleted == 1 (or true). I do not want to require an && x.IsDeleted == false at the end of every query.

1
1
7/27/2017 5:40:01 PM

Expert Answer

Disclaimer: I'm the owner of the project Entity Framework Plus

As you will see in @Adem link, our library supports query filtering.

You can easily enable/disable a global/instance filter

QueryFilterManager.Filter<Item>(q => q.Where(x => !x.IsDeleted));

Wiki: EF Query Filter

Edit: Answer sub question

Care to explain how this works behind the scene?

Firstly, you can either initialize filter globally or by instance

// Filter by global configuration
QueryFilterManager.Filter<Customer>(q => q.Where(x => x.IsActive));
var ctx = new EntitiesContext();
// TIP: You can also add this line in EntitiesContext constructor instead
QueryFilterManager.InitilizeGlobalFilter(ctx);

// Filter by instance configuration
var ctx = new EntitiesContext();
ctx.Filter<Post>(MyEnum.EnumValue, q => q.Where(x => !x.IsSoftDeleted)).Disable();

Under the hood, the library will loop on every DbSet of the context and checks if a filter can be applied to the generic type.

In this case, the library will filter the original/filtered query from the DbSet using the filter then modify the current internal query for the new filtered query.

In summary, we changed some DbSet internal value to use the filtered query.

The code is FREE & Open Source if you want to learn about how it works.

Edit: Answer sub question

@jonathan will this filter included navigation collections too?

For EF Core, it's not supported yet since Interceptor is not available yet. But starting from EF Core 2.x, the EF Team has implemented Global query filters which should allow this.

2
8/26/2018 7:54:25 PM

Popular Answer

If you can migrate to EF Core 2.0 you can use Model-level query filters https://docs.microsoft.com/en-us/ef/core/what-is-new/index

If you use EF Core 1.0 You can make some trick with available EF Core features:

Inheritance https://docs.microsoft.com/en-us/aspnet/core/data/ef-mvc/inheritance

Shadow properties https://docs.microsoft.com/en-us/ef/core/modeling/shadow-properties

public class Attachment : AttachmentBase
{}

public abstract class AttachmentBase
{
    public const string StatePropertyName = "state";

    public Guid Id { get; set; }
}

public enum AttachmentState
{
    Available,
    Deleted
}

public class AttachmentsDbContext : DbContext
{
    public AttachmentsDbContext(DbContextOptions options)
        : base(options)
    {
    }

    public DbSet<Attachment> Attachments { get; set; }

    public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
    {
        IEnumerable<EntityEntry<Attachment>> softDeletedAttachments = ChangeTracker.Entries<Attachment>().Where(entry => entry.State == EntityState.Deleted);

        foreach (EntityEntry<Attachment> softDeletedAttachment in softDeletedAttachments)
        {
            softDeletedAttachment.State = EntityState.Modified;
            softDeletedAttachment.Property<int>(AttachmentBase.StatePropertyName).CurrentValue = (int)AttachmentState.Deleted;
        }
        return base.SaveChangesAsync(cancellationToken);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<AttachmentBase>()
            .HasDiscriminator<int>(AttachmentBase.StatePropertyName)
            .HasValue<Attachment>((int)AttachmentState.Available);

        modelBuilder.Entity<AttachmentBase>().Property<int>(AttachmentBase.StatePropertyName).Metadata.IsReadOnlyAfterSave = false;

        modelBuilder.Entity<Attachment>()
            .ToTable("available_attachment");

        modelBuilder.Entity<AttachmentBase>()
            .ToTable("attachment");

        base.OnModelCreating(modelBuilder);
    }
}


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow