web-dev-qa-db-fra.com

DbContext ignore les modifications sans en disposer

J'ai une application cliente de bureau qui utilise des fenêtres modales pour définir les propriétés des objets hiérarchiques. Puisqu'il s'agit d'une application client et que l'accès au DbContext n'est pas threadé, j'utilise un contexte de longue durée sur le formulaire principal qui est transmis aux enfants modaux.

Ces fenêtres modales utilisent PropertyGrid pour afficher les propriétés des entités et disposent également de boutons d'annulation. Si des données sont modifiées et que vous appuyez sur le bouton d'annulation, les modifications sont reflétées dans le formulaire parent (où je ne peux pas disposer du DbContext object).

Existe-t-il un moyen de supprimer les modifications apportées si la méthode DbContext.SaveChanges() n'a PAS été appelée?

UPDATE: Entity Framework version 4.4.

44
Raheel Khan

Que diriez-vous de l'envelopper dans une transaction?

    using(var scope = new TransactionScope(TransactionScopeOption.Required,
        new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted })){

        // Do something 
        context.SaveChanges();
        // Do something else
        context.SaveChanges();

        scope.Complete();
}
10
Martin
public void RejectChanges()
    {
        foreach (var entry in ChangeTracker.Entries())
        {
            switch (entry.State)
            {
                case EntityState.Modified:
                case EntityState.Deleted:
                    entry.State = EntityState.Modified; //Revert changes made to deleted entity.
                    entry.State = EntityState.Unchanged;
                    break;
                case EntityState.Added:
                    entry.State = EntityState.Detached;
                    break;
            }
        }
    }

Mettre à jour:

Certains utilisateurs suggèrent d'ajouter .ToList() pour éviter que 'la collection ait été modifiée' exception . Mais je pense qu'il y a une raison à cette exception.

Comment obtenez-vous cette exception? Vous utilisez probablement le contexte de manière non threadsafe.

113
Sergey Shuvalov

Ceci est basé sur la réponse de Surgey Shuvalov. Il ajoute un support pour les changements de propriété de navigation.

public void RejectChanges()
{
    RejectScalarChanges();
    RejectNavigationChanges();
}

private void RejectScalarChanges()
{
    foreach (var entry in ChangeTracker.Entries())
    {
        switch (entry.State)
        {
            case EntityState.Modified:
            case EntityState.Deleted:
                entry.State = EntityState.Modified; //Revert changes made to deleted entity.
                entry.State = EntityState.Unchanged;
                break;
            case EntityState.Added:
                entry.State = EntityState.Detached;
                break;
        }
    }
}

private void RejectNavigationChanges()
{
    var objectContext = ((IObjectContextAdapter)this).ObjectContext;
    var deletedRelationships = objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted).Where(e => e.IsRelationship && !this.RelationshipContainsKeyEntry(e));
    var addedRelationships = objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Added).Where(e => e.IsRelationship);

    foreach (var relationship in addedRelationships)
        relationship.Delete();

    foreach (var relationship in deletedRelationships)
        relationship.ChangeState(EntityState.Unchanged);
}

private bool RelationshipContainsKeyEntry(System.Data.Entity.Core.Objects.ObjectStateEntry stateEntry)
{
    //prevent exception: "Cannot change state of a relationship if one of the ends of the relationship is a KeyEntry"
    //I haven't been able to find the conditions under which this happens, but it sometimes does.
    var objectContext = ((IObjectContextAdapter)this).ObjectContext;
    var keys = new[] { stateEntry.OriginalValues[0], stateEntry.OriginalValues[1] };
    return keys.Any(key => objectContext.ObjectStateManager.GetObjectStateEntry(key).Entity == null);
}
4
Jerther

Vous pouvez essayer de le faire manuellement, quelque chose comme ceci .. vous n'êtes pas sûr que cela fonctionne pour votre scénario mais vous pouvez l'essayer:

public void UndoAll(DbContext context)
    {
        //detect all changes (probably not required if AutoDetectChanges is set to true)
        context.ChangeTracker.DetectChanges();

        //get all entries that are changed
        var entries = context.ChangeTracker.Entries().Where(e => e.State != EntityState.Unchanged).ToList();

        //somehow try to discard changes on every entry
        foreach (var dbEntityEntry in entries)
        {
            var entity = dbEntityEntry.Entity;

            if (entity == null) continue;

            if (dbEntityEntry.State == EntityState.Added)
            {
                //if entity is in Added state, remove it. (there will be problems with Set methods if entity is of proxy type, in that case you need entity base type
                var set = context.Set(entity.GeType());
                set.Remove(entity);
            }
            else if (dbEntityEntry.State == EntityState.Modified)
            {
                //entity is modified... you can set it to Unchanged or Reload it form Db??
                dbEntityEntry.Reload();
            }
            else if (dbEntityEntry.State == EntityState.Deleted)
                //entity is deleted... not sure what would be the right thing to do with it... set it to Modifed or Unchanged
                dbEntityEntry.State = EntityState.Modified;                
        }
    }
4
jure

Vous pouvez appliquer ceci:

context.Entry(TEntity).Reload();

Je l'essaie et ça marche bien pour moi.

Remarque: Cette méthode ( Reload ) Recharge l'entité de la base de données en remplaçant les valeurs de propriété par les valeurs de la base de données. L'entité sera dans l'état Inchangé après avoir appelé cette méthode.

2
Rayan Elmakki

Je suis tombé sur une mauvaise surprise - appel à ChangeTracker.Entries () se bloque si vous devez annuler les modifications en raison d'une exception dans DbContext, par exemple.

System.InvalidOperationException: 
'The property 'Id' on entity type 'TestEntity' is part of a key and so cannot be modified or marked as modified. 
To change the principal of an existing entity with an identifying foreign key first delete the dependent and invoke 'SaveChanges' then associate the dependent with the new principal.'

alors je suis venu avec la version piratée de la restauration manuelle

    public async Task RollbackChanges()
    {
        var oldBehavoir = ChangeTracker.QueryTrackingBehavior;
        var oldAutoDetect = ChangeTracker.AutoDetectChangesEnabled;

        // this is the key - disable change tracking logic so EF does not check that there were exception in on of tracked entities
        ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
        ChangeTracker.AutoDetectChangesEnabled = false;

        var entries = ChangeTracker.Entries().ToList();

        foreach (var entry in entries)
        {
            switch (entry.State)
            {
                case EntityState.Modified:
                    await entry.ReloadAsync();
                    break;
                case EntityState.Deleted:
                    entry.State = EntityState.Modified; //Revert changes made to deleted entity.
                    entry.State = EntityState.Unchanged;
                    break;
                case EntityState.Added:
                    entry.State = EntityState.Detached;
                    break;
            }
        }

        ChangeTracker.QueryTrackingBehavior = oldBehavoir;
        ChangeTracker.AutoDetectChangesEnabled = oldAutoDetect;
    }
0
Evgeny