Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET MVC / EF4 / POCO / Repository - How to Update Relationships?

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.

like image 852
RPM1984 Avatar asked Jan 11 '11 03:01

RPM1984


2 Answers

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.

like image 130
RPM1984 Avatar answered Sep 22 '22 10:09

RPM1984


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.

like image 42
Ladislav Mrnka Avatar answered Sep 19 '22 10:09

Ladislav Mrnka