12

I know it's several similar posts out there, but I cannot find any with a solution to this issue.

I want to add a (sort of) AudioLog when adding, changing or deleting entities (soft-delete) in Entity Framework 6. I've overridden the SaveChanges and because I only want to add log entries for EntityStates Added, Modified or Deleted, I fetch the list before I call SaveChanges the first time. The problem is, because I need to log what operation has been executed, I need to inspect the EntityState of the entities. But after SaveChanges is called, the EntityState is Unchanged for all entries.

public override int SaveChanges()
{
    using (var scope = new TransactionScope())
    {
        var modifiedEntries = ChangeTracker.Entries()
            .Where(e => e.State == EntityState.Added || e.State == EntityState.Deleted || e.State == EntityState.Modified)
            .ToList();

        int changes = base.SaveChanges();
        foreach (var entry in modifiedEntries)
        {
            ApplyAuditLog(entry);
        }

        base.SaveChanges();
        scope.Complete();
        return changes;
    }
}

private void ApplyAuditLog(DbEntityEntry entry)
{
    ILog entity = entry.Entity as ILog;

    if (entity != null)
    {
        LogOperation operation;
        switch (entry.State)
        {
            case EntityState.Added:
                operation = LogOperation.CreateEntity;
                break;
            case EntityState.Deleted:
                operation = LogOperation.DeleteEntity;
                break;
            case EntityState.Modified:
                operation = LogOperation.UpdateEntity;
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }

        AuditLog log = new AuditLog
        {
            Created = DateTime.Now,
            Entity = entry.Entity.GetType().Name,
            EntityId = entity.Id,
            Operation = operation,
        };

        AuditLog.Add(log);
    }
}
5
  • If the id is database generated, then you need to retrieve it after the SaveChanges() call, because before the id is simply not existant. Commented Nov 5, 2013 at 20:12
  • If you look at the code, that is exactly what I do. That is not the issue, it's the EntityState changing after SaveChanges. Commented Nov 5, 2013 at 20:42
  • 1
    Sorry, I overlooked that. After saving the EntityState will change, because the EntityState is not added or modified anymore - it's unmodified. So if you want to keep the EntityState, you have to store that data before saving - and retrieve the id after saving. Commented Nov 5, 2013 at 20:48
  • That I am aware of. But I hoped someone have a nice way of solving this. Since I do not have the id before the SaveChanges(), I can't figure out how I can map the right id to the right AuditLog object after the first SaveChanges(). Do I need to rely on the index position of each element? Or is there a cleaner alternative? If so, please provide an example of that. Commented Nov 6, 2013 at 8:17
  • If you do it in one go you can save the changes and get the id back in one go. Have posted the code below. It does change to EntityState.Unchanged, but you can log the id in the top loop and then lookup the entity in the bottom loop Commented Nov 13 at 14:19

3 Answers 3

19

Ahhh... Off course!! The id will only be a "problem" for the entities that are newly added, so by splitting the list into two (one for modified/deleted and one for added), I create the AuditLog in two stages.

For anyone else who want to apply this kind of AuditLog, here is my working code:

public override int SaveChanges()
{
    using (var scope = new TransactionScope())
    {
        var addedEntries = ChangeTracker.Entries().Where(e => e.State == EntityState.Added).ToList();
        var modifiedEntries = ChangeTracker.Entries().Where(e => e.State == EntityState.Deleted || e.State == EntityState.Modified).ToList();

        foreach (var entry in modifiedEntries)
        {
            ApplyAuditLog(entry);
        }

        int changes = base.SaveChanges();
        foreach (var entry in addedEntries)
        {
            ApplyAuditLog(entry, LogOperation.CreateEntity);
        }

        base.SaveChanges();
        scope.Complete();
        return changes;
    }
}

private void ApplyAuditLog(DbEntityEntry entry)
{
    LogOperation operation;
    switch (entry.State)
    {
        case EntityState.Added:
            operation = LogOperation.CreateEntity;
            break;
        case EntityState.Deleted:
            operation = LogOperation.DeleteEntity;
            break;
        case EntityState.Modified:
            operation = LogOperation.UpdateEntity;
            break;
        default:
            throw new ArgumentOutOfRangeException();
    }

    ApplyAuditLog(entry, operation);
}

private void ApplyAuditLog(DbEntityEntry entry, LogOperation logOperation)
{
    ILog entity = entry.Entity as ILog;

    if (entity != null)
    {
        AuditLog log = new AuditLog
        {
            Created = DateTime.Now,
            Entity = entry.Entity.GetType().Name,
            EntityId = entity.Id,
            Operation = logOperation,
        };
        AuditLog.Add(log);
    }
}
Sign up to request clarification or add additional context in comments.

3 Comments

You may want to wrap SaveChanges in a try/throw to catch an exception and back out the audit entries. If the commit fails the audit entries shouldn't exist
Actually, you need the id in case of addition, the audit must be related to original table for future investigation.
Why are u using 2 foreach ? instead you can do it once
0

you can save the EntityState to Entity key-value pair and use it after first changes.

var entries = this.ChangeTracker.Entries() .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified || e.State == EntityState.Deleted) .Select(e => new { e.State, e }).ToList();

2 Comments

Please correct the formatting of your response Editing Help and ensure your question answers the original authors question.
Please read How to answer and update your answer.
0

It might be easier to just check for the temporary key value. As it is marked as Temporary by EF. Once you SaveChanges it will have the New Id from the database. The only hassle is that it is now marked a Unchanged, but you can still process the records by building the audit record from the current entry in ChangeTracker.

  
  using (var scope = new TransactionScope())
  {
      foreach (var key in
               from entry in changedEntries
               from property in entry.Properties
               let type = ((RuntimeEntityType)entry.Metadata).Name
               where property.Metadata.IsPrimaryKey()
               select property)
      {
          var type = key.EntityEntry.Entity.ToString();

          //Skip Types that are not audited
          if (type is "HistoryLog") continue;

          if (key.IsTemporary)
          {
              //If Key is Temporary then the record to get Identity Values from DB
              base.SaveChanges();
              var id = key.CurrentValue;
          }

          //Build the Audit record from the new Entry 
          foreach (var entry in ChangeTracker.Entries())
          {
            //Build the Audit Records as it will now have the new ID
     
          }
      }

      scope.Complete();
  }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.