135

this might be a trivial question but: Since ADO.NET entity framework automatically tracks changes (in generated entities) and therefore keeps the original values, how can I rollback changes made to the entity objects?

I have a form which allows the user to edit a set of "Customer" entities in a grid view.

Now I have two buttons "Accept" and "Revert": if "Accept" is clicked, I call Context.SaveChanges() and the changed objects are written back to the database. If "Revert" is clicked, I would like for all objects to get their original property values. What would be the code for that?

Thanks

MartinStettner
  • 28,719
  • 15
  • 79
  • 106

13 Answers13

194

Query ChangeTracker of DbContext for dirty items. Set deleted items state to unchanged and added items to detached. For modified items, use original values and set current values of the entry. Finally set state of modified entry to unchanged:

public void RollBack()
{
    var context = DataContextFactory.GetDataContext();
    var changedEntries = context.ChangeTracker.Entries()
        .Where(x => x.State != EntityState.Unchanged).ToList();

    foreach (var entry in changedEntries)
    {
        switch(entry.State)
        {
            case EntityState.Modified:
                entry.CurrentValues.SetValues(entry.OriginalValues);
                entry.State = EntityState.Unchanged;
                break;
            case EntityState.Added:
                entry.State = EntityState.Detached;
                break;
            case EntityState.Deleted:
                entry.State = EntityState.Unchanged;
                break;
        }
    }
 }
Taher Rahgooy
  • 6,528
  • 3
  • 19
  • 30
  • 5
    You should probably set the original values to deleted entries as well. It's possible you first changed an item and deleted it after that. – Bas de Raad Mar 07 '14 at 20:21
  • 35
    Setting `State` to **EntityState.Unchanged** will override all values with `Original Values` as well so there is no need to call `SetValues` method. – Abolfazl Hosnoddin Apr 19 '14 at 09:49
  • Any idea what to do when Entries() throws? It's throwing for me for an entity that has a composite PK but the PK values were not yet set. – Yuyo Sep 08 '14 at 21:05
  • There is a scenario where this solution may not be the right one. It depends on whether saving happens independently of closing the edit dialog. If so, then here is a scenario to watch out for: 1) user opens edit dialog 2) edits and accepts changes (save is not performed here because save is a function of the bigger aggregate in parent window) 3) user takes a break/gets distracted/does other changes elsewhere, forgets if he/she edited that thing, and opens the edit dialog again 4) sees that changes are indeed good, cancels out of the window, then saves, unknowingly dismissing his changes – tomosius Apr 06 '16 at 21:15
  • 11
    Cleaner version of this answer: http://stackoverflow.com/a/22098063/2498426 – Jerther Sep 14 '16 at 19:27
  • 1
    Mate, this is awesome! Only modification I made is to use the the generic version of Entries() so that it works for my respositories. This gives me more control and I can roll back per entity type. Thanks! – Daniel Mackay Aug 17 '17 at 23:55
75

There is no revert or cancel changes operation in EF. Each entity has ObjectStateEntry in ObjectStateManager. State entry contains original and actual values so you can use original values to overwrite current values but you must do it manually for each entity. It will not reveret changes in navigation properties / relations.

The common way to "revert changes" is disposing context and reload entities. If you want to avoid reloading you must create clones of entities and modify those clones in new object context. If user cancel changes you will still have original entities.

Ladislav Mrnka
  • 360,892
  • 59
  • 660
  • 670
  • 4
    @LadislavMrnka Surely `Context.Refresh()` is a counter-example to your claim that there is no revert operation? Using `Refresh()` seems a better approach (i.e. more easily targeted at specific entities) than disposing the context and losing all tracked changes. – Rob Feb 14 '12 at 19:21
  • 15
    @robjb: No. Refresh is able to refresh only single entity or collection of entities which you manually define but refreshing functionality only affects simple properties (not relations). It also doesn't solve problem with added or deleted entities. – Ladislav Mrnka Feb 14 '12 at 20:38
37
dbContext.Entry(entity).Reload();

Accroding to MSDN:

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.

Note that reverting through the request to database has some drawbacks:

  • network traffic
  • DB overload
  • the increased application response time
Ilya Serbis
  • 21,149
  • 6
  • 87
  • 74
18

This worked for me:

dataContext.customer.Context.Refresh(RefreshMode.StoreWins, item);

Where item is the customer entity to be reverted.

DaveShaw
  • 52,123
  • 16
  • 112
  • 141
Matyas
  • 181
  • 1
  • 2
13

Easy way without tracking any changes. It should be faster than looking at every entities.

public void Rollback()
{
    dataContext.Dispose();
    dataContext= new MyEntities(yourConnection);
}
Guish
  • 4,968
  • 1
  • 37
  • 39
  • Time of creating a single entitity object... which is a couple of ms (50 ms). Looping through the collection may be a faster or longer depending of it's size. Performance wise O(1) is rarely a problem compare to O(n). [Big O notation](http://en.wikipedia.org/wiki/Big_O_notation) – Guish Jul 02 '14 at 18:46
  • Not following you - performance of disposing and recreating connection. I tested it on existing project and it finished somewhat faster then above `Rollback` procedure, which makes it far better choice if one wants to revert entire database state. Rollback could cherry pick tho. – majkinetor Jul 03 '14 at 12:02
  • 'n' means the number of objects. Recreating connection take about 50 ms... O(1) means it is always the same time `50ms+0*n= 50ms`. O(n) means the performance is influenced by the number of objects... performance is maybe `2ms+0.5ms*n`... so bellow 96 objects it would be faster but time would increase linearly with the amount of data. – Guish Jul 03 '14 at 14:56
  • If you are not going to cherry pick what is(n't) rolled back this is the way to go provided you are not worried about bandwidth. – Anthony Nichols Aug 24 '15 at 21:37
13

While this question predates Entity Framework Core, the EF Core developers have provided a simple solution to this problem.

As of EF Core 5.0, the ChangeTracker now provides a method to clear out tracked entities which is more efficient than detaching all changed entities.

context.ChangeTracker.Clear()

Stops tracking all currently tracked entities.

DbContext is designed to have a short lifetime where a new instance is created for each unit-of-work. This manner means all tracked entities are discarded when the context is disposed at the end of each unit-of-work. However, clearing all tracked entities using this method may be useful in situations where creating a new context instance is not practical.

This method should always be preferred over detaching every tracked entity. Detaching entities is a slow process that may have side effects. This method is much more efficient at clearing all tracked entities from the context.

Note that this method does not generate StateChanged events since entities are not individually detached.

Microsoft Documentation

saluce
  • 13,035
  • 3
  • 50
  • 67
7
// Undo the changes of all entries. 
foreach (DbEntityEntry entry in context.ChangeTracker.Entries()) 
{ 
    switch (entry.State) 
    { 
        // Under the covers, changing the state of an entity from  
        // Modified to Unchanged first sets the values of all  
        // properties to the original values that were read from  
        // the database when it was queried, and then marks the  
        // entity as Unchanged. This will also reject changes to  
        // FK relationships since the original value of the FK  
        // will be restored. 
        case EntityState.Modified: 
            entry.State = EntityState.Unchanged; 
            break; 
        case EntityState.Added: 
            entry.State = EntityState.Detached; 
            break; 
        // If the EntityState is the Deleted, reload the date from the database.   
        case EntityState.Deleted: 
            entry.Reload(); 
            break; 
        default: break; 
    } 
} 

It worked for me. However you must to reload your data from the context to bring the old data. Source here

Alejandro del Río
  • 3,966
  • 3
  • 33
  • 31
3

"This worked for me:

dataContext.customer.Context.Refresh(RefreshMode.StoreWins, item);

Where item is the customer entity to be reverted."


I have made tests with ObjectContext.Refresh in SQL Azure, and the "RefreshMode.StoreWins" fires a query against database for each entity and causes a performance leak. Based on microsoft documentation ():

ClientWins : Property changes made to objects in the object context are not replaced with values from the data source. On the next call to SaveChanges, these changes are sent to the data source.

StoreWins : Property changes made to objects in the object context are replaced with values from the data source.

ClientWins isn't a good ideia neither, because firing .SaveChanges will commit "discarded" changes to the datasource.

I dont' know what's the best way yet, because disposing the context and creating a new one is caused a exception with message: "The underlying provider failed on open" when I try to run any query on a new context created.

regards,

Henrique Clausing

3

We are using EF 4, with the Legacy Object context. None of the above solutions directly answered this for me -- although it DID answer it in the long run by pushing me in the right direction.

We can't just dispose and rebuild the context because some of the objects we have hanging around in memory (damn that lazy loading!!) are still attached to the context but have children that are yet-to-be-loaded. For these cases we need to bump everything back to original values without hammering the database and without dropping the existing connection.

Below is our solution to this same issue:

    public static void UndoAllChanges(OurEntities ctx)
    {
        foreach (ObjectStateEntry entry in
            ctx.ObjectStateManager.GetObjectStateEntries(~EntityState.Detached))
        {
            if (entry.State != EntityState.Unchanged)
            {
                ctx.Refresh(RefreshMode.StoreWins, entry.Entity);
            }
        }
    }

I hope this helps others.

Jerry
  • 4,507
  • 9
  • 50
  • 79
3

As for me, better method to do it is to set EntityState.Unchanged on every entity you want to undo changes on. This assures changes are reverted on FK and has a bit more clear syntax.

Naz
  • 5,104
  • 8
  • 39
  • 63
2

I found this to be working fine in my context:

Context.ObjectStateManager.ChangeObjectState(customer, EntityState.Unchanged);

Alex Pop
  • 655
  • 1
  • 11
  • 20
  • 1
    I believe this will prevent changes to the entity from persisting upon calling `DbContext.SaveChanges()`, but it will not return the entity values to the original values. And if the entity state becomes modified from a later change, possibly all the previous modifications will be persisted upon saving? – Carl G Dec 26 '12 at 04:37
  • 1
    Check this link http://code.msdn.microsoft.com/How-to-undo-the-changes-in-00aed3c4 It says that setting an entity to Unchaged state restores the original values "under the covers". – Hannish Oct 21 '13 at 13:56
2

This is an example of what Mrnka is talking about. The following method overwrites an entity's current values with the original values and doesn't call out the database. We do this by making use of the OriginalValues property of DbEntityEntry, and make use of reflection to set values in a generic way. (This works as of EntityFramework 5.0)

/// <summary>
/// Undoes any pending updates 
/// </summary>
public void UndoUpdates( DbContext dbContext )
{
    //Get list of entities that are marked as modified
    List<DbEntityEntry> modifiedEntityList = 
        dbContext.ChangeTracker.Entries().Where(x => x.State == EntityState.Modified).ToList();

    foreach(  DbEntityEntry entity in modifiedEntityList ) 
    {
        DbPropertyValues propertyValues = entity.OriginalValues;
        foreach (String propertyName in propertyValues.PropertyNames)
        {                    
            //Replace current values with original values
            PropertyInfo property = entity.Entity.GetType().GetProperty(propertyName);
            property.SetValue(entity.Entity, propertyValues[propertyName]); 
        }
    }
}
BizarroDavid
  • 745
  • 8
  • 9
0

Some good ideas above, I chose to implement ICloneable and then a simple extension method.

Found here: How do I clone a generic list in C#?

To be used as:

ReceiptHandler.ApplyDiscountToAllItemsOnReciept(LocalProductsOnReciept.Clone(), selectedDisc);

This way I was able to clone my product entities list, apply a discount to each item and not have to worry about reverting any changes on the original entity. No need to talk with the DBContext and ask for a refresh or work with the ChangeTracker. You might say I am not making full use of EF6 but this is a very nice and simple implementation and avoids a DB hit. I cannot say whether or not this has a performance hit.

Community
  • 1
  • 1
IbrarMumtaz
  • 4,235
  • 7
  • 44
  • 63