72

I have a desktop client application that uses modal windows to set properties for hierarchical objects. Since this is a client application and access to the DbContext is not threaded, I use a long-running context on the main Form that gets passed around to modal children.

These modal windows use the PropertyGrid to display entity properties and also have cancel buttons. If any data is modified and the cancel button is pressed, the changes are reflected in the parent form (where I cannot dispose the DbContext object).

Is there a way to discard any changes made if the DbContext.SaveChanges() method has NOT been called?

UPDATE: Entity Framework Version 4.4.

Galma88
  • 2,398
  • 6
  • 29
  • 50
Raheel Khan
  • 14,205
  • 13
  • 80
  • 168
  • The application does not keep the DbContext object throughout its lifetime. Editing a hierarchical object is also a unit of work that requires children to be edited. In my case, I am stuck with modal windows and connected/attached entities. – Raheel Khan May 08 '13 at 09:33
  • 11
    Use a DTO (or a clone of the edited object) in the modal window. When the edit is cancelled, just discard the DTO and nothing happens to the original object. When you want to save first copy the DTO values to the original object and save changes. – Gert Arnold May 08 '13 at 11:27
  • 5
    @GertArnold: Over time, your advice has lasted and served better than performing acrobatics on the entity classes. – Raheel Khan Apr 24 '16 at 18:28

9 Answers9

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

Update:

Some users suggest to add .ToList() to avoid 'collection was modified' exception. But I believe there is a reason for this exception.

How do you get this exception? Probably, you are using context in non threadsafe manner.

Sergey Shuvalov
  • 2,098
  • 2
  • 17
  • 21
  • 13
    In the **Entity.Modified** Case you don't need to set the **CurrentValues** to the **OriginalValues**. Changing the state to **Unchanged** will do it for you ^.^! – MaxVerro Oct 08 '15 at 12:04
  • See my answer. It adds support for navigation property changes to this excellent answer. – Jerther Sep 05 '17 at 14:58
  • 3
    For me this was throwing "collection was modified .. " exception. Changed ChangeTracker.Entries() to ChangeTracker.Entries().ToList() to avoid the exception. – T M Aug 30 '18 at 10:56
  • 1
    context.TEntity.Local.Clear(); https://stackoverflow.com/questions/5466677/undo-changes-in-entity-framework-entities/56103270 – RouR May 12 '19 at 20:37
  • Doesn't have to be in a non threadsafe manner. Using it purely synchronously in EFCore – Captain Prinny Oct 15 '19 at 20:54
21

In the simple case of cancelling the changes made to properties of a single entity you can set the current values to the original values.

context.Entry(myEntity).CurrentValues.SetValues(context.Entry(myEntity).OriginalValues);
//you may also need to set back to unmodified -
//I'm unsure if EF will do this automatically
context.Entry(myEntity).State = EntityState.UnModified;

or alternatively reload (but results in db hit)

context.Entry(myEntity).Reload();

  • 6
    You don't need to set the **CurrentValues** to the **OriginalValues**. Changing the entity **State** to **Unchanged** will do it for you ^.^! – MaxVerro Oct 08 '15 at 12:04
  • Will throw an exception if myEntity has its state set to Deleted. – Jerther Sep 14 '16 at 19:11
  • @MaxVerro Changing state to Unchanged only restores values of properties having primitive data types. In my case, an item was added in the collection and changing state to Unchanged didn't restore the collection to its original state ... neither Reload() worked well for the collection property. – Syed Irfan Ahmad Feb 09 '22 at 10:16
14

How about wrapping it in a transaction?

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

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

        scope.Complete();
}
Martin
  • 920
  • 7
  • 24
  • 1
    I have to +1 this answer simply because too many questions have been asked about the effects of calling `context.SaveChanges` multiple times within a transaction. This does not however, address the core question. – Raheel Khan Apr 24 '16 at 18:34
  • 2
    Took a while but this is what we ended up using. @Gert Arnold's comment on the question should be noted as the best practice though. – Raheel Khan Sep 28 '16 at 06:10
9

This is based on Surgey Shuvalov's answer. It adds support for navigation property changes.

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);
}
Jerther
  • 5,558
  • 8
  • 40
  • 59
  • The exception related to `KeyEntry` is thrown when `Entity` property is null. It is defined like this: `get { return null == this._wrappedEntity; }` from the disassembled module. – Marco Luzzara Oct 23 '18 at 14:02
  • @Jerther in what scenario the two foreach loops will be executed? I tried by adding and removing an item from my Shipment.ShipmentItems collection but loops didn't get executed (as i was expecting). – Syed Irfan Ahmad Feb 09 '22 at 13:31
6

You colud try to do it manually, something like this.. not sure this works for your scenario but you can give it a try:

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;                
        }
    }
Jurica Smircic
  • 6,117
  • 2
  • 22
  • 27
  • 2
    This is very good. The only thing I changed was that in case of a deleted entity setting the entityState to modified will not cut it. Instead dbEntityEntry.Reload(); will provide the required effect (just as in case of a modified entity). – Daniel May 04 '14 at 15:02
  • @Daniel well it will cause a connection to db and may not be performant when you have a lot of deleted entities. Can you suggest an alternative? – Bamdad Jan 16 '21 at 07:34
  • Changing state to Unchanged only restores values of properties having primitive data types. In my case, an item was added in the collection and changing state to Unchanged didn't restore the collection to its original state ... neither Reload() worked well for the collection property. – Syed Irfan Ahmad Feb 09 '22 at 10:31
5

You can apply this:

context.Entry(TEntity).Reload();

I try it and its work well for me.

Note: This method (Reload) Reloads the entity from the database overwriting any property values with values from the database. The entity will be in the Unchanged state after calling this method.

Rayan Elmakki
  • 1,144
  • 13
  • 7
3

If we want to discard all the changes regardless of any type of change, in entity framework core we can do it in single steps.

DbContextObject.ChangeTracker.Clear()

Please refer the link below for reference.

https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.changetracking.changetracker.clear?view=efcore-5.0

Nitin Jain
  • 123
  • 7
  • No, this only clears the change tracker. Not the changes in the entities. Also, now all entities must be re-attached to save any new changes. This doesn't solve OP's problem. – Gert Arnold Jun 15 '21 at 09:07
  • @GertArnold Here's an example where this could be helpful: Say you're saving some records to the database and it failed due to a certain error. You are logging your errors into the DB within a middleware, so you need to disregard any failure that occurs and insert a error log entry within the log table using the same DB context in order not to create a new instance. So you clear all the changes like this not caring about the data since you'll be saving an error log entry and returning an error response. – Ziad Akiki Jul 24 '21 at 12:22
  • All well and good, but this doesn't "discard any changes made" as OP wants. You're answering from your own frame of mind, not OP's. – Gert Arnold Jul 24 '21 at 12:29
  • I stumbled on this thread looking for exactly this! I guess my problem didn't perfectly match OP's, but thanks a lot for the answer! – Christian Fosli Oct 31 '22 at 08:50
1

I encountered a problem with Jerther's solution, in the situation where a relationship containing a Key Entry has been deleted, with this exception being thrown:

A relationship from the 'TableAValue_TableA' AssociationSet is in the 'Deleted' state. Given multiplicity constraints, a corresponding 'TableAValue_TableA_Source' must also in the 'Deleted' state.

The problem appears to be that RejectNavigationChanges() can't restore the deleted relationship to its previous state, because it contains a Key Entry, but the associated objects have already been restored by RejectScalarChanges().

The solution is to change the way RejectScalarChanges() restores deleted entities, to using entry.Reload().

My working solution:

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

private void RejectScalarChanges()
{
    var changedEntries = _dbContext.ChangeTracker.Entries()
        .Where(e => e.State != EntityState.Unchanged);

    foreach (var entry in changedEntries)
    {
        switch (entry.State)
        {
            case EntityState.Added:
                entry.State = EntityState.Detached;
                break;

            case EntityState.Modified:
                entry.State = EntityState.Unchanged; 
                break; 

            // Where a Key Entry has been deleted, reloading from the source is required to ensure that the entity's relationships are restored (undeleted).
            case EntityState.Deleted:
                entry.Reload();
                break;
        }
    }
}

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

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

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

    bool RelationshipContainsKeyEntry(ObjectStateEntry stateEntry)
    {
        var keys = new[] { stateEntry.OriginalValues[0], stateEntry.OriginalValues[1] };
        return keys.Any(key => objectContext.ObjectStateManager.GetObjectStateEntry(key).Entity == null);
    }
}

Gary Pendlebury
  • 336
  • 5
  • 8
0

I came across nasty surprise - call to ChangeTracker.Entries() crashes if you need to rollback changes due to exception in DbContext e.g.

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.'

so I came up with hacked version of manual rollback

    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;
    }
Evgeny
  • 181
  • 2
  • 8
  • I can't test at the moment but, perhaps you could try calling `ChangeTracker.Entries()` before disabling tracking. – Raheel Khan Mar 05 '18 at 10:19
  • It crashes... I have to scrap all this, does not work on other scenarious – Evgeny Mar 09 '18 at 03:22
  • With all the improvements we're bringing on the original code, I think it would be worth a new project on github along with a NuGet package. Just an idea though as I don't work with EF anymore. – Jerther Jan 22 '19 at 14:41