13

I have a 1..* relationship between Review and Recommendations.

The relevant portion of my model (which is also the POCO mapped by EF4):

public class Review
{
   public ICollection<Recommendations> Recommendations { get; set; }
}

On an Edit View, i represent the Recommendations as a set of checkboxes.

When i try and add a new Recommendation as part of editing the Review (e.g check another box), nothing is happening - and i know why...

I use the "stub technique" to update my entities - e.g i create a entity with the same key, attach it to the graph, then do ApplyCurrentValues. But this only works for scalar properties, not for navigational properties.

I found this StackOverflow question which looks good, but i am trying to work out how to get this to work with POCO's/Repository (and ASP.NET MVC - detached context).

As i'm using POCO's, review.Recommendations is an ICollection<Recommendation>, so i can't do review.Recommendations.Attach. I'm not using Self-Tracking Entities either, so i need to manually work with the graph/change tracking - which hasn't been a problem until now.

So you can visualize the scenario:

Review:

  • Recommendations (ICollection<Recommendation>):
    • RecommendationOne (Recommendation)
    • RecommendationTwo (Recommendation)

If im on the edit view, two of the checkboxes are already checked. The third one (representing RecommendationThree) is unchecked.

But if i check that box, the above model becomes:

Review:

  • Recommendations (ICollection<Recommendation>):
    • RecommendationOne (Recommendation)
    • RecommendationTwo (Recommendation)
    • RecommendationThree (Recommendation)

And so i need to attach RecommendationThree to the graph as a new entity.

Do i need hidden fields to compare the posted data the existing entity? Or should i store the entity in TempData and compare that to the posted entity?

EDIT

To avoid confusion, here is the full app stack call:

ReviewController

[HttpPost]
public ActionResult Edit(Review review)
{
   _service.Update(review); // UserContentService
   _unitOfWork.Commit();
}

UserContentService

public void Update<TPost>(TPost post) where TPost : Post, new()
{
   _repository.Update(post); // GenericRepository<Post>
}

GenericRepository - used as GenericRepository<Post>

public void Update<T2>(T2 entity) where T2 : class, new()
{
   // create stub entity based on entity key, attach to graph.

   // override scalar values
   CurrentContext.ApplyCurrentValues(CurrentEntitySet, entity);
}

So, the Update (or Add or Delete) Repository methods needs to be called for each recommendation, depending it's new/modified/deleted.

Community
  • 1
  • 1
RPM1984
  • 69,608
  • 55
  • 212
  • 331

3 Answers3

8

I've accepted @jfar's answer because he put me on the right track, but thought i'd add an answer here for other people's benefit.

The reason the relationships were not getting updated is for the following reasons:

1) Completely disconnected scenario. ASP.NET = stateless, new context newed up each HTTP request.

2) Edited entity created by MVC (model binding), but not existing in graph.

3) When using POCO's with no change tracking, performing .Attach on an entity will add it to the graph, but the entity and any child relationships will be Unchanged.

4) I use the stub entity trick and ApplyCurrentValues to update the entity, but this only works for scalar properties, not navigational ones.

So - in order to get the above to work, i would have to explicity set the EntityState for the object (which happens automatically because of ApplyCurrentValues), and also the navigational properties.

And there is the problem - how do i know if the navigational property was added/modified/deleted? I have no object to compare to - only a entity which i know was "edited", but i don't know what was edited.

So the solution in the end was this:

[HttpPost]
public ActionResult Edit(Review review)
{
   var existingReview = _service.FindById(review.Id); // review is now in graph.
   TryUpdateModel(existingReview); // MVC equivalent of "ApplyCurrentValues" - but works for ALL properties - including navigationals
   _unitOfWork.Commit(); // save changed
}

That's it. I don't even need my _service.Update method - as i don't need the stub trick anymore - because the review is in the graph with the retrieval, and ApplyCurrentValues is replaced by TryUpdateModel.

Now of course - this is not a concurrency-proof solution.

If i load the Review Edit View, and before i click "Submit" someone else changes the Review, my changes could be lost.

Fortunately i have a "last-in-wins" concurrency mode, so it's not an issue for me.

I love POCO's, but man are they a pain when you have the combination of a stateless environment (MVC) and no change tracking.

RPM1984
  • 69,608
  • 55
  • 212
  • 331
  • This can be pretty big security hole when working with automatic model binding because malicious request can also send modified fields which are normally not editable on the form. – Ladislav Mrnka Jan 12 '11 at 11:30
  • 1
    @Ladislav Mrnka - That's why you pass an exclusion list to the `TryUpdateModel` overload. The above is just a simple example. – RPM1984 Jan 12 '11 at 20:54
  • @RPM1984, is there a comprehensive way to reflect a model received from client, along with all its navigation properties, both single and collections to the store - including deletion of entities removed by client? – Shimmy Weitzhandler Apr 19 '15 at 22:22
  • The problem is that the related collection are set as `Added` eventho they already exist! – Shimmy Weitzhandler Apr 20 '15 at 01:57
6

Working with detached object graphs is my favorite drawback of EF. Simply pain in the ass. First you have to deal with it at your own. EF will not help you with it. It means that in addition to Review you also have to send some information about made changes. When you attach Review to context it sets Review all Recommendation and all relations to Unchanged state. ApplyCurrentValues works only for scalar values as you have already found. So you have to use your additional information about made changes and set state of relations to Added by using ObjectContext.ObjectStateManager.ChangeRelationshipState.

I personaly gave up with this approach and I'm loading object graph from DB first merging my changes into attached graph and save it.

I answered similar question more deeply here.

Community
  • 1
  • 1
Ladislav Mrnka
  • 349,807
  • 56
  • 643
  • 654
  • 1
    "I personaly gave up with this approach and I'm loading object graph from DB first merging my changes into attached graph and save it." - that is *exactly* what i have done, lol. – RPM1984 Jan 12 '11 at 03:24
  • After spending a few days on this and finally being stuck on `ChangeRelationshipState` refusing to work with `EntityState.Modified`, I also gave up and enabled Foreign Key properties. Your [blog post](http://www.ladislavmrnka.com/2011/05/foreign-key-vs-independent-associations-in-ef-4/) was an interesting read. – user247702 May 07 '12 at 12:18
  • @Stijn: Relation cannot have modified state. Changing relation means deleting old relation and creating a new relation. – Ladislav Mrnka May 07 '12 at 12:39
  • @LadislavMrnka Yeah, I figured that out, but then I'd first have to retrieve the old relation to be able to delete it. So I found exposing the FKs to be the lesser evil. – user247702 May 07 '12 at 13:55
4

Perhaps I need more context but whats wrong with:

recommendations.Add(newRecomendation)

?

In reply to comment:

Ok so whats wrong with

SomeServiceOrRepository.AddNewRecommendation( newRecommendation )

or

SomeServiceOrRepository.AddNewRecommendation( int parentId, newRecommendation )

Last Sentence? You mean the two questions?

This shouldn't be hard at all.

To summarize my answer I think you are doing things "the hard way" and really should focus on posting form values that correspond to the CRUD action your trying to accomplish.

If a new entity could come in at the same time as your edited entities you should really prefix them differently so the model binder can pick up on it. Even if you have multiple new items you can use the same [0] syntax just prefix the "name" field with New or something.

A lot of times in this scenario you can't rely on Entity Frameworks graph features because removing an entity from a collection never means it should be set for deletion.

If the form is immutable you could also try using the generized attach function off of ObjectSet:

theContect.ObjectSet<Review>().Attach( review )

Tons of ways out of this. Maybe you could post your controller and view code?

John Farrell
  • 24,322
  • 10
  • 73
  • 108
  • that only adds the item to the collection, i need to add that entity to the graph as well. remember - detached context, POCO's, no STE's. in these scenarios, EF doesn't know that this recommendation needs to be added to the DB -i need to tell it so. – RPM1984 Jan 11 '11 at 04:07
  • in reply - edit. and how do i *know* that that recommendation should be added/modified/deleted? do i need to compare all the "old" recommendations to the new ones? look at the last sentence in the question. – RPM1984 Jan 11 '11 at 04:17
  • @jfar - edited question. read your edit - keep in mind im using `ApplyCurrentValues`. `Attach(review)` will only modify scalar values - not the recommendation. – RPM1984 Jan 11 '11 at 04:41
  • @RPM1984 - The docs: "If the object being attached has related objects, those objects are also attached to the object context." Pretty sure I've used attach to do the same thing your trying to do. – John Farrell Jan 11 '11 at 04:54
  • @jfar - can you show the portion of the docs you got that from? I would agree with you in non-POCO scenarios. but i've inspected the OSM. When i attach the review, only the review get's attached, not the recommendations. im guessing it's because recommendations isn't a "related object" (EntityCollection), it's an ICollection. – RPM1984 Jan 11 '11 at 04:58
  • @RPM1984 - http://msdn.microsoft.com/en-us/library/bb896271.aspx - I think some wire is crossed here. The docs also say that ApplyCurrentValues only copies scalars: "Copies the scalar values from the supplied object into the object in the ObjectContext that has the same key. Any values that differ from the original values will be marked as modified." – John Farrell Jan 11 '11 at 04:59
  • 1
    don't worry - found it. and i don't think it works with POCO's/detached context. Check out this statment: "If you are using POCO entities without proxies, you must call the DetectChanges method to synchronize the related objects in the object context. If you are working with disconnected objects you must manually manage the synchronization." Im using disconnected objects (constructed from form values). – RPM1984 Jan 11 '11 at 05:01
  • @RPM1984 - Gotcha, detached can be a real pain. As a rule I always "get" before I set to avoid these kinds of issues. One more select isn't going to kill you. ;) – John Farrell Jan 11 '11 at 05:02
  • even then, say i grab the entity from the context - which has 2 recommendations. now i have a "review" coming in from the post, with 3 recommendations. what do i do? it's the same issue - the core of the problem is *how to know* when to add/edit/delete a relationship. `ApplyCurrentValues` just compares all properties. i need that logic for relationships. arggh. thanks for your help though - i'll keep working and come back here with an edit once i progress. – RPM1984 Jan 11 '11 at 05:09
  • @jfar - accepting your answer since i've just retrieved the object again from the db. because it's in the graph, when you make changes (TryUpdateModel), it will automatically update the relationships - e.g Recommendations. I don't really care about concurrency (last wins), so this will do for now. Thanks. – RPM1984 Jan 12 '11 at 03:25
  • Looks like you both didn't check the link I provided. I guess I found all information in Msdn related to this problem. – Ladislav Mrnka Jan 12 '11 at 05:58
  • 1
    @Ladislav Mrnka - We were working through this late at night before you even posted an answer. So your right, we didn't check the link, it wasn't there! ;) – John Farrell Jan 12 '11 at 13:53