开发者

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


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


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.


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


This worked for me:

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

Where item is the customer entity to be reverted.


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

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


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


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


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.


"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


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.


I found this to be working fine in my context:

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


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


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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜