111

Entity Framework 4, POCO objects and ASP.Net MVC2. I have a many to many relationship, lets say between BlogPost and Tag entities. This means that in my T4 generated POCO BlogPost class I have:

public virtual ICollection<Tag> Tags {
    // getter and setter with the magic FixupCollection
}
private ICollection<Tag> _tags;

I ask for a BlogPost and the related Tags from an instance of the ObjectContext and send it to another layer (View in the MVC application). Later I get back the updated BlogPost with changed properties and changed relationships. For example it had tags "A" "B" and "C", and the new tags are "C" and "D". In my particular example there are no new Tags and the properties of the Tags never change, so the only thing which should be saved is the changed relationships. Now I need to save this in another ObjectContext. (Update: Now I tried to do in the same context instance and also failed.)

The problem: I can't make it save the relationships properly. I tried everything I found:

  • Controller.UpdateModel and Controller.TryUpdateModel don't work.
  • Getting the old BlogPost from the context then modifying the collection doesn't work. (with different methods from the next point)
  • This probably would work, but I hope this is just a workaround, not the solution :(.
  • Tried Attach/Add/ChangeObjectState functions for BlogPost and/or Tags in every possible combinations. Failed.
  • This looks like what I need, but it doesn't work (I tried to fix it, but can't for my problem).
  • Tried ChangeState/Add/Attach/... the relationship objects of the context. Failed.

"Doesn't work" means in most cases that I worked on the given "solution" until it produces no errors and saves at least the properties of BlogPost. What happens with the relationships varies: usually Tags are added again to the Tag table with new PKs and the saved BlogPost references those and not the original ones. Of course the returned Tags have PKs, and before the save/update methods I check the PKs and they are equal to the ones in the database so probably EF thinks that they are new objects and those PKs are the temp ones.

A problem I know about and might make it impossible to find an automated simple solution: When a POCO object's collection is changed, that should happen by the above mentioned virtual collection property, because then the FixupCollection trick will update the reverse references on the other end of the many-to-many relationship. However when a View "returns" an updated BlogPost object, that didn't happen. This means that maybe there is no simple solution to my problem, but that would make me very sad and I would hate the EF4-POCO-MVC triumph :(. Also that would mean that EF can't do this in the MVC environment whichever EF4 object types are used :(. I think the snapshot based change tracking should find out that the changed BlogPost has relationships to Tags with existing PKs.

Btw: I think the same problem happens with one-to-many relations (google and my colleague say so). I will give it a try at home, but even if that works that doesn't help me in my six many-to-many relationships in my app :(.

Community
  • 1
  • 1
peterfoldi
  • 7,451
  • 5
  • 21
  • 19
  • Please post your code. This is a common scenario. – John Farrell Sep 03 '10 at 13:17
  • 1
    I have an automatic solution to this problem, it's hidden in the answers below so many would miss it but please take a look as it will save you a hell of a job [see post here](http://stackoverflow.com/questions/3635071/update-relationships-when-saving-changes-of-ef4-poco-objects/13820802#13820802) – refactorthis Feb 27 '13 at 00:32
  • @brentmckendrick I think another approach is better. Instead of sending the entire modified object graph over the wire, why not instead just send the delta? You wouldn't even need generated DTO classes in that case. If you have an opinion about this either way, please let's discuss is at http://stackoverflow.com/questions/1344066/calculate-object-delta . – HappyNomad Nov 18 '13 at 05:21

5 Answers5

148

Let's try it this way:

  • Attach BlogPost to context. After attaching object to context the state of the object, all related objects and all relations is set to Unchanged.
  • Use context.ObjectStateManager.ChangeObjectState to set your BlogPost to Modified
  • Iterate through Tag collection
  • Use context.ObjectStateManager.ChangeRelationshipState to set state for relation between current Tag and BlogPost.
  • SaveChanges

Edit:

I guess one of my comments gave you false hope that EF will do the merge for you. I played a lot with this problem and my conclusion says EF will not do this for you. I think you have also found my question on MSDN. In reality there is plenty of such questions on the Internet. The problem is that it is not clearly stated how to deal with this scenario. So lets have a look on the problem:

Problem background

EF needs to track changes on entities so that persistance knows which records have to be updated, inserted or deleted. The problem is that it is ObjectContext responsibility to track changes. ObjectContext is able to track changes only for attached entities. Entities which are created outside the ObjectContext are not tracked at all.

Problem description

Based on above description we can clearly state that EF is more suitable for connected scenarios where entity is always attached to context - typical for WinForm application. Web applications requires disconnected scenario where context is closed after request processing and entity content is passed as HTTP response to the client. Next HTTP request provides modified content of the entity which has to be recreated, attached to new context and persisted. Recreation usually happends outside of the context scope (layered architecture with persistance ignorace).

Solution

So how to deal with such disconnected scenario? When using POCO classes we have 3 ways to deal with change tracking:

  • Snapshot - requires same context = useless for disconnected scenario
  • Dynamic tracking proxies - requires same context = useless for disconnected scenario
  • Manual synchronization.

Manual synchronization on single entity is easy task. You just need to attach entity and call AddObject for inserting, DeleteObject for deleting or set state in ObjectStateManager to Modified for updating. The real pain comes when you have to deal with object graph instead of single entity. This pain is even worse when you have to deal with independent associations (those that don't use Foreign Key property) and many to many relations. In that case you have to manually synchronize each entity in object graph but also each relation in object graph.

Manual synchronization is proposed as solution by MSDN documentation: Attaching and Detaching objects says:

Objects are attached to the object context in an Unchanged state. If you need to change the state of an object or the relationship because you know that your object was modified in detached state, use one of the following methods.

Mentioned methods are ChangeObjectState and ChangeRelationshipState of ObjectStateManager = manual change tracking. Similar proposal is in other MSDN documentation article: Defining and Managing Relationships says:

If you are working with disconnected objects you must manually manage the synchronization.

Moreover there is blog post related to EF v1 which criticise exactly this behavior of EF.

Reason for solution

EF has many "helpful" operations and settings like Refresh, Load, ApplyCurrentValues, ApplyOriginalValues, MergeOption etc. But by my investigation all these features work only for single entity and affects only scalar preperties (= not navigation properties and relations). I rather not test this methods with complex types nested in entity.

Other proposed solution

Instead of real Merge functionality EF team provides something called Self Tracking Entities (STE) which don't solve the problem. First of all STE works only if same instance is used for whole processing. In web application it is not the case unless you store instance in view state or session. Due to that I'm very unhappy from using EF and I'm going to check features of NHibernate. First observation says that NHibernate perhaps has such functionality.

Conclusion

I will end up this assumptions with single link to another related question on MSDN forum. Check Zeeshan Hirani's answer. He is author of Entity Framework 4.0 Recipes. If he says that automatic merge of object graphs is not supported, I believe him.

But still there is possibility that I'm completely wrong and some automatic merge functionality exists in EF.

Edit 2:

As you can see this was already added to MS Connect as suggestion in 2007. MS has closed it as something to be done in next version but actually nothing had been done to improve this gap except STE.

Ladislav Mrnka
  • 360,892
  • 59
  • 660
  • 670
  • This won't add the Tags, that's good. It adds the new relationships, that's also good. It doesn't remove the existing relationships. That's bad :(. using (var context = new Entities.Blog()) { context.BlogPosts.Attach(BlogPost); context.ObjectStateManager.ChangeObjectState(BlogPost, System.Data.EntityState.Modified); foreach (Tag t in BlogPost.Tags) { context.ObjectStateManager.ChangeRelationshipState(BlogPost, t, c => c.Tags, System.Data.EntityState.Added); } context.DetectChanges(); context.SaveChanges(); } – peterfoldi Sep 03 '10 at 13:12
  • It also fails when user edits the same BlogPost again and tried to save. It says: "An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key." On the line: context.BlogPosts.Attach(BlogPost); – peterfoldi Sep 03 '10 at 13:14
  • But that are elementary principles when working with ORM. You have to say context what was deleted. It didn't delete record which is not known. – Ladislav Mrnka Sep 03 '10 at 13:35
  • You have to track changes. Context doesn't know about them unitl you show it what was changed. If you want to go easier way you can explicitly load the whole object graph again and merge loaded and attached object with data from View. – Ladislav Mrnka Sep 03 '10 at 13:38
  • "If you want to go easier way you can explicitly load the whole object graph again and merge loaded and attached object with data from View" This is what I want. Snapshot based change tracking done by EF and not me. Loading the graph is ok, since in the controller I would need to go to the database anyway if I want to find out the changes. Could you please give me some hints? I tried this already. I got the original graph and then tried to apply the changes in different ways, but it never worked :(. – peterfoldi Sep 03 '10 at 13:45
  • "But that are elementary principles when working with ORM. You have to say context what was deleted. It didn't delete record which is not known." This sounds reasonable, except that I expect a feature, which is: "Here is my BlogPost object in its new state including all the relationships. Please update the database accordingly." – peterfoldi Sep 03 '10 at 13:51
  • I have added some description because you have misconceived me. Easier solution with reloading object graph will not do the merge for you. It will only allow you easily find which relationships are new and wich are deleted. You will still have to set their state. – Ladislav Mrnka Sep 04 '10 at 14:02
  • I see. My conclusion was similar. And I also decided to have a look at NHibernate hoping that it can do this very basic thing for me. Thank you for the answer. – peterfoldi Sep 05 '10 at 14:11
  • 9
    This is one of the best answers I've read on SO. You've clearly stated what so many MSDN articles, documentation, and blog posts on the topic failed to get across. EF4 doesn't inherently support updating relationships from "detached" entities. It only provides tools for you to implement it yourself. Thank you! – tyriker Nov 17 '10 at 16:34
  • 1
    So after few month past, how about the NHibernate related to this issue compared to EF4? – CallMeLaNN Apr 07 '11 at 03:15
  • @CallMeLaNN: Few months passed but I was forced to use EF for the whole time because of company policy. After that I changed the job but I haven't investigate whole related functionality in NHibernate yet. There is one workaround in ASP.NET MVC - loading the graph from database and using `TryUpdateModel` in controller but that will pass a lot of logich into controller. – Ladislav Mrnka Jun 04 '11 at 22:19
  • I leave the EF and I used the Fluent NHibernate which is very nice because don't need to deal with XML configuration. NHibernate also have SchemaExport which will update db from entitiy classes, seems the features compared to EF not much different. Currently I am not using many-to-many rel in my project but I test the relationship before and seems got no problem. Quite nice and stright forward, just it takes time to learn and build the first config. then developing eintitiy classes quite interesting. I also use session per request. – CallMeLaNN Jun 06 '11 at 01:46
  • 1
    This is very well supported in NHibernate :-) no need to manually merge, in my example it's 3-level deep object graph, question has answers, each answer has comments, and question has comments too. NHibernate can persist/merge your object graph, no matter how complex it is http://www.ienablemuch.com/2011/01/nhibernate-saves-your-whole-object.html Another satisfied NHibernate user: http://www.codinginstinct.com/2009/11/nhibernate-feature-saveorupdatecopy.html – Michael Buen Aug 09 '11 at 11:30
  • 2
    One of the best explanations which I have ever read !! Thanks a lot – marvelTracker Jun 14 '12 at 10:14
  • @Ladislav Mrnka , Is this question still valid in the Entity Framework 4.3 or Are there any alternative ways of achieving this in new version ? – marvelTracker Jun 15 '12 at 03:01
  • Look like in EF 5 they still have the problem, thanks for all post and comment here, it is the best explanations i have read for this problem, and i have read a lot about this. Thanks Ladislav for always be there for our EF questions, well i think we could now ask you about NHibernates issues :P – RJardines Oct 25 '12 at 17:59
  • Hi everyone. Please take a look at my answer below which solves this issue in EF. Hopefully the idea will be picked up by the EF team themselves to use in the future. – refactorthis Dec 11 '12 at 12:52
  • 2
    The EF team plans to address this post-EF6. You may want to vote for http://entityframework.codeplex.com/workitem/864 – Eric J. Feb 11 '13 at 20:04
19

I have a solution to the problem that was described above by Ladislav. I have created an extension method for the DbContext which will automatically perform the add/update/delete's based on a diff of the provided graph and persisted graph.

At present using the Entity Framework you will need to perform the updates of the contacts manually, check if each contact is new and add, check if updated and edit, check if removed then delete it from the database. Once you have to do this for a few different aggregates in a large system you start to realize there must be a better, more generic way.

Please take a look and see if it can help http://refactorthis.wordpress.com/2012/12/11/introducing-graphdiff-for-entity-framework-code-first-allowing-automated-updates-of-a-graph-of-detached-entities/

You can go straight to the code here https://github.com/refactorthis/GraphDiff

Eric J.
  • 147,927
  • 63
  • 340
  • 553
refactorthis
  • 1,893
  • 15
  • 9
9

I know it's late for the OP but since this is a very common issue I posted this in case it serves someone else. I've been toying around with this issue and I think I got a fairly simple solution, what I do is:

  1. Save main object (Blogs for example) by setting its state to Modified.
  2. Query the database for the updated object including the collections I need to update.
  3. Query and convert .ToList() the entities I want my collection to include.
  4. Update the main object's collection(s) to the List I got from step 3.
  5. SaveChanges();

In the following example "dataobj" and "_categories" are the parameters received by my controller "dataobj" is my main object, and "_categories" is an IEnumerable containing the IDs of the categories the user selected in the view.

    db.Entry(dataobj).State = EntityState.Modified;
    db.SaveChanges();
    dataobj = db.ServiceTypes.Include(x => x.Categories).Single(x => x.Id == dataobj.Id);
    var it = _categories != null ? db.Categories.Where(x => _categories.Contains(x.Id)).ToList() : null;
    dataobj.Categories = it;
    db.SaveChanges();

It even works for multiple relations

c0y0teX
  • 1,462
  • 1
  • 23
  • 25
7

The Entity Framework team is aware that this is a usability issue and plans to address it post-EF6.

From the Entity Framework team:

This is a usability issue that we are aware of and is something we have been thinking about and plan to do more work on post-EF6. I have created this work item to track the issue: http://entityframework.codeplex.com/workitem/864 The work item also contains a link to the user voice item for this--I encourage you to vote for it if you have not done so already.

If this impacts you, vote for the feature at

http://entityframework.codeplex.com/workitem/864

Eric J.
  • 147,927
  • 63
  • 340
  • 553
1

All of the answers were great to explain the problem, but none of them really solved the problem for me.

I found that if I didn't use the relationship in the parent entity but just added and removed the child entities everything worked just fine.

Sorry for the VB but that is what the project I am working in is written in.

The parent entity "Report" has a one to many relationship to "ReportRole" and has the property "ReportRoles". The new roles are passed in by a comma separated string from an Ajax call.

The first line will remove all the child entities, and if I used "report.ReportRoles.Remove(f)" instead of the "db.ReportRoles.Remove(f)" I would get the error.

report.ReportRoles.ToList.ForEach(Function(f) db.ReportRoles.Remove(f))
Dim newRoles = If(String.IsNullOrEmpty(model.RolesString), New String() {}, model.RolesString.Split(","))
newRoles.ToList.ForEach(Function(f) db.ReportRoles.Add(New ReportRole With {.ReportId = report.Id, .AspNetRoleId = f}))