Wednesday, February 24, 2010

How to find a dirty property in Entity Framework 4.0

In the new Entity Framwork release, Microsoft finally added support for POCO or Persistance Ignorent (PI) objects. It's now possible to generate Proxies for my POCO's. This has the advantage that the change tracking is automatically done for me.

In my daily work, I often feel the need to find out if a property is dirty, before I let the ObjectContext save my changes. For example, when a customers address property is dirty, I want to create an AddressChange record. In EF4, there's a method ObjectStateEntry.GetModifiedProperties(). This method returns an IEnumerable of strings that contain the propertynames which are changed.

Assume we have the following code:

using (DBTestOrdersContext ctx = new DBTestOrdersContext())
{
ctx.ContextOptions.LazyLoadingEnabled = true;
ctx.ContextOptions.ProxyCreationEnabled = true;

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

I think we can assume that the OrderNumber property is dirty now. So let's write a method that finds a dirty property:


private bool IsDirtyPropertyNotWorking(ObjectContext ctx, object entity, string propertyName)
{
ObjectStateEntry entry;
if (ctx.ObjectStateManager.TryGetObjectStateEntry(entity, out entry))
{
IEnumerable changedNames = entry.GetModifiedProperties();

return changedNames.Any(x => x == propertyName);
}
return false;
}


I expected this method to return true for my OrderNumber property, but unfortunately, nothing is what it seems in EF4. It seems that we first have to call ObjectContext.DetectChanges() before it shows up in the ObjectStateEntry.GetModifiedProperties() list.


private bool IsDirtyPropertyWorking(ObjectContext ctx, object entity, string propertyName)
{
ObjectStateEntry entry;
if (ctx.ObjectStateManager.TryGetObjectStateEntry(entity, out entry))
{
ctx.DetectChanges();

IEnumerable<string> changedNames = entry.GetModifiedProperties();

return changedNames.Any(x => x == propertyName);
}
return false;
}


Microsoft admits that ObjectContext.DetectChanges can be a very expensive operation, because the complete ObjectContext has to be 'scanned' for modifications. Another disadvantage is that changes on all other entities in my Context are also detected, and there are scenarios that I don't want that yet. So I decided to write an IsDirtyProperty method that does not have to call ObjectContext.DetectChanges():


private bool IsDirtyProperty(ObjectContext ctx, object entity, string propertyName)
{
ObjectStateEntry entry;
if (ctx.ObjectStateManager.TryGetObjectStateEntry(entity, out entry))
{
int propIndex = this.GetPropertyIndex(entry, propertyName);

if (propIndex != -1)
{
var oldValue = entry.OriginalValues[propIndex];
var newValue = entry.CurrentValues[propIndex];

return !Equals(oldValue, newValue);
}
else
{
throw new ArgumentException(String.Format("Cannot find original value
for property '{0}' in entity entry '{1}'",
propertyName,
entry.EntitySet.ElementType.FullName));
}
}

return false;
}


private int GetPropertyIndex(ObjectStateEntry entry, string propertyName)
{
OriginalValueRecord record = entry.GetUpdatableOriginalValues();

for (int i = 0; i <> record.FieldCount; i++)
{
FieldMetadata metaData = record.DataRecordInfo.FieldMetadata[i];
if (metaData.FieldType.Name == propertyName)
{
return metaData.Ordinal;
}
}

return -1;
}


Of course you can decide to make these Extension Methods on ObjectContext or ObjectStateEntry

Best regards
Harm Neervens

8 comments:

  1. Thanks for posting. This is just what I needed. :)

    ReplyDelete
  2. THANKS!!!!

    I think I would have lost a lot of time looking for this!

    ReplyDelete
  3. If you're new to property investing, and want some buy to let advice, many people started investing in buy-to-let property because they became disillusioned with investment returns in stocks and shares, mutual funds, unit trust and private pensions.

    ReplyDelete
  4. Want to expand great source of earning in UK? Property Duck provides you realistic way to build a property portfolio by using niche investment strategies.

    ReplyDelete
  5. that's a weird looking 'not equals' for C# code :)

    for (int i = 0; i <> record.FieldCount; i++)

    ReplyDelete
  6. BTW, likely personal preference, but since you're just going to throw for the case of the property index being returned as -1, IMHO you should just go ahead and throw from the GetPropertyIndex (which has all the info necessary for the exception message) so the calling code can stay cleaner :)

    ReplyDelete
  7. Great post! I was looking for something like this and was glad to see someone had already done the work.

    You can change the IsDirtyProperty method to except a lambda instead of a string, to make sure that the property is valid AND if a rename is done, that it gets updated

    public static bool IsPropertyDirty(this ObjectContext ctx, object entity, Expression> expr)
    {
    ObjectStateEntry entry;
    string propertyName = ((MemberExpression)((UnaryExpression)expr.Body).Operand).Member.Name;

    ReplyDelete
  8. Great post.

    However I would change GetPropertyIndex similar to what James Manning wrote...

    private int GetPropertyIndex2(ObjectStateEntry entry, string propertyName)
    {
    try
    {
    return entry.GetUpdatableOriginalValues().GetOrdinal(propertyName);
    }
    catch (Exception)
    {
    throw new ArgumentException("bla bla...");
    }
    }

    ReplyDelete