¿Cómo puedo filtrar automáticamente las entidades eliminadas con Entity Framework?

Estoy usando Entity Framework Code First. SaveChanges en DbContext para permitirme hacer una “eliminación suave”:

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

Lo cual es genial, por lo que el objeto sabe cómo IsDeleted a sí mismo como una eliminación suave (en este caso, simplemente establece IsDeleted en true ).

Mi pregunta es ¿cómo puedo hacer que cuando recupero el objeto ignore cualquiera con IsDeleted ? Entonces, si dijera _db.Users.FirstOrDefault(UserId == id) si ese usuario tuviera IsDeleted == true , lo ignoraría. Esencialmente quiero filtrar?

Nota: no quiero simplemente poner && IsDeleted == true Es por eso que estoy marcando las clases con una interfaz para que el remove sepa cómo “solo trabajar” y me gustaría modificar de algún modo la recuperación para saber cómo “solo Trabajo “también basado en que esa interfaz está presente.

Tengo un borrado suave que funciona para todas mis entidades y los elementos borrados suaves no se recuperan a través del contexto utilizando una técnica sugerida por esta respuesta . Eso incluye cuando accede a la entidad a través de las propiedades de navegación.

Agregue un discriminador IsDeleted a cada entidad que pueda eliminarse por software. Lamentablemente, no he resuelto cómo hacer este bit en función de la entidad que se deriva de una clase abstracta o una interfaz (la asignación EF actualmente no admite interfaces como una entidad ):

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

Anule SaveChanges y encuentre todas las entradas que se eliminarán:

Editar Otra forma de anular la eliminación de SQL es cambiar los procedimientos almacenados generados por 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(); } 

El método SoftDelete ejecuta sql directamente en la base de datos porque las columnas discriminator no se pueden incluir en las entidades:

 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 devuelve la tabla que se actualizará para una entidad. Maneja el caso donde la tabla está vinculada a BaseType en lugar de a un tipo derivado. Sospecho que debería estar revisando toda la jerarquía de herencia … Pero hay planes para mejorar la API de Metadatos y si tengo que buscar en el Código EF Primer Mapeo entre Tipos y Tablas

 private readonly static Dictionary _mappingCache = new Dictionary(); 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() .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 ); } 

Anteriormente había creado índices en claves naturales en una migración con un código que se veía así:

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

Pero eso significa que no puede crear una nueva Organización con el mismo nombre que una Organización eliminada. Para permitir esto, cambié el código para crear los índices a esto:

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

Y eso excluye elementos eliminados del índice

Nota Si bien las propiedades de navegación no se completan si el elemento relacionado se elimina de forma continua, la clave externa es. Por ejemplo:

 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; 

PS Vote for global filtering aquí https://entityframework.codeplex.com/workitem/945?FocusElement=CommentTextBox# y filtrado incluye aquí

Use EntityFramework.DynamicFilters . Le permite crear filtros globales que se aplicarán automáticamente (incluso contra propiedades de navegación) cuando se ejecutan consultas.

Hay un ejemplo de filtro “IsDeleted” en la página del proyecto que se ve así:

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

Ese filtro inyectará automáticamente una cláusula where en cualquier consulta en contra de una entidad que sea ISoftDelete. Los filtros están definidos en su DbContext.OnModelCreating ().

Descargo de responsabilidad: soy el autor.

Una opción sería encapsular el !IsDeleted en un método de extensión. Algo como a continuación es solo un ejemplo. Tenga cuidado, es solo para darle una idea de un método de extensión, el siguiente no se comstackrá.

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

Uso:

 _db.Users.FirstOrDefaultExcludingDeletes(UserId == id) 

Gran pregunta

Debería interceptar la consulta SQL antes de que se ejecute de alguna manera, luego agregar cláusula where adicional para eliminar los elementos ‘eliminados’ de la selección. Desafortunadamente, Entity no tiene GetCommand que se pueda usar para cambiar la consulta.

Quizás EF Provider Wrapper, que se encuentra en el lugar correcto, podría modificarse para permitir el cambio de consulta.

O bien, puede utilizar QueryInterceptor pero cada consulta debería usar InterceptWith(visitor) para cambiar las expresiones …

Por lo tanto, me concentraría en este enfoque ya que AFAIK no tiene otra opción que interceptar la consulta y corregirla (si desea mantener el código sin consultas).

De todos modos, si descubres algo útil, háznoslo saber.