3

I was in the middle of implementing a database audit trail whereby CRUD operations performed through my controllers in my Web API project would serialize the old and new poco's and store their values for later retrieval (historical, rollback, etc...).

When I got it all working, I did not like how it made my controllers look during a POST because I ended up having to call SaveChanges() twice, once to get the ID for the inserted entity and then again to commit the audit record which needed to know that ID.

I set out to convert the project (still in its infancy) to use sequences instead of identity columns. This has the added bonus of further abstracting me from SQL Server, though that is not really an issue, but it also allows me to reduce the number of commits and lets me pull that logic out of the controller and stuff it into my service layer which abstracts my controllers from the repositories and lets me do work like this auditing in this "shim" layer.

Once the Sequence object was created and a stored procedure to expose it, I created the following class:

public class SequentialIdProvider : ISequentialIdProvider
{
    private readonly IService<SequenceValue> _sequenceValueService;

    public SequentialIdProvider(IService<SequenceValue> sequenceValueService)
    {
        _sequenceValueService = sequenceValueService;
    }

    public int GetNextId()
    {
        var value = _sequenceValueService.SelectQuery("GetSequenceIds @numberOfIds", new SqlParameter("numberOfIds", SqlDbType.Int) { Value = 1 }).ToList();
        if (value.First() == null)
        {
            throw new Exception("Unable to retrieve the next id's from the sequence.");
        }

        return value.First().FirstValue;
    }

    public IList<int> GetNextIds(int numberOfIds)
    {
        var values = _sequenceValueService.SelectQuery("GetSequenceIds @numberOfIds", new SqlParameter("numberOfIds", SqlDbType.Int) { Value = numberOfIds }).ToList();
        if (values.First() == null)
        {
            throw new Exception("Unable to retrieve the next id's from the sequence.");
        }

        var list = new List<int>();
        for (var i = values.First().FirstValue; i <= values.First().LastValue; i++)
        {
            list.Add(i);
        }

        return list;
    }
}

Which simply provides two ways to get IDs, a single and a range.

This all worked great during the first set of unit tests but as soon as I started testing it in a real world scenario, I quickly realized that a single call to GetNextId() would return the same value for the life of that context, until SaveChanges() is called, thus negating any real benefit.

I am not sure if there is a way around this short of creating a second context (not an option) or going old school ADO.NET and making direct SQL calls and use AutoMapper to get to the same net result. Neither of these are appeal to me so I am hoping someone else has an idea.

abatishchev
  • 98,240
  • 88
  • 296
  • 433
James Legan
  • 1,903
  • 2
  • 14
  • 21
  • How do your audit records look like? One for every table, one for all tables? Which info is stored in the audit? Which operations do you audit? Do you have a generic data layer? – JotaBe Jun 09 '14 at 23:21
  • Single Audit table that contains the following data: Serialized POCO's for the old or new data (or both in the case of a PUT), the table in affected, the record ID in that table, list of changed columns and the operation performed that generated the audit (insert, delete, update). – James Legan Jun 10 '14 at 03:14
  • Then there is no way to do it. The two SaveChanges option looks like the only solution. You should not worry two much about this, for two reasons: 1) .NET uses connection pooling, 2) EF doesn't send the queries in batches, but one by one. So the difference between using two SaveChanges or not should be negligible. The only other option is using GUID as PK: http://stackoverflow.com/questions/18200817/how-to-set-newid-for-guid-in-entity-framework but this adds more disadvantages (poor index performance) because GUID is too long. – JotaBe Jun 10 '14 at 09:00
  • @JotaBe - I was not too worried about the performance but rather where I have to perform the work in order to tie it all together. My UoW and Repository are independent of one another. My repository does not have a reference to my UoW, only my controller does. As such, if I go with the double SaveChanges() approach, I have to perform the work in my controller which adds a bunch of noise that I would rather place in my service class which abstracts the repository. I worked up a solution using my container to inject my connection string into an ADONet class and am going that route for time being – James Legan Jun 10 '14 at 12:41

1 Answers1

0

Don't know if this might help you, but this is how I did my audit log trail using code first. The following is coded into a class inheriting from DbContext.

in my constructor I have the following

IObjectContextAdapter objectContextAdapter = (this as IObjectContextAdapter);
objectContextAdapter.ObjectContext.SavingChanges += SavingChanges;

This is my saving changes method wired up previously

void SavingChanges(object sender, EventArgs e) {
        Debug.Assert(sender != null, "Sender can't be null");
        Debug.Assert(sender is ObjectContext, "Sender not instance of ObjectContext");

        ObjectContext context = (sender as ObjectContext);
        IEnumerable<ObjectStateEntry> modifiedEntities = context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified);
        IEnumerable<ObjectStateEntry> addedEntities = context.ObjectStateManager.GetObjectStateEntries(EntityState.Added);

        addedEntities.ToList().ForEach(a => {
            //Assign ids to objects that don't have
            if (a.Entity is IIdentity && (a.Entity as IIdentity).Id == Guid.Empty)
                (a.Entity as IIdentity).Id = Guid.NewGuid();

            this.Set<AuditLogEntry>().Add(AuditLogEntryFactory(a, _AddedEntry));
        });

        modifiedEntities.ToList().ForEach(m => {
            this.Set<AuditLogEntry>().Add(AuditLogEntryFactory(m, _ModifiedEntry));
        });
    }

And these are the methods used previosly to build up the audit log details

private AuditLogEntry AuditLogEntryFactory(ObjectStateEntry entry, string entryType) {
        AuditLogEntry auditLogEntry = new AuditLogEntry() {
            EntryDate = DateTime.Now,
            EntryType = entryType,
            Id = Guid.NewGuid(),
            NewValues = AuditLogEntryNewValues(entry),
            Table = entry.EntitySet.Name,
            UserId = _UserId
        };

        if (entryType == _ModifiedEntry) auditLogEntry.OriginalValues = AuditLogEntryOriginalValues(entry);

        return auditLogEntry;
    }

    /// <summary>
    /// Creates a string of all modified properties for an entity.
    /// </summary>
    private string AuditLogEntryOriginalValues(ObjectStateEntry entry) {
        StringBuilder stringBuilder = new StringBuilder();

        entry.GetModifiedProperties().ToList().ForEach(m => {
            stringBuilder.Append(String.Format("{0} = {1},", m, entry.OriginalValues[m]));
        });

        return stringBuilder.ToString();
    }

    /// <summary>
    /// Creates a string of all modified properties' new values for an entity.
    /// </summary>
    private string AuditLogEntryNewValues(ObjectStateEntry entry) {
        StringBuilder stringBuilder = new StringBuilder();

        for (int i = 0; i < entry.CurrentValues.FieldCount; i++) {
            stringBuilder.Append(String.Format("{0} = {1},",
                entry.CurrentValues.GetName(i), entry.CurrentValues.GetValue(i)));
        }

        return stringBuilder.ToString();
    }

Hopefully this might point you into a direction that might help you solve your problem.

3dd
  • 2,520
  • 13
  • 20
  • What I found with this approach (I had it all wired up and working) is that "CurrentValues" and "OriginalValues" are the same in a detached, code first repository pattern approach. For example, in my RESTful web api, when an update comes in, I do a find on the ID being updated (it is detached), I make the necessary changes and then I pass the update into the repository. When EF attaches the entity, both Original and Current values are the same. This is a known limitation (not to me at the time) of the framework. You can see the feature vote here http://entityframework.codeplex.com/workitem/864 – James Legan Jun 10 '14 at 12:38
  • Thanks for the comment @JDBuckSavage this is something I was unaware of, need to go check my unit tests to ensure my app functions correctly. – 3dd Jun 11 '14 at 04:24