locked
Rejecting Changes where Enities have been deleted RRS feed

  • Question

  • Hi,
    I have used the following post to successfully reject changes to Modified and Added entities :
    But am unable to get changes to be rejected for entities that are in the Deleted state.
    I have tried setting the State to Unchanged for Deleted entities by adding a case as follows:
        foreach (var entry in context.ChangeTracker.Entries())
        {
            if (entry.State == EntityState.Modified)
            {
                entry.State = EntityState.Unchanged;
            }
            else if (entry.State == EntityState.Added)
            {
                entry.State = EntityState.Detached;
            }
            // Handle deleted entities:
            else if ( entry.State == EntityState.Deleted )
            {
                entry.State = EntityState.Unchanged;
            }
        }
    
    

    But this does not seem to reject changes to FK relationships for deleted entities, i.e. the entity does not appear to be put back into the collection which it was a member of before it was deleted.
    According to the post above changes to FK relationships should also be rejected.

    Thanks in advance,
    Joel Gordon.
    Monday, October 10, 2011 12:24 AM

Answers

  • Hi, Joel.

    You are right, the FK ParentId is not restored in this case. Why it happens - I don't know. Perhaps it's due to the fact that all data are located in the same table.

    But please note, on calling context.SaveChanges() NO sql update/delete script is issued. So, the main goal (to reject changes) is achived.

    The simple workaround to completely restore a local object graph is:

    else if (entry.State == EntityState.Deleted)
    {
    	string propertyName = "ParentId";
    	int parentId = (int)entry.OriginalValues[propertyName];
    					
    	entry.State = EntityState.Unchanged;
    					
    	entry.OriginalValues[propertyName] = parentId;
    	entry.State = EntityState.Unchanged;
    }
    
    

    Comments to the code: when you change entity state from Deleted to Unchanged,  EF erases FK property's Original value, and never reconstructs it again. We can remedy the situation by caching this value in temporary variable before trying to restore the entity, and later setting it back manually. Finally we need to set entity's state to Unchanged again to prevent the EF to consider this entity as Modified.

    Regards

    • Marked as answer by JoelGordon Thursday, October 13, 2011 3:25 AM
    Wednesday, October 12, 2011 8:18 AM

All replies

  • Hi, Joel.

    The blog states: ".. This will also reject changes to FK relationships since the original value of the FK will be restored."

    To rephrase it: if an entity HAS a FK property Entity Framework would be able to restore its original value on changes rejection. This happens because Entity Framework keeps original values of entities that it tracks.  And as FK property is an ordinary property, it gets restored its value as well and have its navigation property reset to its original value.

    But if an entity HAS NO Foreign Key property (this is the case when an entity is a container for other entities, has ICollection<T> property), Entity Framework has no means to reconstruct relationships to their related objects that it had before deletion.

    Let's consider a simple model that shows  Master (1) - (*) Detail  relationship:

    public class Master
    {
    	public int Id { get; set; }
    	public string MasterName { get; set; }
    	public virtual ICollection<Detail> Details { get; set; }
    
    	public Master()
    	{
    		this.Details = new HashSet<Detail>();
    	}
    }
    
    public class Detail
    {
    	public int Id { get; set; }
    	public string DetailName { get; set; }
    
    	public int MasterId { get; set; }
    	public virtual Master Master { get; set; }
    }
    
    <br/>
    

    When you delete/reject changes of Detail object - EF is able to restore its FK relationship because Detail has MasterId FK property.

    But if you delete/reject changes of Master object - because Master has no information (no FK defined) about its related objects, EF can't reconstruct its relationships.

    You can work around it by finding related objects first and then reject changes on them:

    foreach (DbEntityEntry<Detail> entry in db.ChangeTracker.Entries<Detail>())
    {
    	if (entry.Entity.MasterId == master.Id)
    		entry.State = System.Data.EntityState.Unchanged;
    }
    
    

     


    And in Many - to - Many relationship case it takes a little more of coding to reject changes, because neither of related entities has FK properties. In this case you need to reject deletion of relationships manually. Here are we assume that Master is in Many-to-Many with Detail (Master.Details < > Detail.Masters )

    Master m = db.Masters.Find(1);
    db.Masters.Remove(m);
    
    DbEntityEntry<Master> entry = db.Entry<Master>(m);
    entry.State = System.Data.EntityState.Unchanged;
    <br/>// getting underlying context and the entity's EntityKey				
    ObjectContext ctx = ((IObjectContextAdapter)db).ObjectContext;
    EntityKey key = ctx.ObjectStateManager.GetObjectStateEntry(m).EntityKey;
    <br/>// enumerating all ObjectStateEntries that are in Deleted state (both EntityEntry and RelationshipEntry will be returned)
    foreach (ObjectStateEntry rel in ctx.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Deleted))
    {
    	if (rel.IsRelationship)
    	{
    		DbDataRecord rec = rel.OriginalValues;
    		EntityKey key1 = rec[0] as EntityKey;
    		EntityKey key2 = rec[1] as EntityKey;
    
    		if (key == key1 || key == key2)
    		{
    			rel.ChangeState(EntityState.Unchanged);
    		}
    
    	}
    }
    
    

    Hope this helps

    Monday, October 10, 2011 9:41 AM
  • Hi Rustam,

       Thanks very much for the detailed answer.

    However EF does not seem to be restoring the FK relationship in the example I tried (see code below).

    The example is a simple implementation of the composite pattern where a Folder contains FolderItems, and each folder has a reference (FK relationship) to it's containing Folder (the "Parent" property).

    using System;
    using System.Data;
    using System.Data.Entity;
    using System.Collections.Generic;
    using System.Linq;
    
    namespace TestFolders
    {
        public abstract class FolderItem
        {
            public virtual int Id        { get; set; }
            public virtual string Name   { get; set; }
            public virtual int? ParentId { get; set; }
            public virtual Folder Parent { get; set; }
        }
    
        public class Folder : FolderItem
        {
            public virtual ICollection<FolderItem> Items { get; set; }
        }
    
        public class Person : FolderItem
        {
            public virtual string Address { get; set; }
        }
    
        public class FolderContext : DbContext
        {
            public DbSet<Folder> Folders { get; set; }
            public DbSet<Person> Persons { get; set; }
    
            public FolderContext() : base( "Data Source=(local);Initial Catalog=TestFolders;Integrated Security=SSPI;" )
            { 
            }
    
            protected override void OnModelCreating( DbModelBuilder modelBuilder )
            {
                modelBuilder.Entity<FolderItem>().HasOptional( e => e.Parent ).WithMany( ef => ef.Items ).HasForeignKey( e => e.ParentId );  
                base.OnModelCreating(modelBuilder);
            }
    
            public Folder RootFolder 
            {
                get
                {
                    return (from item in Folders where item.Parent == null let root = item select root).FirstOrDefault();
                }
            }
    
            public class Initializer : DropCreateDatabaseAlways<FolderContext>
            {
                protected override void Seed( FolderContext context )
                {
                    Folder rootFolder = context.Folders.Create();
                    rootFolder.Name = "RootFolder";
                    context.Folders.Add( rootFolder );
    
                    Person person1 = context.Persons.Create();
                    person1.Name = "John Smith";
                    person1.Address = "Address 1";
                    rootFolder.Items.Add( person1 );
    
                    Folder subFolder = context.Folders.Create();
                    subFolder.Name = "Sub-Folder";
                    rootFolder.Items.Add( subFolder );
    
                    context.SaveChanges();
                }
            }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                Database.SetInitializer<FolderContext>( new FolderContext.Initializer() );
    
                using (FolderContext context = new FolderContext())
                {
                    Person person = context.Persons.Where( e => e.Name == "John Smith" ).FirstOrDefault();
    
                    Console.WriteLine( "Before Delete:======================" );
                    PrintFolders( context.RootFolder, "" );
                    Console.WriteLine( "person.ParentId : " + person.ParentId );
    
                    context.Persons.Remove( person );
    
                    Console.WriteLine();
                    Console.WriteLine( "After Delete:======================" );
                    PrintFolders( context.RootFolder, "" );
    
                    RejectChanges( context );
    
                    Console.WriteLine();
                    Console.WriteLine( "After Reject Changes:==============" );
                    PrintFolders( context.RootFolder, "" );
    
                    Console.WriteLine();
                    Console.WriteLine( "====================================" );
                    Console.WriteLine( "person.ParentId : " + person.ParentId );
                }
            }
    
            private static void PrintFolders( Folder folder, string indent )
            {
                Console.WriteLine( indent + folder.Name );
                indent += "   ";
                foreach ( FolderItem item in folder.Items )
                {
                    if ( item is Folder )
                    {
                        PrintFolders( (Folder)item, indent );
                    }
                    else
                    {
                        Console.WriteLine( indent + item.Name + ", " + ((Person)item).Address );
                    }
                }
            }
    
            private static void RejectChanges( DbContext context )
            {
                foreach ( var entry in context.ChangeTracker.Entries() )
                {
                    if ( entry.State == EntityState.Modified )
                    {
                        entry.State = EntityState.Unchanged;
                    }
                    else if ( entry.State == EntityState.Added )
                    {
                        entry.State = EntityState.Detached;
                    }
                    else if ( entry.State == EntityState.Deleted )
                    {
                        entry.State = EntityState.Unchanged;
                    }
                }
            }
        }
    }
    

    The program creates a root folder which in turn contains a person and a sub-folder. It then deletes the person and restores the person (using the RejectChanges method).

     

    Output from running the program  is as following :

    Before Delete:======================
    RootFolder
       John Smith, Address 1
       Sub-Folder
    person.ParentId : 1
    
    After Delete:======================
    RootFolder
       Sub-Folder
    
    After Reject Changes:==============
    RootFolder
       Sub-Folder
    
    ====================================
    person.ParentId :

    As you can see the ParentId is not restored to what it was prior to being deleted?

     

    Am I missing something?

     

    Thanks again,

    Joel Gordon.

     

     

    Wednesday, October 12, 2011 12:48 AM
  • Hi, Joel.

    You are right, the FK ParentId is not restored in this case. Why it happens - I don't know. Perhaps it's due to the fact that all data are located in the same table.

    But please note, on calling context.SaveChanges() NO sql update/delete script is issued. So, the main goal (to reject changes) is achived.

    The simple workaround to completely restore a local object graph is:

    else if (entry.State == EntityState.Deleted)
    {
    	string propertyName = "ParentId";
    	int parentId = (int)entry.OriginalValues[propertyName];
    					
    	entry.State = EntityState.Unchanged;
    					
    	entry.OriginalValues[propertyName] = parentId;
    	entry.State = EntityState.Unchanged;
    }
    
    

    Comments to the code: when you change entity state from Deleted to Unchanged,  EF erases FK property's Original value, and never reconstructs it again. We can remedy the situation by caching this value in temporary variable before trying to restore the entity, and later setting it back manually. Finally we need to set entity's state to Unchanged again to prevent the EF to consider this entity as Modified.

    Regards

    • Marked as answer by JoelGordon Thursday, October 13, 2011 3:25 AM
    Wednesday, October 12, 2011 8:18 AM
  • Hi Rustam,

      Thanks for reply - looks like a possible bug.

    I agree with you that reject changes should mean that context.SaveChanges() issues no SQL, BUT it should also mean that the network of entity objects is restored to the way it was when it was loaded from the database (at least that's the way I want it to behave).

    Anyway the workaround code you posted should do the trick.

    Thanks again.

    Regards,

    Joel Gordon.

    Thursday, October 13, 2011 3:34 AM