locked
Entity Framework POCO with snapshot-based tracking RRS feed

  • Question

  • Hi,

    I have a Visual Studio 2010 generated set of POCO classes (just out of the box POCO templates generated from DB Schema).  For a given use case I let the user load an entity (a CRM contact) and act on it - add Phone Numbers (which in itself is a separate entity related by foreign key) and address (also a separate entity) etc.  In between postbacks I store the modified entities in ViewState (I don't want to save the changes to the database right away).  The problem arises when the user hits the Save button.  The main CRM Contact will be saved fine (any changes are detected and saved) but none of the related properties get saved - no matter if it's a new addition or modified EF just ignores it.

    How do I force Entity Framework to detect that I have changes in my related properties?  I'm using this to save my main contact:

      
        //contact is an instance of CRMContact retrieved from ViewState
        if (contact.Id == 0) {
            CRMEntities.CRMContacts.AddObject(contact);
        } else {
            CRMContact orig = CRMEntities.CRMContacts.Where(c => c.Id == contact.Id).FirstOrDefault();
            CRMEntities.CRMContacts.ApplyCurrentValues(contact);
        }
    
        CRMEntities.SaveChanges(SaveOptions.AcceptAllChangesAfterSave);

    This works fine for the contact entity but not for my related ones.  What do I need to add for phone numbers and emails to be added and/or updated?

        
    I tried looping through my navigation properties and doing AddObject or ApplyCurrentValues on each individually, however, with that code in place I get An item with the same key has already been added exception when saving changes, so I guess my question is how to properly create a "save" function in this situation


    Note that I don't want to use proxy-based change tracking.

    Thanks
    Wednesday, March 3, 2010 7:03 PM

Answers

  • Thanks David, but that was actually only a small part of the problem (I tried SaveChanges with any possible combinations of SaveOptions I could think of.  After a lot of trial and error I managed to put together something that works.  Note that I have no idea if this is the proper way of doing this.  This is a code from a different part of the project.  `IAHeader` is a main entity, `IAAttachment` and `IAComment` are both linked to the header by a foreign key:

       
    public static void Save(IAHeader head) {
            IAHeader orig = new IAHeader();
            if (head.Id == 0) {
                IAData.Entities.IAHeaders.AddObject(head);
            } else {
                orig = IAData.Entities.IAHeaders.Where(h => h.Id == head.Id).FirstOrDefault();
                IAData.Entities.IAHeaders.ApplyCurrentValues(head);
    
                foreach (IAComment comm in head.Comments.ToList()) {
                    if (comm.Id == 0) {
                        comm.IAHeader = null; //disassociate this entity from the parent, otherwise parent will be re-added
                        comm.IAId = head.Id;
                        IAData.Entities.IAComments.AddObject(comm);
                    } else {
                        IAComment origComm = orig.Comments.Where(c => c.Id == comm.Id).First();
                        IAData.Entities.IAComments.ApplyCurrentValues(comm);
                    }
                }
    
                foreach (IAAttachment att in head.Attachments.ToList()) {
                    if (att.Id == 0) {
                        att.IAHeader = null; //disassociate this entity from the parent, otherwise parent will be re-added
                        att.IAId = head.Id;
                        IAData.Entities.IAAttachments.AddObject(att);
                    } else {
                        IAAttachment origAtt = orig.Attachments.Where(a => a.Id == att.Id).First();
                        IAData.Entities.IAAttachments.ApplyCurrentValues(att);
                    }
                }
            }
            IAData.Entities.SaveChanges(SaveOptions.DetectChangesBeforeSave | SaveOptions.AcceptAllChangesAfterSave);
        }


    Lots of improvements can be made, obviously, but this is what I came up with so far that works for my scenario.  The important part that was mostly messing me up is having to disassociate my navigation properties from my main entity, otherwise I would either get the "entity key already exists" error or the main entity would be saved twice.
    Wednesday, March 3, 2010 11:14 PM

All replies

  • Calling SaveChanges with no parameters will automatically detect changes before a save. However in this case you have overridden the default behavior of SaveChanges by passing in SaveOptions.AcceptAllChangesAfterSave as a parameter. This stops the Entity Framework from detecting changes, so you will either need to change your code to call ObjectContext.DetectChanges before saving, or you can remove the SaveOptions parameter from your call to SaveChanges.

    Note that the SaveChanges overload calls the SaveChanges overload with one parameter with a value of SaveOptions.AcceptAllChangesAfterSave | SaveOptions.DetectChangesBeforeSave.

    Hope that helps,
    David

    Blog - http://blogs.rev-net.com/ddewinter/ Twitter - @ddewinter
    Wednesday, March 3, 2010 7:43 PM
    Answerer
  • Thanks David, but that was actually only a small part of the problem (I tried SaveChanges with any possible combinations of SaveOptions I could think of.  After a lot of trial and error I managed to put together something that works.  Note that I have no idea if this is the proper way of doing this.  This is a code from a different part of the project.  `IAHeader` is a main entity, `IAAttachment` and `IAComment` are both linked to the header by a foreign key:

       
    public static void Save(IAHeader head) {
            IAHeader orig = new IAHeader();
            if (head.Id == 0) {
                IAData.Entities.IAHeaders.AddObject(head);
            } else {
                orig = IAData.Entities.IAHeaders.Where(h => h.Id == head.Id).FirstOrDefault();
                IAData.Entities.IAHeaders.ApplyCurrentValues(head);
    
                foreach (IAComment comm in head.Comments.ToList()) {
                    if (comm.Id == 0) {
                        comm.IAHeader = null; //disassociate this entity from the parent, otherwise parent will be re-added
                        comm.IAId = head.Id;
                        IAData.Entities.IAComments.AddObject(comm);
                    } else {
                        IAComment origComm = orig.Comments.Where(c => c.Id == comm.Id).First();
                        IAData.Entities.IAComments.ApplyCurrentValues(comm);
                    }
                }
    
                foreach (IAAttachment att in head.Attachments.ToList()) {
                    if (att.Id == 0) {
                        att.IAHeader = null; //disassociate this entity from the parent, otherwise parent will be re-added
                        att.IAId = head.Id;
                        IAData.Entities.IAAttachments.AddObject(att);
                    } else {
                        IAAttachment origAtt = orig.Attachments.Where(a => a.Id == att.Id).First();
                        IAData.Entities.IAAttachments.ApplyCurrentValues(att);
                    }
                }
            }
            IAData.Entities.SaveChanges(SaveOptions.DetectChangesBeforeSave | SaveOptions.AcceptAllChangesAfterSave);
        }


    Lots of improvements can be made, obviously, but this is what I came up with so far that works for my scenario.  The important part that was mostly messing me up is having to disassociate my navigation properties from my main entity, otherwise I would either get the "entity key already exists" error or the main entity would be saved twice.
    Wednesday, March 3, 2010 11:14 PM