benno
benno

Reputation: 167

Audit Logging Inserts

I was wondering if anyone could offer some input for an implementation of audit logging for inserts. I need to ensure that it is transactional.

I have an audit DbSet built upon the following POCO:

public class Audit {
    public int Id { get; set; }
    public User User { get; set; }
    public DateTime Created { get; set; }
    public string Type { get; set; }
    public int EntityId { get; set; }
    public string Message { get; set; }
}

I have a DbSet, say Users, that when I do an insert I want to create automatically add an Audit entity in the Audit DbSet. Consider the following code:

//var user = new User();
//user.Created = DateTime.Now;
//user.Username = "testuser";
//user.Password = "testpassword";
//dataContext.Users.Add(user);

var post = new Post();
post.Created = DateTime.Now;
post.Title = "A sample post";
post.Published = true;
post.Body = "Some content goes in here...";

dataContext.Posts.Add(post);

var audit = new Audit();
audit.Created = DateTime.Now;
audit.User = CurrentUser.User; // Currently logged in user
audit.Type = "Post.Add";
audit.EntityId = post.Id;
audit.Message = "New post was created";

dataContext.Audits.Add(audit);

dataContext.SaveChanges();

In this instance, an audit entity will be added but the "EntityId" property will be set to 0 (default value) and not to the identity of the created user post, i.e. the identity value (SCOPE_IDENTITY()/@@IDENTITY).

I'd like to keep both points in a single transaction, rather than split the items into two transactions, i.e. persist the User Post first, then persist the Audit second as there is a chance the Audit may fail.

Upvotes: 1

Views: 2614

Answers (2)

Bryan Batchelder
Bryan Batchelder

Reputation: 3637

Override DbContext.SaveChanges() and look at all the ChangeTracker.Entries().Where(e=>e.State != EntityState.Unchanged).

Here is an example from a R&D project of mine:

public override int SaveChanges()
{
    int recordsUpdated = 0;

    var journalEntries = new List<AuditJournal>();

    foreach (var entry in this.ChangeTracker.Entries<ITrackedEntity>().Where(e=>e.State != EntityState.Unchanged))
    {
        AuditJournal journal = new AuditJournal();
        journal.Id = Guid.NewGuid();
        journal.EntityId = entry.Entity.Id.Value;
        journal.EntityType = entry.Entity.EntityType;
        journal.ActionType = entry.State.ToString();
        journal.OccurredOn = DateTime.UtcNow;
        //journal.UserId = CURRENT USER
        //journal.PreviousEntityData = XML SERIALIZATION OF ORIGINAL ENTITTY

        journalEntries.Add(journal);
    }

    using (var scope = new TransactionScope())
    {
        recordsUpdated = base.SaveChanges();

        foreach (var journalEntry in journalEntries)
            this.AuditJournal.Add(journalEntry);

        base.SaveChanges(); //Save journal entries

        scope.Complete();
    }

    return recordsUpdated;
}

//Every entity that needs to be audited has to implement this interface
public interface ITrackedEntity
{
    string EntityType { get; }
    Guid? Id { get; set; }
}

Upvotes: 2

The Evil Greebo
The Evil Greebo

Reputation: 7138

If you don't mind adding a nullable reference to Post to your Audit table, you can let EF do the work for you. Drop the EntityId property and add to Audit:

public Post Post {get; set;}

And when you record the event:

audit.Post = Post;

EF will insert the Post, grab its ID and add the reference to Audit for you in the same transaction. We do this exact thing in our own AuditEvent model, we have several nullable references to our various types so with any given event we can link back to the related object or objects.

Upvotes: 0

Related Questions