web-dev-qa-db-fra.com

Comment puis-je filtrer automatiquement les entités supprimées à l'aide de Entity Framework?

J'utilise Entity Framework Code First. Je remplace SaveChanges dans DbContext pour me permettre d'effectuer une "suppression logicielle":

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

    continue;
}

Ce qui est génial, donc l'objet sait comment se marquer lui-même en tant que suppression logicielle (dans ce cas, il définit simplement IsDeleted sur true).

Ma question est la suivante: comment puis-je faire en sorte que lorsque je récupère l'objet, il en ignore tout avec IsDeleted? Donc, si je disais _db.Users.FirstOrDefault(UserId == id) si cet utilisateur avait IsDeleted == true, il l'ignorerait. Essentiellement, je veux filtrer? 

Note: Je ne veux pas simplement mettre && IsDeleted == true C'est pourquoi je marque les classes avec une interface afin que la suppression sache comment "Just Work" et j'aimerais modifier en quelque sorte la récupération pour savoir comment "Just Work" "également basé sur cette interface étant présent.

31
Jordan

La suppression logicielle fonctionne pour toutes mes entités et les éléments supprimés ne sont pas récupérés via le contexte à l'aide d'une technique suggérée par cette réponse . Cela inclut lorsque vous accédez à l'entité via les propriétés de navigation.

Ajoutez un discriminateur IsDeleted à chaque entité pouvant être supprimée progressivement. Malheureusement, je n'ai pas trouvé comment utiliser ce bit en fonction de l'entité dérivée d'une classe abstraite ou d'une interface ( le mappage EF ne prend actuellement pas en charge les interfaces en tant qu'entité ):

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

Ignorer SaveChanges et rechercher toutes les entrées à supprimer:

EditUne autre façon de remplacer le sql de suppression consiste à modifier les procédures stockées générées par 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();
}

La méthode SoftDelete exécute SQL directement sur la base de données car les colonnes discriminantes ne peuvent pas être incluses dans les entités:

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 renvoie la table à mettre à jour pour une entité. Il gère le cas où la table est liée au type de base plutôt qu'à un type dérivé. Je soupçonne que je devrais vérifier toute la hiérarchie d'héritage .... Mais il est prévu de améliorer l'API de métadonnées et si je dois aller examiner le premier mappage de code EF entre types et Les 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 );
}

J'avais précédemment créé des index sur les clés naturelles dans une migration avec un code qui ressemblait à ceci:

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

Mais cela signifie que vous ne pouvez pas créer une nouvelle organisation avec le même nom qu'une organisation supprimée. Afin de permettre cela, j'ai changé le code pour créer les index suivants:

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

Et cela exclut les éléments supprimés de l'index

Remarque Bien que les propriétés de navigation ne soient pas renseignées si l'élément associé est supprimé de manière progressive, la clé étrangère est . Par exemple:

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. Votez pour le filtrage global ici https://entityframework.codeplex.com/workitem/945?FocusElement=CommentTextBox# } _ et filtré inclut ici

35
Colin

Utilisez EntityFramework.DynamicFilters . Il vous permet de créer des filtres globaux qui seront appliqués automatiquement (y compris par rapport aux propriétés de navigation) lors de l’exécution des requêtes. 

Il existe un exemple de filtre "IsDeleted" sur la page de projet qui ressemble à ceci:

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

Ce filtre injectera automatiquement une clause where sur toute requête contre une entité qui est ISoftDelete. Les filtres sont définis dans votre DbContext.OnModelCreating ().

Disclaimer: je suis l'auteur.

35
John

Une option serait d'encapsuler le !IsDeleted dans une méthode d'extension. Quelque chose comme ci-dessous n'est qu'un exemple. Attention, c'est juste pour vous donner une idée d'une méthode d'extension, la compilation ci-dessous ne compilera pas.

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

Utilisation:  

_db.Users.FirstOrDefaultExcludingDeletes(UserId == id)
7
Ricky G

Vous pouvez utiliser Filtres de requête globaux sur 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);
}
0
hkutluay