Wednesday, February 24, 2010

How to call SaveChanges for a single entity in Entity Framework 4.0

Last week I installed VS2010 RC and I still wonder why I can't save a single entity in Entity Framework 4.0. I use POCO entities with Proxies, so the change tracking is done automatically. By using automatic change tracking, saving changes means 'commit all' or 'commit nothing'. Saving a single entity is not possible out of the box.
I tried to dive into the Entity Framework source code to figure out how Microsoft does the trick of detecting changes, but source stepping is not possible for .NET 4 assemblies in Visual Studio 2010 RC.

So I decided to install .NET Reflector 6.0 Pro. With this tool you can decompile the .NET 4 assemblies and start stepping through the code.

With some voodoo reflection tricks I managed to save a single entity to the database, without committing the changes that were made on the other dirty entities in the ObjectContext. Most of the types and methods you need are internal in System.Data.Entity, so the only way to do the trick is with help of reflection.

To commit a single entity, we have to disable the DetectChangesBeforeSave option when we call ObjectContext.SaveChanges(). By making the ObjectStateManager detect only the changes of the single entity, SaveChanges will only commit the changes that are detected, so only the single entity will be persisted to the database.



private void button2_Click(object sender, EventArgs e)
{
using (DBTestOrdersContext ctx = new DBTestOrdersContext())
{
ctx.ContextOptions.LazyLoadingEnabled = true;
ctx.ContextOptions.ProxyCreationEnabled = true;

Order order = ctx.GetObjectById<Order>(1);
order.OrderNumber += 1;

Order order2 = ctx.GetObjectById<Order>(2);
order2.OrderNumber += 1;

this.DetectChanges(ctx, order, false);

ctx.SaveChanges(SaveOptions.None);
}
}

private void DetectChanges(ObjectContext ctx, object entity)
{
ObjectStateEntry entry;
if (ctx.ObjectStateManager.TryGetObjectStateEntry(entity, out entry))
{
// We have to call the next PRIVATE method on ObjectStateManager:
// DetectChangesInScalarAndComplexProperties(IList<EntityEntry> entries)

// Because EntityEntry is an internal type, we have
// to instantiate List<EntityEntry> with reflection!
// Covariance of IList<ObjectStateEntry> to IList<EntityEntry> does
// not work at runtime !!!
System.Type listType = typeof(System.Collections.Generic.List<>).MakeGenericType(entry.GetType());
var list = Activator.CreateInstance(listType);
MethodInfo mi = list.GetType().GetMethod("Add", BindingFlags.Public BindingFlags.Instance);
mi.Invoke(list, new object[] { entry });

// Invoke DetectChangesInScalarAndComplexProperties
mi = ctx.ObjectStateManager.GetType().GetMethod("DetectChangesInScalarAndComplexProperties", BindingFlags.NonPublic BindingFlags.Instance);
mi.Invoke(ctx.ObjectStateManager, new object[] { list });
}
}


In the first piece of code I get 2 Order entities from the database. On both entities the OrderNumber property is changed. Calling DetectChanges only on the first order ensures that only the changes of the first order are persisted to the database.

Best regards,
Harm Neervens

No comments:

Post a Comment