39

I am using Entity Framework Code First. I override SaveChanges in DbContext to allow me to do a "soft delete":

if (item.State == EntityState.Deleted && typeof(ISoftDelete).IsAssignableFrom(type))
{
    item.State = EntityState.Modified;
    item.Entity.GetType().GetMethod("Delete")
        .Invoke(item.Entity, null);

    continue;
}

Which is great, so the object knows how to mark itself as a soft delete (In this case it just sets IsDeleted to true).

My question is how can I make it such that when I retrieve the object it ignores any with IsDeleted? So if I said _db.Users.FirstOrDefault(UserId == id) if that user had IsDeleted == true it would ignore it. Essentially I want to filter?

Note: I do not want to just put && IsDeleted == true That's why I am marking the classes with an interface so the remove knows how to "Just Work" and I'd like to somehow modify the retrieval to know how to "Just Work" also based on that interface being present.

Scott Weldon
  • 9,673
  • 6
  • 48
  • 67
Jordan
  • 2,708
  • 4
  • 22
  • 35
  • 1
    Unless I'm misunderstand you, you simply add another clause into your Linq query. I.e, you make it `FirstOrDefault(UserId == id && !IsDeleted)` – Arran Oct 02 '12 at 21:44
  • Or you use an `IQueryable` that is already filtered, as in `_repository.ActiveUsers.FirstOrDefault(UserId == id)` – Robert Harvey Oct 02 '12 at 21:47
  • 2
    @Arran Yeah I'd like to be able to avoid doing that so I don't have to know what classes are soft deleted throughout my code. I have the soft delete classes use an interface ISoftDelete so when a remove it done and then savechanges it sees it implements that interface and handles the soft delete. Is there no similar way to handle retrieval? – Jordan Oct 02 '12 at 21:47
  • You could try to implement something like that. Or you could simply do a Find and Replace in Visual Studio. :) – Robert Harvey Oct 02 '12 at 21:49
  • @RobertHarvey but if I use `IQueryable` I lose things like Add() I want all the benefits of `DbSet` but to be able to filter them :) I don't want to have a `DbSet` for my users and an `IQueryable` for active users or something like that (If that's what you're suggesting) – Jordan Oct 02 '12 at 22:06
  • Yes, that's what I'm suggesting. In theory you can allow calling clients to access your `dbContext` directly; in practice, you want a Repository layer, especially if you want to do things like filtering for soft deletes or providing data access security. But I'm old-school; I always design the database first, so code-first might have an impact on the way things are done. – Robert Harvey Oct 02 '12 at 22:21
  • @RobertHarvey I also always design database first :) Hence my problem, I didn't get to chose the implementation on this project and I'm having to deal with the ramifications of that :p Thank you though! – Jordan Oct 02 '12 at 22:27
  • BTW Soft delete via `IsDeleted` breaks all sorts of Key Constraints on SQL Databases. Perhaps you should look at Database auditing instead. – Aron Feb 13 '15 at 09:57

5 Answers5

43

Use EntityFramework.DynamicFilters. It allows you to create global filters that will be applied automatically (including against navigation properties) when queries are executed.

There is an example "IsDeleted" filter on the project page that looks like this:

modelBuilder.Filter("IsDeleted", (ISoftDelete d) => d.IsDeleted, false);

That filter will automatically inject a where clause on any query against an entity that is ISoftDelete. Filters are defined in your DbContext.OnModelCreating().

Disclaimer: I'm the author.

John
  • 1,186
  • 10
  • 9
  • This solution provides a very simple, easy to use interface, especially if you want minimal impact on your context code file. The ability to turn these filters on and off is especially useful in various use cases. Great work! –  Oct 08 '15 at 22:35
  • Can you confirm that EntityFramework.DynamicFilters works when traversing collection navigation properties? – Felix Nov 02 '15 at 19:54
  • Yes, the filters work on navigation properties as well. So if you have an entity with a collection of ISoftDelete, loading that entity and "including" the child collection will apply the filter to the child collection at the same time. – John Dec 17 '15 at 14:58
  • 1
    this solution is good , but In my project I want Implement feture to Recovery `Soft Deleted ` , How Can I do it ? in this way I have just Records where `SoftDelete=0` , ? – Uthman Rahimi Feb 18 '16 at 08:19
  • 1
    You can temporarily disable the filter when you need to. This is described on the github project page. – John Feb 19 '16 at 00:39
  • 3
    Is there anything like this available for EF Core? – Costin_T Jun 05 '18 at 09:22
  • EF Core implements [Global Query Filters](https://learn.microsoft.com/en-us/ef/core/querying/filters) since EF Core 2.0 – LuisEduardox Aug 27 '19 at 02:41
40

I've got soft delete working for all my entities and soft deleted items are not retrieved via the context using a technique suggested by this answer. That includes when you access the entity via navigation properties.

Add an IsDeleted discriminator to every entity that can be soft deleted. Unfortunately I haven't worked out how to do this bit based on the entity deriving from an abstract class or an interface (EF mapping doesn't currently support interfaces as an entity):

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
   modelBuilder.Entity<Foo>().Map(m => m.Requires("IsDeleted").HasValue(false));
   modelBuilder.Entity<Bar>().Map(m => m.Requires("IsDeleted").HasValue(false));

   //It's more complicated if you have derived entities. 
   //Here 'Block' derives from 'Property'
   modelBuilder.Entity<Property>()
            .Map<Property>(m =>
            {
                m.Requires("Discriminator").HasValue("Property");
                m.Requires("IsDeleted").HasValue(false);
            })
            .Map<Block>(m =>
            {
                m.Requires("Discriminator").HasValue("Block");
                m.Requires("IsDeleted").HasValue(false);
            });
}

Override SaveChanges and find all the entries to be deleted:

Edit Another way to override the delete sql is to change the stored procedures generated by EF6

public override int SaveChanges()
{
   foreach (var entry in ChangeTracker.Entries()
             .Where(p => p.State == EntityState.Deleted 
             && p.Entity is ModelBase))//I do have a base class for entities with a single 
                                       //"ID" property - all my entities derive from this, 
                                       //but you could use ISoftDelete here
    SoftDelete(entry);

    return base.SaveChanges();
}

The SoftDelete method runs sql directly on the database because discriminator columns cannot be included in entities:

private void SoftDelete(DbEntityEntry entry)
{
    var e = entry.Entity as ModelBase;
    string tableName = GetTableName(e.GetType());
    Database.ExecuteSqlCommand(
             String.Format("UPDATE {0} SET IsDeleted = 1 WHERE ID = @id", tableName)
             , new SqlParameter("id", e.ID));

    //Marking it Unchanged prevents the hard delete
    //entry.State = EntityState.Unchanged;
    //So does setting it to Detached:
    //And that is what EF does when it deletes an item
    //http://msdn.microsoft.com/en-us/data/jj592676.aspx
    entry.State = EntityState.Detached;
}

GetTableName returns the table to be updated for an entity. It handles the case where the table is linked to the BaseType rather than a derived type. I suspect I should be checking the whole inheritance hierarchy.... But there are plans to improve the Metadata API and if I have to will look into EF Code First Mapping Between Types & Tables

private readonly static Dictionary<Type, EntitySetBase> _mappingCache 
       = new Dictionary<Type, EntitySetBase>();

private ObjectContext _ObjectContext
{
    get { return (this as IObjectContextAdapter).ObjectContext; }
}

private EntitySetBase GetEntitySet(Type type)
{
    type = GetObjectType(type);

    if (_mappingCache.ContainsKey(type))
        return _mappingCache[type];

    string baseTypeName = type.BaseType.Name;
    string typeName = type.Name;

    ObjectContext octx = _ObjectContext;
    var es = octx.MetadataWorkspace
                    .GetItemCollection(DataSpace.SSpace)
                    .GetItems<EntityContainer>()
                    .SelectMany(c => c.BaseEntitySets
                                    .Where(e => e.Name == typeName 
                                    || e.Name == baseTypeName))
                    .FirstOrDefault();

    if (es == null)
        throw new ArgumentException("Entity type not found in GetEntitySet", typeName);

    _mappingCache.Add(type, es);

    return es;
}

internal String GetTableName(Type type)
{
    EntitySetBase es = GetEntitySet(type);

    //if you are using EF6
    return String.Format("[{0}].[{1}]", es.Schema, es.Table);

    //if you have a version prior to EF6
    //return string.Format( "[{0}].[{1}]", 
    //        es.MetadataProperties["Schema"].Value, 
    //        es.MetadataProperties["Table"].Value );
}

I had previously created indexes on natural keys in a migration with code that looked like this:

public override void Up()
{
    CreateIndex("dbo.Organisations", "Name", unique: true, name: "IX_NaturalKey");
}

But that means that you can't create a new Organisation with the same name as a deleted Organisation. In order to allow this I changed the code to create the indexes to this:

public override void Up()
{
    Sql(String.Format("CREATE UNIQUE INDEX {0} ON dbo.Organisations(Name) WHERE IsDeleted = 0", "IX_NaturalKey"));
}

And that excludes deleted items from the index

Note While navigation properties are not populated if the related item is soft deleted, the foreign key is. For example:

if(foo.BarID != null)  //trying to avoid a database call
   string name = foo.Bar.Name; //will fail because BarID is not null but Bar is

//but this works
if(foo.Bar != null) //a database call because there is a foreign key
   string name = foo.Bar.Name;

P.S. Vote for global filtering here https://entityframework.codeplex.com/workitem/945?FocusElement=CommentTextBox# and filtered includes here

Community
  • 1
  • 1
Colin
  • 22,328
  • 17
  • 103
  • 197
  • Starting from 6.1, it is a bit easier to find out the table name: http://romiller.com/2014/04/08/ef6-1-mapping-between-types-tables/ – DixonD Feb 15 '15 at 14:58
  • 1
    Any thoughts on using this, but also having the ability to issue queries that return the soft deleted record in the rare cases when you need them? For example, I want my app to never include logically deleted lookup values, except for on the admin page where I manage them. – jskentzos Apr 29 '15 at 15:10
  • Faced with a problem when base.SaveChanges failed and all changes are being reverted, except softly deleted entities, as we delete them with ExecuteSqlCommand. ExecuteSqlCommand runs in different transaction from SaveChanges. – Vasyl Senko Mar 01 '17 at 15:25
9

One option would be to encapsulate the !IsDeleted into an extension method. Something like below is just an example. Beware its just to give you an idea of an extension method, the below won't compile.

public static class EnumerableExtensions
{
    public static T FirstOrDefaultExcludingDeletes<T>(this IEnumerable<T> source, Func<T, bool> predicate)
    {
        return source.Where(args => args != IsDeleted).FirstOrDefault(predicate);
    }
}

Usage:

_db.Users.FirstOrDefaultExcludingDeletes(UserId == id)
Scott Weldon
  • 9,673
  • 6
  • 48
  • 67
Ricky Gummadi
  • 4,559
  • 2
  • 41
  • 67
  • 1
    That's a great answer so +1, it would solve some of my problem but it won't solve all of it. It needs to go deeper into the relationship I suppose somehow. So if I Pull a group that loads the users for that group it wouldn't load the deleted ones. – Jordan Oct 03 '12 at 00:19
  • I guess you could create another extension method ExcludeDeletes and do something like _db.Groups.ExcludeSoftDeletes(groupId == id).Users.ExcludeSoftDeletes(UserId == id) – Ricky Gummadi Oct 03 '12 at 00:35
  • 1
    Thanks, but I don't really want to have to write a new API for this, again, overriding some internal method that controls what data is retrieved, perhaps hooking the internal retrieval into a view or something is ideal because that won't force me to rewrite the EF API. Say I want Single, or Any, I would need to write extensions for everything. Good idea though! Thanks – Jordan Oct 03 '12 at 00:44
  • fair point; all of the EF API have the (Func predicate) as a parameter any way, so you can just pass the !IsDeleted as a predicate – Ricky Gummadi Oct 03 '12 at 01:04
9

You can use Global Query Filters on Entity Framework Core 2.0.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>().Property<string>("TenantId").HasField("_tenantId");

    // Configure entity filters
    modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "TenantId") == _tenantId);
    modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);
}
hkutluay
  • 6,794
  • 2
  • 33
  • 53
-6

Great question.

You would need to intercept SQL query before it gets executed somehow, then add additional where clause to remove 'deleted' items from selection. Unfortunately, Entity doesn't have GetCommand that can be used to change the query.

Perhaps EF Provider Wrapper which sits in the right place could be modified to allow for query change.

Or, u can utilize QueryInterceptor but each query would have to use InterceptWith(visitor) to change the expressions...

So, I would concentrate on this approach as there is AFAIK no other option then intercepting the query and fixing it (if you want to keep code that queries unchanged).

Anyway, if you figure out something useful, let us know.

majkinetor
  • 8,730
  • 9
  • 54
  • 72