Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Tracking changes in Entity Framework for many-to-many relationships with behavior

Tags:

I'm currently attempting to use Entity Framework's ChangeTracker for auditing purposes. I'm overriding the SaveChanges() method in my DbContext and creating logs for entities that have been added, modified, or deleted. Here is the code for that FWIW:

public override int SaveChanges() {     var validStates = new EntityState[] { EntityState.Added, EntityState.Modified, EntityState.Deleted };     var entities = ChangeTracker.Entries().Where(x => x.Entity is BaseEntity && validStates.Contains(x.State));     var entriesToAudit = new Dictionary<object, EntityState>();     foreach (var entity in entities)     {         entriesToAudit.Add(entity.Entity, entity.State);     }     //Save entries first so the IDs of new records will be populated     var result = base.SaveChanges();     createAuditLogs(entriesToAudit, entityRelationshipsToAudit, changeUserId);     return result; } 

This works great for "normal" entities. For simple many-to-many relationships, however, I had to extend this implementation to include "Independent Associations" as described in this fantastic SO answer which accesses changes via the ObjectContext like so:

private static IEnumerable<EntityRelationship> 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 => new EntityRelationship(                     e.EntitySet.Name,                     objectContext.GetObjectByKey((EntityKey)getValue(e, 0)),                     objectContext.GetObjectByKey((EntityKey)getValue(e, 1)))); } 

Once implemented, this also worked great, but only for many-to-many relationships that use a junction table. By this, I'm referring to a situation where the relationship is not represented by a class/entity, but only a database table with two columns - one for each foreign key.

There are certain many-to-many relationships in my data model, however, where the relationship has "behavior" (properties). In this example, ProgramGroup is the many-to-many relationship which has a Pin property:

public class Program {     public int ProgramId { get; set; }     public List<ProgramGroup> ProgramGroups { get; set; } }  public class Group {     public int GroupId { get; set; }     public IList<ProgramGroup> ProgramGroups { get; set; } }  public class ProgramGroup {     public int ProgramGroupId { get; set; }     public int ProgramId { get; set; }     public int GroupId { get; set; }     public string Pin { get; set; } } 

In this situation, I'm not seeing a change to a ProgramGroup (eg. if the Pin is changed) in either the "normal" DbContext ChangeTracker, nor the ObjectContext relationship method. As I step through the code, though, I can see that the change is in the ObjectContext's StateEntries, but it's entry has IsRelationship=false which, of course, fails the .Where(e => e.IsRelationship) condition.

My question is why is a many-to-many relationship with behavior not appearing in the normal DbContext ChangeTracker since it's represented by an actual class/entity and why is it not marked as a relationship in the ObjectContext StateEntries? Also, what is the best practice for accessing these type of changes?

Thanks in advance.

EDIT: In response to @FrancescCastells's comment that perhaps not explicitly defining a configuration for the ProgramGroup is cause of the problem, I added the following configuration:

public class ProgramGroupConfiguration : EntityTypeConfiguration<ProgramGroup> {     public ProgramGroupConfiguration()     {         ToTable("ProgramGroups");         HasKey(p => p.ProgramGroupId);          Property(p => p.ProgramGroupId).IsRequired();         Property(p => p.ProgramId).IsRequired();         Property(p => p.GroupId).IsRequired();         Property(p => p.Pin).HasMaxLength(50).IsRequired();     } 

And here are my other configurations:

public class ProgramConfiguration : EntityTypeConfiguration<Program> {     public ProgramConfiguration()     {         ToTable("Programs");         HasKey(p => p.ProgramId);         Property(p => p.ProgramId).IsRequired();         HasMany(p => p.ProgramGroups).WithRequired(p => p.Program).HasForeignKey(p => p.ProgramId);     } }  public class GroupConfiguration : EntityTypeConfiguration<Group> {     public GroupConfiguration()     {         ToTable("Groups");         HasKey(p => p.GroupId);         Property(p => p.GroupId).IsRequired();         HasMany(p => p.ProgramGroups).WithRequired(p => p.Group).HasForeignKey(p => p.GroupId);     } 

When these are implemented, EF still does not show the modified ProgramGroup in the ChangeTracker.

like image 573
im1dermike Avatar asked Nov 09 '15 14:11

im1dermike


People also ask

Does Entity Framework have tracking?

Entity Framework provides ability to track the changes made to entities and their relations, so the correct updates are made on the database when the SaveChanges method of context is called. This is a key feature of the Entity Framework.

What does AsNoTracking do in Entity Framework?

The AsNoTracking() extension method returns a new query and the returned entities will not be cached by the context (DbContext or Object Context). This means that the Entity Framework does not perform any additional processing or storage of the entities that are returned by the query.


2 Answers

While the concept of "relationship with attributes" is mentioned in the theory of entity-relationship modelling, as far as Entity Framework is concerned, your ProgramGroup class is an entity. You're probably unwittingly filtering it out with the x.Entity is BaseEntity check in the first code snippet.

like image 198
cynic Avatar answered Oct 22 '22 20:10

cynic


I believe the problem lies in the definition of your Program and Group class and overridden SaveChanges method. With the current definition of the classes the EF is unable to use change tracking proxies, that catch changes as they are being made. Instead of that the EF relies on the snapshot change detection, that is done as part of SaveChanges method. Since you call base.SaveChanges() at the end of the overridden method, the changes are not detected yet when you request them from ChangeTracker.

You have two options - you can either call ChangeTracker.DetectChanges(); at the beginning of the SaveChanges method or change definition of your classes to support change tracking proxies.

public class Program {     public int ProgramId { get; set; }     public virtual ICollection<ProgramGroup> ProgramGroups { get; set; } }  public class Group {     public int GroupId { get; set; }     public virtual ICollection<ProgramGroup> ProgramGroups { get; set; } } 

The basic requirements for creating change tracking proxies are:

  • A class must be declared as public

  • A class must not be sealed

  • A class must not be abstract

  • A class must have a public or protected constructor that does not have parameters.

  • A navigation property that represents the "many" end of a relationship must have public virtual get and set accessors

  • A navigation property that represents the "many" end of a relationship must be defined as ICollection<T>

like image 34
Lukas Kabrt Avatar answered Oct 22 '22 18:10

Lukas Kabrt