5

We are using Entity Framework Code First, and I am running into issues trying to rollback entity changes for Insert, Update, and Delete when we don't want to SaveChanges().

Specifically, I have a datagridview which I am using as a TableEditor, to make changes to some auxiliary tables. The datagridview is bound to DbSet<TEntity>.

My rollback for Update seems to work fine, I just set the currentValues to back to their OriginalValues, and change state to unchanged.

When a record is inserted to the gridview (but no changes saved), it never shows up in the entity class, and I never see it again... So I guess it doesn't make it to the dbSet, and no rollback is needed for this?

But my main problem lies with Delete:

From what I understand, when a record is "deleted" (eg.tableData.Remove(currentItem);), it is simply marked for deletion until SaveChanges is called. So if I change the State from deleted back to unchanged, that should handle the rollback, right?

Well, the record does show back up again, BUT the navigational properties of the record are gone! (ie. the columns containing foreign keys and required relationships to other entities). Why is this??!

Here is what I have so far:

    public void RollbackChanges(DbEntityEntry entry)
    {
        if (entry.State == EntityState.Modified)
        {
            foreach (var propertyName in entry.OriginalValues.PropertyNames)
            {
                entry.CurrentValues[propertyName] = entry.OriginalValues[propertyName];
            }
            entry.State = EntityState.Unchanged;
        }
        else if (entry.State == EntityState.Deleted)
        {
            entry.State = EntityState.Unchanged;
        }
        else if ((entry.State == EntityState.Added) || (entry.State == EntityState.Detached))
        {
            MessageBox.Show("I don't think this ever happens?");
        }
    }

Example usage:

    foreach (var entity in db.CertificationDecisions)
                {
                    DbEntityEntry entry = db.Entry(entity );
                    if (entry.State != EntityState.Unchanged)
                    {
                        RollbackChanges(entry);
                    }
                }

Any ideas why the navigational properties would disappear from the record? (Or what I can do to get them back?)


EDIT: @Chris regarding using Refresh:

I am using a DbContext, so I replaced my rollback methods with this line:

((IObjectContextAdapter)db).ObjectContext.Refresh(RefreshMode.StoreWins, db.CertificationDecisions);

However, this does not seem to reload the context, as the record is still missing... Am I using Refresh wrong?

This sounds like a possible solution to my problem, but I am still wondering why the navigation properties would be removed?

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
sǝɯɐſ
  • 2,470
  • 4
  • 33
  • 47
  • Are you rolling back changes committed to the database, or changes in your datagridview? – Chris Dec 19 '12 at 17:22
  • No changes are committed to the database as I don't call `SaveChanges()`... The datagridview is databound to an entity, and I am attempting to roll the entity's current values back to the original (or database) values – sǝɯɐſ Dec 19 '12 at 17:29
  • In that case, how critical is the performance of this piece of code or how large is the dataset? It may make sense to just refresh the datagridview from the database. – Chris Dec 19 '12 at 17:56
  • I'm not sure about this but it may be that after calling `Remove` EF cuts the associations with the removed object by doing "relationship fixup". So maybe you can restore the situation by forcing relationship fixup again after setting the state to `Unchanged`. You can do that by calling `context.ChangeTracker.DetectChanges()`. – Gert Arnold Dec 19 '12 at 23:14

2 Answers2

3

Any ideas why the navigational properties would disappear from the record?

The navigation properties are most likely removed due to a foreign-key relationship. Lets take the following classes:

public class Dog
{
  public guid ID { get; set; }
  public string Title { get; set; }
}

public class Person
{
  public guid ID { get; set; }

  public ICollection<Dog> FavoriteDogs { get; set; }
}

In the database, the collection of FavoriteDogs is a table that has a reference to both a Person and a Dog, but is hidden automatically by the navigation properties. When the Parent state is set to Deleted, EF automatically marks the reference entities as Deleted. Entity Framework will not set these reference entities to Unchanged if the Parent entity is set to Unchanged because there is no referential integrity required to do so (only for delete operations).

Or what I can do to get them back?

There is currently no automated process within EF that will do this. I don't believe there is even a manual way to change reference entities. And the new Entity Framework 5 does not expose any way to view or change the state of DbRefrenceEntry.

Maybe try DbReferenceEntry.Reload.

Community
  • 1
  • 1
Erik Philips
  • 53,428
  • 11
  • 128
  • 150
  • 1
    Interesting... Well, I tried changing my delete rollback from `entry.State = EntityState.Unchanged;` to `entry.Reload();`, and it seems to refresh and mark the entity state as unchanged again. It shows up under "Results View" when debugging, but not under "Local", and not in my datagridview... I am calling `DbSet.Load()` each time I load the form; why would they not show back up under "Local" and in my gridview? – sǝɯɐſ Dec 19 '12 at 20:17
2

This is not really an answer to my question, but is the alternative that seems to be working for me so far (in case anyone is looking for an answer to this question):

Basically, I create a new context and pass an entity from the new context to my form. If I cancel the form, I simply dispose of the context, so no changes are saved (no need to rollback). If I save the form, then I save the changes from the new context, but then I need to make my original context aware of the changes.

So I use .Refresh, which seems to successfully find new entities, but does not remove deleted ones. So then I loop through the entities and compare them to the database values.

If the database returns 'null', we know the entity has been deleted in the database and needs to be removed from the context. (However, I can't remove it in the loop, as trying to change the contents of a context while enumerating through it will cause an error, so instead I add the entries to a list to remove from the context in a seperate loop afterwards.)

Below is the code:
(note: I give no guarantee that this is a good solution, but it seems to be working for me)

            using (FmlaManagementContext auxDb = new FmlaManagementContext())
            {
                AuxiliaryTableEditor<State> statesTableEditor = new AuxiliaryTableEditor<State>(auxDb.States);
                if (statesTableEditor.ShowDialog() != DialogResult.OK)
                {
                    auxDb.Dispose();
                }
                else
                {
                    auxDb.SaveChanges();
                    auxDb.Dispose();

                    //refresh to pick up any new
                    ((IObjectContextAdapter)db).ObjectContext.Refresh(RefreshMode.StoreWins, db.States);

                    //loop through to remove deleted
                    List<State> nulls = new List<State>();
                    foreach (State state in db.States.Local)
                    {
                        DbEntityEntry entry = db.Entry(state);
                        DbPropertyValues databaseValues = entry.GetDatabaseValues();
                        if (databaseValues == null)
                        {
                            nulls.Add(state); 
                        }
                    }
                    foreach (State state in nulls)
                    {
                        db.States.Remove(state);
                    }
                }
            }

If I end up changing this or finding problems with it, I will try to remember to come back and update...

sǝɯɐſ
  • 2,470
  • 4
  • 33
  • 47