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