次の方法で共有


Creating an original value object from the separate values stored in the ObjectStateManager

Update October 21, 2009: The EF has progressed quite a bit since this original post, but the need for this capability still comes up. Recently someone asked me how to do this kind of thing, and when I pointed them at this post, they had some trouble because their entities were POCO classes that did not implement IEntityWithKey. While updating the code below to deal with that (pretty simple really—just need to look up the ObjectStateEntry using the object reference rather than the EntityKey and then get the EntitySet name from the EntityKey on the state entry rather than off the entity itself), I also thought that it would be a lot nicer for this to be a generic method rather than taking and returning data that's just typed “object”. Especially given the compiler’s ability to infer types, this makes things pretty nice. So the code below has been updated for these capabilities.

Update December 3, 2007: While thinking about further uses for this routine, I decided to make a few tweaks to its semantics from what I originally published here. First off, it seemed useful for unchanged entities to return a copy of the object rather than just directly returning the source, because I ultimately want to build an original values graph (and I don't want the current values and the original values graphs to be connected to each other). Secondly, I wanted to set the EntityKey on the original objects if they implement IEntityWithKey. The code below has been updated to reflect these changes.

While thinking about remoting object graphs and the like, I decided to build up a few useful routines to facilitate my experiments. And of course once I put something like this together, I figure maybe someone else would be interested…

The first of these is a method which will construct an object corresponding to the original values of an attached entity. When an entity is attached, the ObjectStateManager will track all changes made to the object, but it optimizes the memory used by maintaining the original values of only those properties which have been changed, so if you want a whole object which has the properties of the original, you need to construct it yourself.

The ObjectStateEntry has a few important features which make this process easier:

· It exposes a DbDataRecord of both the current and original values that gives you a nice, late-bound interface to the data.

· The DbDataRecord contains only those properties which are defined in the conceptual model which is an advantage for our purposes since it will skip properties and fields that reflection would show but aren’t needed for persistence.

· The current values for the state entry are stored in the object itself, and the current values record is just a façade over the object with dynamic methods which make reads and writes for those properties much faster than reflection (think orders of magnitude faster).

So, without further ado, here’s the routine. Just for fun, I made it an extension method on the object context.

 public static T CreateOriginalValuesObject<T>(this ObjectContext context, T source) 
    where T : class, new()
{
    // Get the state entry for the source -- just lookup by object reference.
    var sourceStateEntry = context.ObjectStateManager.GetObjectStateEntry(source);

    // Can't get original values for added or detached entities.
    switch (sourceStateEntry.State)
    {
        case EntityState.Added:
            return null;

        case EntityState.Detached:
            throw new InvalidOperationException("Can’t get original values when detached.");
    }

    // Create target object and add it to the context.  Use EntityKey from the source 
    // state entry to find the entity set name.
    T target = Activator.CreateInstance<T>();
    string fullEntitySetName = sourceStateEntry.EntityKey.EntityContainerName + 
        "." + sourceStateEntry.EntityKey.EntitySetName;
    context.AddObject(fullEntitySetName, target);
    var targetStateEntry = context.ObjectStateManager.GetObjectStateEntry(target);

    // Copy values.  NOTE: This only copies the properties that would be saved to the 
    // database not other non-persisted properties that may exist on the source object.
    for (int i = 0; i < sourceStateEntry.OriginalValues.FieldCount; i++)
    {
        targetStateEntry.CurrentValues.SetValue(i, sourceStateEntry.OriginalValues[i]);
    }

    // Detach target -- it was only temporarily attached while copying values.
    context.Detach(target);

    // If the source implements IEntityWithKey, copy the EntityKey to the target to 
    // make attaching and things a little easier
    var sourceWithKey = source as IEntityWithKey;
    if (sourceWithKey != null)
    {
        ((IEntityWithKey)target).EntityKey = sourceWithKey.EntityKey;
    }

    return target;
}

Code for using it would look something like this:

 using var ctx = new NorthwindEntities()) 
{ 
    var customer = ctx.Customers.First(); 
    customer.ContactName = "Danny";

    var originalCustomer= ctx.CreateOriginalValuesObject(customer);
    Console.WriteLine("Current: {0} - Original: {1}", 
                      customer.ContactName, originalCustomer.ContactName);
} 

Next on the agenda: Creating not just an original values object but an entire graph. That gets a bit more complicated… We’ll leave it for another day.

- Danny

Comments