2

I found this answer which (unless I'm missing something) gives a good solution to catch all changes made to all independent associations in a DbContext. I am trying to adapt that solution to something more specific, to create a function that can find the changes for a particular navigation property on a particular entity. The goal is for the user of this function to not have to worry about any of the internals of EntityFramework, and to only be concerned with his POCO models.

This is what I have so far, and it works. But to my eye it looks very messy and prone to break if internal implementation details of Entity Framework change--particularly there's a "magic string" "ClrPropertyInfo" in there that may not be intended as part of the EF interface contract.

public partial class EntityFrameworkMaterializer
{
    public IEnumerable<T2> GetAssociationChanges<T1,T2>(T1 parent, string propertyName, EntityState findState)
    {
        // this.context is a DbContext instance that is a property of the object.
        ObjectContext ocontext = ((IObjectContextAdapter)this.context).ObjectContext;
        MetadataWorkspace metadataWorkspace = ocontext.MetadataWorkspace;

        // Find the AssociationType that matches the property traits given as input            
        AssociationType atype =
            metadataWorkspace.GetItems<AssociationType>(DataSpace.CSpace)
            .Where(a => a.AssociationEndMembers.Any(
                ae => ae.MetadataProperties.Any(mp => mp.Name == "ClrPropertyInfo" // Magic string!!!
                    && ((PropertyInfo)mp.Value).Name == propertyName
                    && typeof(T1).IsAssignableFrom(((PropertyInfo)mp.Value).DeclaringType)
                    )
                    )
                    ).First();

        // Find added or deleted DbDataRecords from the above discovered type
        ocontext.DetectChanges();
        IEnumerable<DbDataRecord> dbDataRecords = ocontext.ObjectStateManager
            .GetObjectStateEntries(findState)
            .Where(e => e.IsRelationship)
            // Oddly, this works, while doing the same thing below requires comparing .Name...?
            .Where(e => e.EntitySet.ElementType == atype)
            .Select(e => findState == EntityState.Deleted ? e.OriginalValues : e.CurrentValues);

        // Get the actual entities using the EntityKeys in the DbDataRecord
        IList<T2> relationChanges = new List<T2>();
        foreach (System.Data.Common.DbDataRecord ddr in dbDataRecords)
        {
            EntityKey ek = (EntityKey)ddr[0];
            // Comparing .ElementType to atype doesn't work, see above...?
            if (!(ek.GetEntitySet(metadataWorkspace).ElementType.Name == atype.Name))
            {
                ek = (EntityKey)ddr[1];
            }
            relationChanges.Add((T2)ocontext.GetObjectByKey(ek));
        }
        return relationChanges;
    }
}

Example of usage:

// Student <--> Course is many-to-many Independent Association
IEnumerable<Course> droppedCourses;
droppedCourses = GetAssociationChanges<Student, Course>(
    student,
    "EnrolledCourses",
    EntityState.Deleted
);

This was the result of a LOT of reverse-engineering and digging around through objects in the debugger. Besides the magic string problem, I wonder if there is a more direct path to figuring out the type of the "relationship entries" representing the IAs based on the "end" entity type and the navigation property name.

Community
  • 1
  • 1
S'pht'Kr
  • 2,809
  • 1
  • 24
  • 43
  • I don't know why you're venturing into this area, but if it is for saving disconnected entities maybe you could look at GraphDiff. It may be worth looking at that code anyway. – Gert Arnold Aug 20 '14 at 19:45
  • @GertArnold Very interesting, thanks…not quite what I'm doing, but similar--I will keep GraphDiff in mind. In my case I am reattaching and reconciling a deserialized object graph, yes. But this code is used between reconciling those changes into the context and actually calling `.SaveChanges()`--since EF already has to do all the change tracking, this method captures the changes that have been made before committing. In my current use case, I use that to record messages about the changes that were made, similar to an audit log, which is where I've seen this kind of thing done elsewhere. – S'pht'Kr Aug 21 '14 at 12:30

0 Answers0