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.
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.
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.
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.
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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With