2

I've seen all the questions out there for tracking changes to an entity, but none of those solutions included navigation properties. Is this even possible? I don't need some SUPER generic solution that just logs everything. I only want things tracked for specific "main" entities. And the audit log is going to be a human readable custom one, so not just some table that has oldData newData like I have seen in the examples on here.

I've gotten it almost to a good point except tracking the navigation properties, I have an Interface on the POCO's service that determines whether I want to track changes, then in my entityService.Save method I have this check:

var trackChangesService = this as ITrackChangesService<TClass>;
if (trackChangesService != null)
{
      TClass oldEntity = Repository.GetOldEntity(entity);
      trackChangesService.SaveChanges(entity, oldEntity);
}

The save changes method is what the ITrackChanges interface exposes and will have custom logic on a per entity basis (this includes email notifications, etc.), but the problem is I can't propertly get the oldEntity. Currently the method looks like this:

public TClass GetOldEntity(TClass entity)
    {
        //DbEntityEntry<TClass> oldEntry = Context.Entry(entity);
        ////oldEntry.CurrentValues.SetValues(oldEntry.OriginalValues);
        //var values = oldEntry.OriginalValues;
        //return (TClass)oldEntry.GetDatabaseValues().ToObject();

        return Context.Set<TClass>().AsNoTracking().FirstOrDefault(x => x.Id == entity.Id);
    }

You can see the code I have been playing with. I can get the regular properties old and new values fine, but the navigation properties do not carry over. But I need this information, say for instance I am editing a user and on the edit user screen, groups can be removed and added. When the save button is clicked, it performs those updates.

So my update view look like this:

private UserSaveModel Update(Guid id, UserSaveModel userSaveModel)
    {
        User user = _userService.GetOne(x => x.Id == id);
        user = _userService.ConvertFromSaveModel(userSaveModel, user);

        _userService.Save(user);

        return userSaveModel;
    }

I guess one way to look at the problem is, I am adjusting the values on the entity on the context in .ConvertFromSaveeModel() on the existing user record, including the navigation properties, so once I call save, that old navigation property data is gone.

Is there anyway to get original navigation property information for an entity using this workflow? If not, how can I change the workflow to make this possible? If I don't query the user object at the beginning and instead make one from scratch, some data that may not be exposed in the viewModel won't pass over, so I am not sure that will work. Any other suggestions? Maybe clone the entity and use that as original before I made changes? Does that carry over navigation properties?

SventoryMang
  • 10,275
  • 15
  • 70
  • 113
  • There are many examples that use an override of `DbContext.SaveChanges`. That's much easier for logging changes to object graphs than your per-entity approach. – Gert Arnold Nov 18 '15 at 10:52
  • @GertArnold Do you have a link? The only ones I've seen still consist of using DbEntry.CurrentValues /OrignialValues, using .AsNoTracking, or specifically exclude navigation properties such as this one:http://stackoverflow.com/questions/6156818/entity-framework-4-1-dbcontext-override-savechanges-to-audit-property-change – SventoryMang Nov 18 '15 at 21:18
  • Take a look at [Audit.EntityFramework](https://github.com/thepirat000/Audit.NET/tree/master/src/Audit.EntityFramework#auditentityframework) library. It can be [configured](https://github.com/thepirat000/Audit.NET/tree/master/src/Audit.EntityFramework#configuration) to store the entire entity object graphs. – thepirat000 Sep 09 '16 at 05:30
  • @thepirat000 Very interesting, I see you are the creator of that. Do you have a sample project using this? I always find them to be super helpful. I didn't see anything in the documentation about storing entire object graphcs though, or is that basically in a transaction storing column changes across many entities? Your library seems to be more from a DbContext approach. I would like to be able to use this to be able to show change history on an entity to users, and maybe allow an entity to be restored to a previous version. Is this possible with your lib? Of course the question becomes... – SventoryMang Sep 09 '16 at 21:43
  • How do you track changed associated with one entity? If I have a user with many departments, and add and remove one department. Where does this change get counted as? technically it would be on the UserDepartment linking table, but there's no such entity to view from the user perspective in the UI. But really you could look at it as a change on both the User and Department entities imagining looking at an audit log for each of those entities. I guess this is something I'd have to program on a per entity basis, but could I get all the relevant information from your audit logs? – SventoryMang Sep 09 '16 at 21:50

1 Answers1

0

Unfortunately, there is no easy way to detect Navigation Properties changes on EF SaveChanges, specially for the EF 7 (Core) version. Check this and this MSDN article.

So, if you don't have a relationship entity, and your entities are related like this:

public class User
{
    [Key]
    public int UserId { get; set; }
    public ICollection<Department> Departments { get; set; }
}

public class Department
{
    [Key]
    public int DepartmentId { get; set; }
    public ICollection<User> Users { get; set; }
}

and you only change a Navigation Property, say for example you add a Department to a User, like this:

using (var ctx = new MyContext())
{
    var user1 = ctx.Users.Include(u => u.Departments).First(u => u.Id == 1);
    var dept3 = ctx.Departments.First(d => d.Id == 3);

    user1.Departments.Add(dept3);

    ctx.SaveChanges();
}

The library Audit.EntityFramework will not detect the change, since you did a Relationship change, but not an Entity change.

You can avoid this problem creating an entity for your relation, i.e. a UserDepartment class. So you will see the changes to the relation as changes to that entity.

So far, this is the only way I found to obtain the relationship changes in case you don't have the relation entity. But... Only works for EF 6 so I didn't included that on my library.

Another workaround is to make sure to change another property value of the entity (i.e. a LastUpdatedDate column), or to explicitly mark the entity as Modified before the SaveChanges, so the library can detect the change and, at least, you'll see the entity current navigation properties.

For example:

using (var ctx = new MyContext())
{
    var user1 = ctx.Users.Include(u => u.Departments).First(u => u.Id == 1);
    var dept3 = ctx.Departments.First(d => d.Id == 3);
    user1.Departments.Add(dept3);
    ctx.Entry(user1).State = EntityState.Modified; // <-- Here
    ctx.SaveChanges();
}

Now the library will detect the change, and it will include some data about the change.

This is how the output (as JSON) would be like:

{
    "EventType": "MyContext",
    //...
    "EntityFrameworkEvent": {
        "Database": "Users",
        "Entries": [{
            "Table": "User",
            "Action": "Update",
            "PrimaryKey": {
                "UserId": 1
            },
            "Entity": {
                "UserId": 1,
                "Username": "federico",
                "Departments": [{
                    "DepartmentId": 3,
                    "DepartmentName": "Dept3",
                    "Users": []
                }]
            },
            "Changes": [],
            "ColumnValues": {
                "UserId": 1,
                "Username": "federico"
            },
            "Valid": true,
            "ValidationResults": []
        }],
        "Result": 1,
        "Success": true
    }
}

You need your context to inherits from AuditDbContext and set the IncludeEntityObjects to true:

[AuditDbContext(IncludeEntityObjects = true)]
public class MyContext : AuditDbContext
{
    public DbSet<User> Users { get; set; }
    public DbSet<Department> Departments { get; set; }
}

I know this is not a solution for your problem, but it could probably help.

Community
  • 1
  • 1
thepirat000
  • 12,362
  • 4
  • 46
  • 72
  • Thanks. I do have a lastUpdated column on all my entites. I also do have the many to many linking table exposed in the context (EF core right now forces you to do this). But it looks like your audit thing would register the change to the UserDepartment table. So there isn't really an automated generic way I could get all changes associated with a user is there? Or could I query all chnages where column values contains UserId = or Table = User and column values contains Id = ? Would that work? – SventoryMang Sep 10 '16 at 23:39
  • If you're using EF7 (so you're forced to have a relation entity), you shouldn't have problems. I think your problem is more in your queries to the audit logs. I suggest you to store the events in a MongoDB database, so you will be able to [query the data](https://docs.mongodb.com/manual/tutorial/query-documents/) as you want. There is an [Audit.MongoDB](https://github.com/thepirat000/Audit.NET/tree/master/src/Audit.NET.MongoDB#auditnetmongodb) extension for Audit.NET that you can use to store on MongoDB. – thepirat000 Sep 11 '16 at 21:37
  • Also you can group your changes by using the EventType string, or by setting a [custom field](https://github.com/thepirat000/Audit.NET/tree/master/src/Audit.EntityFramework#customization) in code, i.e. by having a _tags_ string list where you can set the entities affected, so for example for UserDepartment relationship change: `context.AddAuditCustomField("Tags", new [] {"User", "Department"});` so you can then use those to filter your queries. – thepirat000 Sep 11 '16 at 21:53
  • Oh goodness, tags sounds like it would work great. Presumably then I could add an update specific unique tag that groups all child and main entity changes under one tag, such that querying them in the future would be a snap right? – SventoryMang Sep 12 '16 at 17:13
  • That's right, you can store whatever you want along with the audit event. Your imagination is the limit (and your DB query capabilities) – thepirat000 Sep 12 '16 at 17:36