Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DbEntityEntry.OriginalValues not populating complex properties

I'm writing an audit trail from snippets of code found online. On the call to my SaveChanges function I loop through all the modified entities registered with the Context and build log entries from their changes.

foreach (DbEntityEntry modifiedEntity in this.ChangeTracker.Entries().Where(p => p.State == System.Data.EntityState.Added || p.State == System.Data.EntityState.Deleted || p.State == System.Data.EntityState.Modified))
        {
            // For each changed record, get the audit record entries and add them
            foreach(AuditLog x in GetAuditRecordsForChange(modifiedEntity, userId))
            {
                this.AuditLog.Add(x);
            }
        }

When I then try to access the original values of the modified entity, all the scalar properties are populated but the complex ones don't exist (property count will be say 6 instead of 8). I then call ToObject() to build the object in its original state but obviously the complex properties are all nulls.

modifiedEntity.OriginalValues.ToObject()

This only happens with some of my domain objects, and those objects always show as proxies after the ToObject() call whereas (I'm not sure why) but the ones that don't have proxies created for them by entity, their complex properties populate fine. When I'm using the POCO proxies as normal throughout my application, lazy loading works fine on them.

I've noticed that if I make a change to one of these complex properties that are not populated as part of the OriginalValues data, the object's state doesn't get changed to Modified, this makes sense as change tracking compares the original values to current to see if it's changed. What doesn't make sense is that the data is still persisted on SaveChanged??

EDIT: I've just noticed, the model object that does populate its complex properties, the complex property in question is (by convention) considered a 'complex type' by Entity i.e no primary key.

Any ideas?

like image 240
SeeNoWeevil Avatar asked Aug 11 '11 08:08

SeeNoWeevil


2 Answers

To get all of the member names of an entity and not just the simple properties you can work with ObjectContext rather than DbContext then access the list of members through the EntityType.

((IObjectContextAdapter)this).ObjectContext.ObjectStateManager.GetObjectStateEntry(dbEntry).EntitySet.ElementType.Members

You can then use the method DbEntityEntry.Member(string propertyName) to get a DbMemberEntry.

Gets an object that represents a member of the entity. The runtime type of the returned object will vary depending on what kind of member is asked for. The currently supported member types and their return types are Reference navigation property (DbReferenceEntry), Collection navigation property (DbCollectionEntry), Primitive/scalar property (DbPropertyEntry) and Complex property (DbComplexPropertyEntry).

The code sample below uses this to log modifications of complex properties. Note that there is probably something sexier to be done when logging complex property changes --- I'm currently logging the whole complex property (serialised to JSON) rather than just the inner properties which have changed, but it gets the job done.

private IEnumerable<AuditLogEntry> GetAuditLogEntries(DbEntityEntry dbEntry)
{
    if (dbEntry.State == EntityState.Added)
    {
        return new AuditLogEntry { ... };
    }

    if (dbEntry.State == EntityState.Deleted)
    {
        return new AuditLogEntry { ... };
    }

    if (dbEntry.State == EntityState.Modified)
    {
        // Create one AuditLogEntry per updated field.

        var list = new List<AuditLogEntry>();

        // We need to object state entry to do deeper things.
        ObjectStateEntry objectStateEntry = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager.GetObjectStateEntry(dbEntry);

        // Iterate over the members (i.e. properties (including complex properties), references, collections) of the entity type
        foreach (EdmMember member in ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager.GetObjectStateEntry(dbEntry).EntitySet.ElementType.Members)
        {
            var dbMemberEntry = dbEntry.Member(member.Name) as DbPropertyEntry;
            if (dbMemberEntry == null || Equals(dbMemberEntry.OriginalValue, dbMemberEntry.CurrentValue))
            {
                // Member entry isn't a property entry or it isn't modified.
                continue;
            }

            string oldValue;
            string newValue;

            if (dbMemberEntry is DbComplexPropertyEntry)
            {
                // Bit a bit lazy here and just serialise the complex property to JSON rather than detect which inner properties have changed.
                var complexProperty = (DbComplexPropertyEntry)dbMemberEntry;
                oldValue = EntitySerialiser.Serialise(complexProperty.OriginalValue as IAuditableComplexType);
                newValue = EntitySerialiser.Serialise(complexProperty.CurrentValue as IAuditableComplexType);
            }
            else
            {
                // It's just a plain property, get the old and new values.
                var property = dbMemberEntry;
                oldValue = property.OriginalValue.ToStringOrNull();
                newValue = property.CurrentValue.ToStringOrNull();
            }

                list.Add(new AuditLogEntry
                        {
                            ...,
                            EventType = AuditEventType.Update,
                            ColumnName = member.Name,
                            OriginalValue = oldValue,
                            NewValue = newValue
                        });
        }

        return list;
    }

    // Otherwise empty.
    return Enumerable.Empty<AuditLogEntry>();
}

I'm looking forward to seeing other solutions to this.

like image 199
dan Avatar answered Oct 05 '22 16:10

dan


I believe this article may give you some insight. It is not EF 4.1 but many of the tips and examples apply.

Complex Types and the New Change Tracking API

Its a bit before halfway through the tutorial with the title of the section being the name of the link. Basically to access original values with complex type you add an extra function specifying the complex property.

var original = modifiedEntity.ComplexProperty(u => u.Address).OriginalValues
like image 30
agradl Avatar answered Oct 05 '22 15:10

agradl