MartinStettner
MartinStettner

Reputation: 29174

Undo changes in entity framework entities

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

Upvotes: 141

Views: 125012

Answers (13)

saluce
saluce

Reputation: 13360

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

At that point, you can retrieve the original values from the DbContext.

Upvotes: 18

Jerry
Jerry

Reputation: 4557

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.

Upvotes: 3

Taher  Rahgooy
Taher Rahgooy

Reputation: 6696

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

Upvotes: 201

Alejandro del Río
Alejandro del Río

Reputation: 4046

// 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

Upvotes: 7

IbrarMumtaz
IbrarMumtaz

Reputation: 4403

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.

Upvotes: 0

BizarroDavid
BizarroDavid

Reputation: 745

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

Upvotes: 2

Guish
Guish

Reputation: 5160

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

public void Rollback()
{
    dataContext.Dispose();
    dataContext= new MyEntities(yourConnection);
}

Upvotes: 13

Ilya Serbis
Ilya Serbis

Reputation: 22333

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

Upvotes: 40

Alex Pop
Alex Pop

Reputation: 655

I found this to be working fine in my context:

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

Upvotes: 2

"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

Upvotes: 3

Matyas
Matyas

Reputation: 181

This worked for me:

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

Where item is the customer entity to be reverted.

Upvotes: 18

Naz
Naz

Reputation: 5144

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.

Upvotes: 3

Ladislav Mrnka
Ladislav Mrnka

Reputation: 364399

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.

Upvotes: 74

Related Questions