Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Entity Framework 6: detect relationship changes

in my DbContext subclass I have overridden the SaveChanges() method so I can implement a sort of Trigger-like functionality (before the changes are actually saved). Now, in some of these triggers it is necessary to detect whether certain relationships have changed, regardless of many-to-many, one-to-one/zero etc.

I have read a number of posts on the internet, including some on this site, that mention that the DbContext API doesn't expose any means of getting relationship info. However, ObjectContext should be able to.

My SaveChanges method:

public override int SaveChanges()
{
    IEntity entity;
    ChangeTracker.DetectChanges();

    var stateManager = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager;
    var added = stateManager.GetObjectStateEntries(EntityState.Added).ToList();
    var updated = stateManager.GetObjectStateEntries(EntityState.Modified).ToList();
    var deleted = stateManager.GetObjectStateEntries(EntityState.Deleted).ToList();
    var unchanged = stateManager.GetObjectStateEntries(EntityState.Unchanged).ToList();

    while ((entity = _entitiesRequiringTriggering.FirstOrDefault(x => x.Value).Key) != null)
    {
        _entitiesRequiringTriggering[entity] = false;
        var entry = ChangeTracker.Entries<IEntity>().SingleOrDefault(x => x.State != EntityState.Unchanged && x.Entity == entity);
        if (entry == null) continue;
        var trigger = Triggers.Triggers.GetTriggerForEntity(entry.Entity, this);
        if (trigger == null) continue;
        trigger.BeforeSave(entry.Entity);
        switch (entry.State)
        {
            case EntityState.Added:
                trigger.BeforeAdd(entry.Entity);
                break;
            case EntityState.Modified:
                trigger.BeforeUpdate(entry.Entity);
                break;
            case EntityState.Deleted:
                trigger.BeforeDelete(entry.Entity);
                break;
        }
    }
    return base.SaveChanges();
}

Note the four variables added, updated, deleted and unchanged. According to what I've found so far, the GetObjectStateEntries is supposed to return a collection of ObjectStateEntry, which has a property IsRelationship.

I run the following code in a test application:

using (var db = container.Resolve<IDatabaseContext>())
{
    var cus = db.Query<Customer>().Single(x => x.Id == 1);
    var newAddress = db.Query<Address>().Single(x => x.Id == 5);

    cus.Address = newAddress; //also sets the foreign key property Customer.AddressId to its new corresponding value
    db.SaveChanges();
}

When I inspect the code in SaveChanges after that call, I get what was expected: one result in the updated list, the Customer object. But at no point do I ever get an ObjectStateEntry for the relationship (one-to-one) Customer_Address.

I need to be able to detect as previously described when the relationship has changed. For normal scalar properties you would do this:

var changed = DbEntry.Property(x => x.Name).OriginalValue == DbEntry.Property(x => x.Name).CurrentValue;

But for reference properties that doesn't work obviously. Any ideas?

like image 557
Timothy Degryse Avatar asked Dec 20 '13 15:12

Timothy Degryse


1 Answers

You can use this ExtensionMethod to get a list of relationships that changed

public static class DbContextExtensions
{
    public static IEnumerable<Tuple<object, object>> GetRelationships(
        this DbContext context)
    {
        return GetAddedRelationships(context)
                .Union(GetDeletedRelationships(context));
    }

    public static IEnumerable<Tuple<object, object>> GetAddedRelationships(
        this DbContext context)
    {
        return GetRelationships(context, EntityState.Added, (e, i) => e.CurrentValues[i]);
    }

    public static IEnumerable<Tuple<object, object>> GetDeletedRelationships(
        this DbContext context)
    {
        return GetRelationships(context, EntityState.Deleted, (e, i) => e.OriginalValues[i]);
    }

    private static IEnumerable<Tuple<object, object>> GetRelationships(
        this DbContext context,
        EntityState relationshipState,
        Func<ObjectStateEntry, int, object> getValue)
    {
        context.ChangeTracker.DetectChanges();
        var objectContext = ((IObjectContextAdapter)context).ObjectContext;

        return objectContext.ObjectStateManager
                            .GetObjectStateEntries(relationshipState)
                            .Where(e => e.IsRelationship)
                            .Select(
                                    e => Tuple.Create(
                                            objectContext.GetObjectByKey((EntityKey)getValue(e, 0)),
                                            objectContext.GetObjectByKey((EntityKey)getValue(e, 1))));
    }
}
like image 68
Alexandre Rondeau Avatar answered Nov 09 '22 07:11

Alexandre Rondeau