31

How can I detect changes of ICollection<> properties (many-to-many relationships)?

public class Company
{
    ...

    public virtual ICollection<Employee> Employees { get; set; }
}

using (DataContext context = new DataContext(Properties.Settings.Default.ConnectionString))
{
    Company company = context.Companies.First();
    company.Employees.Add(context.Employees.First());

    context.SaveChanges();
}

public class DataContext : DbContext
{
    public override int SaveChanges()
    {
        return base.SaveChanges();

        // Company's entity state is "Unchanged" in this.ChangeTracker
    }
}
royhowie
  • 11,075
  • 14
  • 50
  • 67
Boris Mitchenko
  • 880
  • 9
  • 18
  • Good question actually. The `ObjectStateManager` returns more entries than the `ChangeTracker` of DbContext, especially also the entries of type "Relationship". For instance: `((IObjectContextAdapter)DbContext).ObjectContext.ObjectStateManager .GetObjectStateEntries(EntityState.Added)` returns one `ObjectStateEntry` which represents the added relationship in your example. But I can't figure out how to proceed from here, the interesting data in this entry (parent and added child entity) are visible in the debugger but all private/internal... – Slauma Sep 20 '11 at 22:36
  • I'm not sure what you mean by "detect changes of ICollection<> properties". If you mean see what changes are going to be made inside SaveChanges, then I can answer that but it's going to be complicated because many-to-many relationships are always independent associations, as hinted at by @Slauma. If you mean, how can you make EF detect these changes in the sense of DetectChanges, then that should already be happening in the code. The state of the company entity hasn't changed because, in accordance with idependent associations, it hasn't changed, just the relationship has. – Arthur Vickers Mar 18 '12 at 18:30
  • Thank you Arthur! I think the question is how to get the added (or deleted) record in the hidden join-table for many-to-many relationship in SaveChanges. I know that is complicated, but my project need it. For example, in my application, user can follow other users, when someone follows (or cancels) someone else, I need to log the event in database.and there are lots of many-to-many relationship, I need to find a clear solution for logging all these relationships. Could you please give me a little code demo for this?Thank you! – Chance Mar 19 '12 at 01:03

2 Answers2

92

Here is how to find all the changed many-to-many relationships. I've implemented the code as extension methods:

public static class IaExtensions
{
    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))));
    }
}

Some explanation. Many-to-many relationships are represented in EF as Independent Associations, or IAs. This is because the foreign keys for the relationship are not exposed anywhere in the object model. In the database the FKs are in a join table, and this join table is hidden from the object model.

IAs are tracked in EF using "relationship entries". These are similar to the DbEntityEntry objects you get from the DbContext.Entry except that they represent a relationship between two entities rather than an entity itself. Relationship entries are not exposed in the DbContext API, so you need to drop down to ObjectContext to access them.

A new relationship entry is created when a new relationship between two entities is created, for example by adding an Employee to the Company.Employees collection. This relationship is in the Added state.

Likewise, when a relationship between two entities is removed, then the relationship entry is put into the Deleted state.

This means that to find changed many-to-many relationships (or actually any changed IA) we need to find added and deleted relationship entries. This is what the GetAddedRelationships and GetDeletedRelationships do.

Once we have relationship entries, we need to make sense of them. For this you need to know a piece of insider knowledge. The CurrentValues property of an Added (or Unchanged) relationship entry contains two values which are the EntityKey objects of the entities at either end of the relationship. Likewise, but annoyingly slightly different, the OriginalValues property of a Deleted relationship entry contains the EntityKey objects for the entities at either end of the deleted relationship.

(And, yes, this is horrible. Please don’t blame me—it is from well before my time.)

The CurrentValues/OriginalValues difference is why we pass a delegate into the GetRelationships private method.

Once we have the EntityKey objects we can use GetObjectByKey to get the actual entity instances. We return these as tuples and there you have it.

Here’s some entities, a context, and an initializer, I used to test this. (Note—testing was not extensive.)

public class Company
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Employee> Employees { get; set; }

    public override string ToString()
    {
        return "Company " + Name;
    }
}

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Company> Companies { get; set; }

    public override string ToString()
    {
        return "Employee " + Name;
    }
}

public class DataContext : DbContext
{
    static DataContext()
    {
        Database.SetInitializer(new DataContextInitializer());
    }

    public DbSet<Company> Companies { get; set; }
    public DbSet<Employee> Employees { get; set; }

    public override int SaveChanges()
    {
        foreach (var relationship in this.GetAddedRelationships())
        {
            Console.WriteLine(
                "Relationship added between {0} and {1}",
                relationship.Item1,
                relationship.Item2);
        }

        foreach (var relationship in this.GetDeletedRelationships())
        {
            Console.WriteLine(
                "Relationship removed between {0} and {1}",
                relationship.Item1,
                relationship.Item2);
        }

        return base.SaveChanges();
    }

}

public class DataContextInitializer : DropCreateDatabaseAlways<DataContext>
{
    protected override void Seed(DataContext context)
    {
        var newMonics = new Company { Name = "NewMonics", Employees = new List<Employee>() };
        var microsoft = new Company { Name = "Microsoft", Employees = new List<Employee>() };

        var jim = new Employee { Name = "Jim" };
        var arthur = new Employee { Name = "Arthur" };
        var rowan = new Employee { Name = "Rowan" };

        newMonics.Employees.Add(jim);
        newMonics.Employees.Add(arthur);
        microsoft.Employees.Add(arthur);
        microsoft.Employees.Add(rowan);

        context.Companies.Add(newMonics);
        context.Companies.Add(microsoft);
    }
}

Here’s an example of using it:

using (var context = new DataContext())
{
    var microsoft = context.Companies.Single(c => c.Name == "Microsoft");
    microsoft.Employees.Add(context.Employees.Single(e => e.Name == "Jim"));

    var newMonics = context.Companies.Single(c => c.Name == "NewMonics");
    newMonics.Employees.Remove(context.Employees.Single(e => e.Name == "Arthur"));

    context.SaveChanges();
} 
Arthur Vickers
  • 7,503
  • 32
  • 26
  • 4
    This answer is great Arthur! The best out there regarding this tricky subject. I wish I could give you more than one upvote! – Hannish Jan 30 '13 at 09:26
  • Thanx! You saved me a bunch of time) – white.zaz May 08 '13 at 06:56
  • Thanks @arthur. It works very well. Just a question though: What does the parameter Func do? Does it allow the function to call this function on the Context? I'm a bit confused about this parameter. Thanks a lot! – Rafael Merlin Nov 12 '14 at 17:26
  • Consider changing DbContext to ObjectContext. It would be helpful for EF4.0 developers as well. – Davut Gürbüz Jan 11 '15 at 19:23
  • Great! Is there anyway to reject these changes in context? I use Local Collections of DbSets for binding in WPF, but i need to reject many to many relationships changes... – Hesam Kashefi Nov 30 '17 at 06:43
  • @Arthur Vickers That is really cool. But how do you use a Tuple with 2 objects? What can be done with objects? – onefootswill May 31 '18 at 03:47
  • It's a great answer, but not with large contexts, this will consume a lot of memory!!! We tried it on production. And our Context is huge, unfortunately! Executing `((IObjectContextAdapter)context).ObjectContext` throw OutOfMemoryException! – bunjeeb Oct 19 '20 at 13:08
4

I cant give you the exact code for your situation, but I can tell you your situation will be simplified ten fold by having a joiner table inbetween Employees and Company just to break up the many to many relationship.

Isuru Fonseka
  • 601
  • 4
  • 15