19

Let's say you have these classes in your entities.

public class Parent
{
    public int ParentID { get; set; }
    public virtual ICollection<Child> Children { get; set; }
}

public class Child
{
    public int ChildID { get; set; }
    public int ParentID { get; set; }
    public virtual Parent Parent { get; set; }
}

And you have a user interface to update the Parent along with its Children, meaning if the user add new Child then you have to insert, if the user edits an existing Child then you need to update, and if the user removes a Child then you have to delete. Now obviously if you use the following code

public void Update(Parent obj)
{
    _parent.Attach(obj);
    _dbContext.Entry(obj).State = EntityState.Modified;
    _dbContext.SaveChanges();
}

it won't be able to detect the changes inside the Child because EF cannot detect changes inside a Navigation Property.

I've been asking this question for like 4 times and get mixed answers. So is it actually possible to do this stuff without it getting complicated? This problem can fix the problem by separating the user interface between Parent and Child but I don't want to because merging both Child and Parent in one menu is pretty common in business application development and more user friendly.

UPDATE : I'm trying the solution below but it doesn't work.

public ActionResult(ParentViewModel model)
{
    var parentFromDB = context.Parent.Get(model.ParentID);

    if (parentFromDB != null)
    {
        parentFromDB.Childs = model.Childs;
    }

    context.SaveChanges();
}

Instead of detecting changes inside the Children, EF won't be able to tell what to do with old child. For example if parentFromDB has 3 children the first time I pull it from DB then I delete the 2nd and 3rd child. Then I'm getting The relationship could not be changed because one or more of the foreign-key properties is non-nullable when saving.

I believe this is what happened : The relationship could not be changed because one or more of the foreign-key properties is non-nullable

Which took me back to square one because in my scenario, I can't just fetch from the DB and update the entry and call SaveChanges.

Community
  • 1
  • 1
tickwave
  • 3,335
  • 6
  • 41
  • 82
  • Not sure if I have misunderstood your question. Is disabling change-tracking a prerequisite? Because otherwise this is easily achieved with EF change-tracking. With change-tracking you don't need to explicitly set the state of entities, EF will do it for you, so any modifications you make to the Childs collection, including modifications on the entities in the collection will be automatically included in the change set when you commit the context (SaveChanges). – odyss-jii Oct 10 '15 at 17:03
  • I agree with odyss-jii's comment, but want to put additional info to it: of course this only works when you keep the context open between retrieval and changing the properties back. otherwise you'll have to set your entity state yourself, and for that you'll have to first get all in the context, so you'll have to create an entry there for each object and set its state. – DevilSuichiro Oct 10 '15 at 17:06
  • can you be more specific? Post a code if possible. This is what I mean (can't find the EF6 version). http://www.entityframeworktutorial.net/EntityFramework4.3/update-one-to-many-entity-using-dbcontext.aspx – tickwave Oct 10 '15 at 17:06
  • 1
    Similar question in SO : http://stackoverflow.com/questions/32964990/confuse-about-tracking-in-ef-updating-entity-with-child-collection http://stackoverflow.com/questions/18054798/entity-framework-not-saving-modified-children http://stackoverflow.com/questions/26366908/complete-insert-update-delete-of-child-entities-in-entity-framework – tickwave Oct 10 '15 at 17:07
  • a code snippet for the described functionality: var obj=_dbcontext.Parent.Include(x=>x.Childs).FirstOrDefault(); obj.childs.FirstOrDefault().Property=1; this will set the entity state of this child object to Modified automatically and will write changes back to the database upon calling SaveChanges(). – DevilSuichiro Oct 10 '15 at 17:11
  • @DevilSuichiro: can you post the complete code so I can mark it as answer? Let's say I have a Parent (P1) with 2 Childs (C1 and C2). Then I remove C1, modify C2, and add C3 & C4. So my Parent now have 3 Childs (C2, C3, and C4) and EF should delete C1 from DB, update C2, and add C3 and C4 in the DB. – tickwave Oct 10 '15 at 17:19
  • Consider to use https://github.com/WahidBitar/EF-Core-Simple-Graph-Update. It works well for me. – Michael Freidgeim Feb 05 '23 at 12:34

4 Answers4

14

because EF cannot detect changes inside Navigation Property

This seems to be a somewhat distorted description of the fact that _dbContext.Entry(obj).State = EntityState.Modified doesn't mark navigaton properties as modified.

Of course EF tracks changes in navigation properties. It tracks changes in properties and associations of all entities that are attached to a context. Therefore, the answer to your question, now positively stated...

Is it possible to update child collection in EF out of the box

... is: yes.

The only thing is: you don't do it out of the box.

The "out of the box" way to update any entity, whether it be a parent or a child in some collection is:

  • Fetch entities from the database.
  • Modify their properties or add/remove elements to their collections
  • Call SaveChanges().

That's all. Ef tracks the changes and you never set entity States explicitly.

However, in a disconnected (n-tier) scenario, this gets more complicated. We serialize and deserialize entities, so there can't be any context that tracks their changes. If we want to store the entities in the database, now it's our task to make EF know the changes. There are basically two ways to do this:

  • Set the states manually, based on what we know about the entities (like: a primary key > 0 means that they exist and should be updated)
  • Paint the state: retrieve the entities from the database and re-apply the changes from the deserialized entities to them.

When it comes to associations, we always have to paint the state. We have to get the current entities from the database and determine which children were added/deleted. There's no way to infer this from the deserialized object graph itself.

There various ways to alleviate this boring and elaborate task of painting the state, but that's beyond the scope of this Q&A. Some references:

Community
  • 1
  • 1
Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
1

Its cozs your doing it weirdly.

This requires Lazy loading for getting childs (obviously modify for your usage)

//get parent

var parent = context.Parent.Where(x => x.Id == parentId).SingleOrDefault();

wrote a whole test method for you. (apply to your case)

EmailMessage(parent) is the parent and it has none or many EmailAttachment's(child's)

 [TestMethod]
    public void TestMethodParentChild()
    {
        using (var context = new MyContext())
        {
            //put some data in the Db which is linked
            //---------------------------------
            var emailMessage = new EmailMessage
            {
                FromEmailAddress = "sss",
                Message = "test",
                Content = "hiehdue",
                ReceivedDateTime = DateTime.Now,
                CreateOn = DateTime.Now
            };
            var emailAttachment = new EmailAttachment
            {
                EmailMessageId = 123,
                OrginalFileName = "samefilename",
                ContentLength = 3,
                File = new byte[123]
            };
            emailMessage.EmailAttachments.Add(emailAttachment);
            context.EmailMessages.Add(emailMessage);
            context.SaveChanges();
            //---------------------------------


            var firstEmail = context.EmailMessages.FirstOrDefault(x => x.Content == "hiehdue");
            if (firstEmail != null)
            {
                //change the parent if you want

                //foreach child change if you want
                foreach (var item in firstEmail.EmailAttachments)
                {
                    item.OrginalFileName = "I am the shit";
                }
            }
            context.SaveChanges();


        }
    }

Update

Do your AutoMappper Stuff... as you said in your comment.

Then when you are ready to save and you have it back as the correct types ie once which represent entitys(Db) then do this.

var modelParent= "Some auto mapper magic to get back to Db types."

var parent = context.Parent.FirstOrDefault(x => x.Id == modelParent.Id);
//use automapper here to update the parent again

if (parent != null)
{
  parent.Childs = modelParent.Childs;
}
//this will update all childs ie if its not in the new list from the return 
//it will automatically be deleted, if its new it will be added and if it
// exists it will be updated.
context.SaveChanges();
Seabizkit
  • 2,417
  • 2
  • 15
  • 32
  • In my case, I would get the existing `EmailMessage` along with its properties aka `EmailAttachment` from context and use `AutoMapper` to map it into my `EmailMessageViewModel` (this ViewModel have 1:1 property with `EmailMessage`). After that I map the `EmailMessageViewModel` back into a new `EmailMessage` object. This new `EmailMessage` might have a modified/new/removed `EmailAttachment`. This is when I have no idea how to update it into the context. My original post : http://stackoverflow.com/questions/32964990/confuse-about-tracking-in-ef-updating-entity-with-child-collection – tickwave Oct 10 '15 at 17:31
  • Easy, fetch the original again... and use automapper to change the property's of the Entity(Parent) which is seen as attached to the context. Then set the Childs of the fetched entity to the new childs. eg. Parent.Childs = Modifyed Childs. EF is smart enough to work it all out for you.... which is why EF is awesome. – Seabizkit Oct 10 '15 at 17:36
  • Okay but what about newly added attachment or removed attachment? So far I take it as : 1. Fetch the original 2. Compare it with `EmailMessageViewModel` 3. Set the `EntityState` to `Modified` if same ID found, `Added` if not found, and `Deleted` if found in the fetched version but not in `EmailMessageViewModel` Will this work? – tickwave Oct 10 '15 at 17:44
  • alright, will give it a try later and mark as answer if worked. Thanks! – tickwave Oct 10 '15 at 17:48
  • defo works ;-) done it many times. EF is awesome once you know what you can and cant do. – Seabizkit Oct 10 '15 at 17:50
  • to note is: this is the same functionality me and a few others mentioned, but with another DB call instead of running through your object tree and set the entity state accordingly. choose which you prefer, in case you can't keep your object tree entirely in the context. – DevilSuichiro Oct 10 '15 at 19:56
  • @Seabizkit: Hello, it's been awhile but I just have the chance to try your solution, but it doesn't work. I've put all details on the first post. – tickwave Dec 28 '15 at 08:04
  • @warheat1990 at a guess you are not including the Child ID/Key you need to include this for it to be able to work out what has changed...'model.Childs' for-each child it must have the ID/Key, from the view include the key/ID in the post data..... – Seabizkit Dec 28 '15 at 08:38
1

I have spent hours trying different solutions to find out some decent way of dealing with this problem. The list is so long that I can't write all of them here but few are...

  • changing parent entity state
  • changing child entity state
  • attaching and detaching entity
  • clearing dbSet.Local to avoid tracking errors
  • tried writing customer logic in ChangeTracker
  • rewriting mapping logic between DB to View models
  • ....and so on....

Nothing worked but finally, here is how just a minor change solved the whole mess.

With this solution, you need to stop setting states manually. Just call dbSet.Update() method once, EF will take care of internal state management.

NOTE: This works even though you are working with detached graphs of entities or even with the entities having nested parent-child relationships.

Before code:

public void Update(Parent obj)
{
    _parent.Attach(obj);
    _dbContext.Entry(obj).State = EntityState.Modified;
    _dbContext.SaveChanges();
}

After code:

public void Update(Parent obj)
{
    dbSet.Update(obj);
    _dbContext.SaveChanges();
}

Reference: https://www.learnentityframeworkcore.com/dbset/modifying-data#:~:text=DbSet%20Update&text=The%20DbSet%20class%20provides,with%20individual%20or%20multiple%20entities.&text=This%20method%20results%20in%20the,by%20the%20context%20as%20Modified%20

Malik Khalil
  • 6,454
  • 2
  • 39
  • 33
  • 1
    The question explicitly mentions deleting an existing child, but this solution can't do that, can it? – GuyB Jan 13 '22 at 16:56
0

If you are using entityframework core, it provides dbSet.Update() method, which takes care of any update in any level of object tree. For reference please check the documentation link here

Usman
  • 333
  • 3
  • 14