Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Entity Framework Multiple Object Contexts

This question has been asked 500 different times in 50 different ways...but here it is again, since I can't seem to find the answer I'm looking for:

I am using EF4 with POCO proxies.

A. I have a graph of objects I fetched from one instance of an ObjectContext. That ObjectContext is disposed.

B. I have an object I fetched from another instance of an ObjectContext. That ObjectContext has also been disposed.

I want to set a related property on a bunch of things from A using the entity in B....something like

foreach(var itemFromA in collectionFromA)
{
   itemFromA.RelatedProperty = itemFromB;
}

When I do that, I get the exception:

System.InvalidOperationException occurred
  Message=The relationship between the two objects cannot be defined because they are attached to different ObjectContext objects.
  Source=System.Data.Entity
  StackTrace:
       at System.Data.Objects.DataClasses.RelatedEnd.Add(IEntityWrapper wrappedTarget, Boolean applyConstraints, Boolean addRelationshipAsUnchanged, Boolean relationshipAlreadyExists, Boolean allowModifyingOtherEndOfRelationship, Boolean forceForeignKeyChanges)
       at System.Data.Objects.DataClasses.RelatedEnd.Add(IEntityWrapper wrappedEntity, Boolean applyConstraints)
       at System.Data.Objects.DataClasses.EntityReference`1.set_ReferenceValue(IEntityWrapper value)
       at System.Data.Objects.DataClasses.EntityReference`1.set_Value(TEntity value)
       at 

I guess I need to detach these entities from the ObjectContexts when they dispose in order for the above to work... The problem is, detaching all entities from my ObjectContext when it disposes seems to destroy the graph. If I do something like:

objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified | EntityState.Unchanged)  
.Select(i => i.Entity).OfType<IEntityWithChangeTracker>().ToList()  
.ForEach(i => objectContext.Detach(i));

All the relations in the graph seem to get unset.

How can I go about solving this problem?

like image 233
Jeff Avatar asked Apr 17 '11 13:04

Jeff


3 Answers

@Danny Varod is right. You should use one ObjectContext for the whole workflow. Moreover because your workflow seems as one logical feature containing multiple windows it should probably also use single presenter. Then you would follow recommended approach: single context per presenter. You can call SaveChanges multiple times so it should not break your logic.

The source of this issue is well known problem with deficiency of dynamic proxies generated on top of POCO entities combined with Fixup methods generated by POCO T4 template. These proxies still hold reference to the context when you dispose it. Because of that they think that they are still attached to the context and they can't be attached to another context. The only way how to force them to release the reference to the context is manual detaching. In the same time once you detach an entity from the context it is removed from related attached entities because you can't have mix of attached and detached entities in the same graph.

The issue actually not occures in the code you call:

itemFromA.RelatedProperty = itemFromB;

but in the reverse operation triggered by Fixup method:

itemFromB.RelatedAs.Add(itemFromA);

I think the ways to solve this are:

  • Don't do this and use single context for whole unit of work - that is the supposed usage.
  • Remove reverse navigation property so that Fixup method doesn't trigger that code.
  • Don't use POCO T4 template with Fixup methods or modify T4 template to not generate them.
  • Turn off lazy loading and proxy creation for these operations. That will remove dynamic proxies from your POCOs and because of that they will be independent on the context.

To turn off proxy creation and lazy loading use:

var context = new MyContext();
context.ContextOptions.ProxyCreationEnabled = false;

You can actually try to write custom method to detach the whole object graph but as you said it was asked 500 times and I haven't seen working solution yet - except the serialization and deserialization to the new object graph.

like image 95
Ladislav Mrnka Avatar answered Sep 27 '22 19:09

Ladislav Mrnka


I think you have a few different options here, 2 of them are:

  1. Leave context alive until you are done with the process, use only 1 context, not 2.

  2. a. Before disposing of context #1, creating a deep clone of graph, using BinaryStreamer or a tool such as ValueInjecter or AutoMapper.

    b. Merge changes from context #2 into cloned graph.

    c. Upon saving, merge changes from cloned graph into graph created by new ObjectContext.


For future reference, this MSDN blogs link can help decide you decide what to do when: http://blogs.msdn.com/b/dsimmons/archive/2008/02/17/context-lifetimes-dispose-or-reuse.aspx

like image 36
Danny Varod Avatar answered Sep 27 '22 18:09

Danny Varod


I don't think you need to detach to solve the problem.

We do something like this:

public IList<Contact> GetContacts()
{
  using(myContext mc = new mc())
  {
    return mc.Contacts.Where(c => c.City = "New York").ToList();
  }
}

public IList<Sale> GetSales()
{ 
  using(myContext mc = new mc())
  {
    return mc.Sales.Where(c => c.City = "New York").ToList();
  }  
}

public void SaveContact(Contact contact)
{
    using (myContext mc = new myContext())
    {
       mc.Attach(contact);
       contact.State = EntityState.Modified;
       mc.SaveChanges();
    }
}

public void Link()
{
   var contacts = GetContacts();
   var sales = GetSales();

   foreach(var c in contacts)
   {
       c.AddSales(sales.Where(s => s.Seller == c.Name));
       SaveContact(c);
   }
}

This allows us to pull the data, pass it to another layer, let them do whatever they need to do, and then pass it back and we update or delete it. We do all of this with a separate context (one per method) (one per request).

The important thing to remember is, if you're using IEnumerables, they are deferred execution. Meaning they don't actually pull the information until you do a count or iterate over them. So if you want to use it outside your context you have to do a ToList() so that it gets iterated over and a list is created. Then you can work with that list.

EDIT Updated to be more clear, thanks to @Nick's input.

like image 40
taylonr Avatar answered Sep 27 '22 17:09

taylonr