Entity Framework 5 Using SaveChanges to add audit log

asp.net-mvc c# entity-framework savechanges

Question

Overriding SaveChanges in EF to include an audit logger seems simple enough. To set the audit properties (created, createdby, updated, updatedby), see the ApplyAuditLogging method below.

   public override int SaveChanges()
    {
        var autoDetectChanges = Configuration.AutoDetectChangesEnabled;

        try
        {
            Configuration.AutoDetectChangesEnabled = false;
            ChangeTracker.DetectChanges();
            var errors = GetValidationErrors().ToList();
            if(errors.Any())
            {
                throw new DbEntityValidationException("Validation errors were found during save: " + errors);
            }

            foreach (var entry in ChangeTracker.Entries().Where(e => e.State == EntityState.Added || e.State == EntityState.Modified))
            {
                ApplyAuditLogging(entry);
            }

            ChangeTracker.DetectChanges();

            Configuration.ValidateOnSaveEnabled = false;

            return base.SaveChanges();
        }
        finally
        {
            Configuration.AutoDetectChangesEnabled = autoDetectChanges;
        }
    }

    private static void ApplyAuditLogging(DbEntityEntry entityEntry)
    {

        var logger = entityEntry.Entity as IAuditLogger;
        if (logger == null) return;

        var currentValue = entityEntry.Cast<IAuditLogger>().Property(p => p.Audit).CurrentValue;
        if (currentValue == null) currentValue = new Audit();
        currentValue.Updated = DateTime.Now;
        currentValue.UpdatedBy = "???????????????????????";
        if(entityEntry.State == EntityState.Added)
        {
            currentValue.Created = DateTime.Now;
            currentValue.CreatedBy = "????????????????????????";
        }
    }

The issue is how to set the UpdatedBy and CreatedBy attributes of the object using the Windows user logon/username. Therefore, I couldn't utilize this!

A new record has to be put to the child table CallHistory anytime the contact is edited, therefore in a different scenario I needed to automatically add a new CallHistory record to my Contact. I performed it in the repository's InsertOrUpdate function, but it seems unclean; it would be wonderful if I could do it at a higher level instead because I now need to set the current user from the database. Once again, the issue arises when I need to construct a CallHistory record (SalesRep = User) and need to get the user from the database.

Currently, the code in my repository creates an audit item everytime an object is created or edited, as well as a call history entry whenever a contact is updated.

ContactRepository.SetCurrentUser(User).InsertOrUpdate(contact)

The user must be in the repository context in order to:

    var prop = typeof(T).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);

    if (prop.GetValue(entity, null).ToString() == "0")
    {
        // New entity
        _context.Set<T>().Add(entity);
        var auditLogger = entity as IAuditLogger;
        if (auditLogger != null)
            auditLogger.Audit = new Audit(true, _principal.Identity.Name);
    }
    else
    {
        // Existing entity
        _context.Entry(entity).State = EntityState.Modified;
        var auditLogger = entity as IAuditLogger;
        if (auditLogger != null && auditLogger.Audit != null)
        {
            (entity as IAuditLogger).Audit.Updated = DateTime.Now;
            (entity as IAuditLogger).Audit.UpdatedBy = _principal.Identity.Name;
        }

        var contact = entity as Contact;
        if (_currentUser != null)
            contact.CallHistories.Add(new CallHistory
                {
                    CallTime = DateTime.Now,
                    Contact = contact,
                    Created = DateTime.Now,
                    CreatedBy = _currentUser.Logon,
                    SalesRep = _currentUser
                });
    }
}

Is there a way to set the SalesRep on my CallHistory (see above code) by somehow injecting the Windows user into the SaveChanges override in the DbContext and retrieving a User from the database based on the Windows login id?

Here is my MVC app's Action on Controller:

[HttpPost]
public ActionResult Create([Bind(Prefix = "Contact")]Contact contact, FormCollection collection)
{
    SetupVOs(collection, contact, true);
    SetupBuyingProcesses(collection, contact, true);

    var result = ContactRepository.Validate(contact);

    Validate(result);

    if (ModelState.IsValid)
    {
        ContactRepository.SetCurrentUser(User).InsertOrUpdate(contact);
        ContactRepository.Save();
        return RedirectToAction("Edit", "Contact", new {id = contact.Id});
    }

    var viewData = LoadContactControllerCreateViewModel(contact);

    SetupPrefixDropdown(viewData, contact);

    return View(viewData);
}
1
6
11/12/2014 5:23:39 PM

Popular Answer

To get the HttpContext.Current.User.Identity.Name from inside your audit code is the quick and lazy method to accomplish it. However, if you have a beautifully tiered application, this will establish a reliance on System.Web.*, which is probably not what you want (and it wouldn't work if you were using true separate tiers).

One alternative would be to just construct an overload that accepts your login rather than overriding SaveChanges. After finishing your work, invoke the actual SaveChanges function. The drawback is that someone may accidentally (or on purpose) use SaveChanges() (the actual one) and avoid the auditing.

A simpler solution would be to include a _currentUser property in your DbContext and provide it to the constructor. The user is just sent in at that point when the context is created. Unfortunately, the constructor doesn't actually allow you to search for the user in the database.

Instead of adding the complete contact, you may only store the ContactID. Your Contact need to be present.

6
9/24/2012 10:51:11 PM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow