Change tracking for many-to-many connections in Entity Framework 4.1+

change-tracking entity-framework many-to-many

Question

How can I find out whether the attributes of an ICollection (many-to-many relationships) have changed?

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
    }
}
1
29
4/16/2015 10:52:21 PM

Accepted Answer

How to locate all the modified many-to-many connections is shown below. The code has been put into practice 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 justification In EF, Independent Associations, or IAs, are used to express many-to-many connections. This is due to the object model's lack of any exposed foreign keys for the relationship. The FKs are stored in a join table in the database, which is concealed from the object model.

"Relationship entries" are used in EF to keep track of IAs. These resemble the DbEntityEntry objects that the DbContext provides. Entry, but rather than representing an entity themselves, they depict a connection between two entities. In order to retrieve relationship entries, you must descend to ObjectContext from the DbContext API.

When a new connection between two entities is established, such as when an Employee is added to the Company, a new relationship entry is produced. Workers' collection. The condition of this connection is added.

The relationship entry is also transferred into the Deleted state when a relationship between two entities is terminated.

This implies that we must search for both new and deleted relationship entries in order to detect modified many-to-many relationships (or indeed any changed IA). The GetAddedRelationships and GetDeletedRelationships functions do this.

We must interpret relationship entries once we get them. You need to be aware of a piece of inside information for this. The EntityKey objects of the entities at each end of the connection are the two values in the CurrentValues property of an Added (or Unchanged) relationship entry. The EntityKey objects for the entities at either end of the deleted relationship are included in the OriginalValues attribute of a Deleted relationship item, which is frustratingly slightly different.

(This is very terrible. Please don't hold that against me; it predates my time.)

We send a delegate into the GetRelationships private function since there is a discrepancy between the CurrentValues and OriginalValues.

GetObjectByKey may be used to get the real entity instances after we have the EntityKey objects. These are what we return as tuples, so there you go.

I tested this using the entities, context, and initializer shown below. (Note that the testing was not thorough.)

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 is an example of how to use 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();
} 
85
3/23/2012 2:02:15 AM

Popular Answer

I am unable to provide you with the specific code for your issue, however I can assure you that by placing a joiner table between the company and the employees in order to break up the many-to-many link, your problem will be 10 times simpler.



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow