6

I am porting an old project over to ASP.NET 5 and Entity Framework 7. I have used the database first approach (DNX scaffold) to create the model.

The old project is based on Entity Framework 4 and audit tracking is implemented by overriding the SaveChanges method of the DbContext:

public override int SaveChanges(System.Data.Objects.SaveOptions options)
{
    int? UserId = null;
    if (System.Web.HttpContext.Current != null) 
        UserId = (from user in Users.Where(u => u.UserName == System.Web.HttpContext.Current.User.Identity.Name) select user.Id).SingleOrDefault();

    foreach (ObjectStateEntry entry in ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Modified))
    {
        Type EntityType = entry.Entity.GetType();

        PropertyInfo pCreated = EntityType.GetProperty("Created");
        PropertyInfo pCreatedById = EntityType.GetProperty("CreatedById");
        PropertyInfo pModified = EntityType.GetProperty("Modified");
        PropertyInfo pModifiedById = EntityType.GetProperty("ModifiedById");

        if (entry.State == EntityState.Added)
        {
            if (pCreated != null)
                pCreated.SetValue(entry.Entity, DateTime.Now, new object[0]);
            if (pCreatedById != null && UserId != null)
                pCreatedById.SetValue(entry.Entity, UserId, new object[0]);
        }
        if (pModified != null)
            pModified.SetValue(entry.Entity, DateTime.Now, new object[0]);
        if (pModifiedById != null && UserId != null)
            pModifiedById.SetValue(entry.Entity, UserId, new object[0]);
        }
    }

    return base.SaveChanges(options);
}

My question is, how can I implement this in Entity Framework 7? Do I have to take the code first approach?

Stian Sandve
  • 520
  • 4
  • 13
  • EF7 is far from finished and not production-ready at all. I'd go for EF6 and DbContext API first. There are still many items on the EF7 backlog that must be implemented before it's mature enough. – Gert Arnold Jan 03 '16 at 19:47
  • My understanding is that you could implement it the exact same way... or well basically the same. – Seabizkit Feb 25 '16 at 11:24
  • Personally you should pass the user id in to a method, rather than referencing System.Web in a data related layer. – Seabizkit Feb 25 '16 at 11:27
  • You can use the ChangeTracker to set the values of the tracking properties. However, you can use read/write properties, read only properties or shadow properties (new in EF7). The answer already shows 2 ways. This post explains the 3 ways: https://www.meziantou.net/2017/07/03/entity-framework-core-generate-tracking-columns – meziantou Aug 27 '17 at 20:28

2 Answers2

6

Basically you have two ways to achieve this:

Using ChangeTracker API (EF 6+):

This is the way we currently do it in EF 6 and it is still valid and working for EF 7:

First you have to make sure your entities are implementing a common interface for audit fields:

public interface IAuditableEntity 
{
    int? CreatedById { get; set; }

    DateTime Created { get; set; }

    int? ModifiedById { get; set; }

    DateTime Modified { get; set; }
}


Then you can override SaveChanges and update each common field with audit values:

public override int SaveChanges()
{
    int? userId = null;
    if (System.Web.HttpContext.Current != null)
        userId = (from user in Users.Where(u => u.UserName == System.Web.HttpContext.Current.User.Identity.Name) select user.Id).SingleOrDefault();

    var modifiedEntries = ChangeTracker.Entries<IAuditableEntity>()
            .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);

    foreach (EntityEntry<IAuditableEntity> entry in modifiedEntries)
    {
        entry.Entity.ModifiedById = UserId;
        entry.Entity.Modified = DateTime.Now;

        if (entry.State == EntityState.Added)
        {
            entry.Entity.CreatedById = UserId;
            entry.Entity.Created = DateTime.Now;
        }
    }

    return base.SaveChanges();
}


Using EF 7 new "Shadow Properties" Feature:

Shadow properties are properties that do not exist in your entity class. The value and state of these properties is maintained purely in the Change Tracker.

In other words, the audit columns will not be exposed on your entities which seems to be a better option compare to the one above where you have to include them on your entities.

To implement shadow properties, first you have to configure them on your entities. Let's say for example you have a User object that needs to have some audit columns:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<User>().Property<int>("CreatedById");

    modelBuilder.Entity<User>().Property<DateTime>("Created");

    modelBuilder.Entity<User>().Property<int>("ModifiedById");

    modelBuilder.Entity<User>().Property<DateTime>("Modified");
}


Once configured, now you can access them on SaveChanges() override and update their values accordingly:

public override int SaveChanges()
{
    int? userId = null;
    if (System.Web.HttpContext.Current != null)
        userId = (from user in Users.Where(u => u.UserName == System.Web.HttpContext.Current.User.Identity.Name) select user.Id).SingleOrDefault();

    var modifiedBidEntries = ChangeTracker.Entries<User>()
        .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);

    foreach (EntityEntry<User> entry in modifiedBidEntries)
    {
        entry.Property("Modified").CurrentValue = DateTime.UtcNow;
        entry.Property("ModifiedById").CurrentValue = userId;

        if (entry.State == EntityState.Added)
        {
            entry.Property("Created").CurrentValue = DateTime.UtcNow;
            entry.Property("CreatedById").CurrentValue = userId;
        }
    }

    return base.SaveChanges();
}


Final Thoughts:

For implementing something like audit columns, I'll take the Shadow Properties approach since these are cross cutting concerns and do not necessarily belong to my domain objects so having them implemented this way will keep my domain objects nice and clean.

Morteza Manavi
  • 33,026
  • 6
  • 100
  • 83
0

I've worked on a library that might help.

Take a look at Audit.EntityFramework library, it intercepts SaveChanges() and is compatible with EF Core versions.

thepirat000
  • 12,362
  • 4
  • 46
  • 72