22

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.

Community
  • 1
  • 1
mellis481
  • 4,332
  • 12
  • 71
  • 118
  • Can you include your mapping for the Program graph? If it says IsRelationship=false maybe there's something wrong in the relantionship configuration? – Francesc Castells Nov 12 '15 at 14:15
  • @FrancescCastells I actually don't explicitly define an `EntityTypeConfiguration` for my Program-ProgramGroup-Group relationships. EntityFramework is handling it by convention. – mellis481 Nov 12 '15 at 14:21
  • OK. Unless you are certain that it maps it correctly, I'd suggest mapping it explicitly, just to try it. For example, does it really know about the relationship with Group without a navigation property? – Francesc Castells Nov 12 '15 at 14:27
  • @FrancescCastells I updated my post with configurations. After implementing it, EF still does not have a changed `ProgramGroup` in the ChangeTracker. – mellis481 Nov 12 '15 at 16:40
  • OK. You mention "Independent Associations" and by definition, they are never marked as Modified, but with this mapping Program groups are not independent associations because they specify the foreign key, so that can't be the reason. I'm afraid I don't have an answer. – Francesc Castells Nov 12 '15 at 17:49
  • @FrancescCastells Yeah, I actually am expecting the `ProgramGroup` to appear in the DbContext's ChangeTracker because I've modeled the relationship. – mellis481 Nov 12 '15 at 18:02
  • As far as EF is concerned the `ProgramGroup`s are entities. You may not be seeing them because you're doing the test `x.Entity is BaseEntity` where I guess `BaseEntity` is your class and your `ProgramGroup` does not derive from it. – cynic Nov 14 '15 at 09:20
  • @cynic Wow. I'm totally embarrassed. You're absolutely correct. I had failed to see that I was basically filtering out the `ProgramGroup` changes by using the `x.Entity is BaseEntity` (which is in place for another, separate reason). What should I do about this now-unnecessary question? And getting you the bounty? – mellis481 Nov 16 '15 at 13:55
  • I'm embarrassed for not posting it as an answer in the first place. – cynic Nov 16 '15 at 14:04

3 Answers3

4

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.

cynic
  • 5,305
  • 1
  • 24
  • 40
2

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>

Lukas Kabrt
  • 5,441
  • 4
  • 43
  • 58
1

Entity Framework represents many-to-many relationships by not having entityset for the joining table in CSDL, instead it manages this through mapping.

Note: Entity framework supports many-to-many relationship only when the joining table does NOT include any columns other than PKs of both the tables

you should have to define navigation property yourself to coupe with this proplem.

this link can be of your help.

YOusaFZai
  • 698
  • 5
  • 21