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 th开发者_运维知识库e 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?
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.
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
More digging, it seems EF change tracking doesn't store any sort of original values for reference or collection type properties on modified entities (someone please correct me if I'm wrong)
I can find out for example that my Vehicle entity had its reference to one VehicleColour object removed and then re-added pointing to a different instance of a VehicleColour. I can't find out for example that it did point to a VehicleColour with a Name "Stardust Silver" and now points to one with "Azure Blue".
精彩评论