Entity Framework 5 Using SaveChanges to add audit log

10,389

Solution 1

Well, the simple and lazy way to do it is to simply access the HttpContext.Current.User.Identity.Name from within your audit code. However, this will create a dependency on System.Web.*, which is probably not what you want if you have a nicely tiered application (and it wouldn't work if you were using actual seperate tiers).

One option would be, instead of overriding SaveChanges, just create an overload that takes your username. Then you do your work, and call the real SaveChanges afterwards. The disadvantage is that someone could call SaveChanges() (the real one) by mistake (or on purpose) and bypass the auditing.

A better way would be to simply add a _currentUser property to your DbContext and uase a constructor to pass it in. Then when you create the context, you just pass the user in at that time. Unfortunately, you can't really look up the user in the database from the constructor.

But you can simply save the ContactID and add that instead of the entire contact. You're Contact should already exist.

Solution 2

i know that this is a late answer but i just stubmled on this question. I had a very similar use case. We did it as follows:

var auditUsername = Current.User.Identity.Name;
var auditDate = DateTime.Now;

And the current class:

public class Current
    {
        public static IPrincipal User
        {
            get
            {
                return System.Threading.Thread.CurrentPrincipal;
            }
            set
            {
                System.Threading.Thread.CurrentPrincipal = value;
            }

        }
    }

This returns the windows user of the proccess, or the user that is logged in in the ASP.NET applicatoin. To read more: http://www.hanselman.com/blog/SystemThreadingThreadCurrentPrincipalVsSystemWebHttpContextCurrentUserOrWhyFormsAuthenticationCanBeSubtle.aspx

Share:
10,389

Related videos on Youtube

user1538467
Author by

user1538467

.Net Architect. Love to write .Net code (or any code for that matter), strongly support DDD and TDD. Have consulted in South Africa, United Kingdom and twice in the USA. Specialize in the .Net stack. Lately converted over to .Net Core 1.0 and just launched my first self hosted Web App and launched to production just last month (Sep 2016). Clean architecture of 1.0 definitely appealing. On the client-side, KnockoutJS is great but have recently switched to AngularJS and Typescript.

Updated on September 15, 2022

Comments

  • user1538467
    user1538467 over 1 year

    Seems straight forward override SaveChanges in EF to add an audit logger. See the ApplyAuditLogging method to set the audit properties (created, createdby, updated, updatedby) 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 problem is that how to get the windows user logon/username to set the UpdatedBy and CreatedBy properties of the object? I could therefore not use this!

    Also, in another case I wanted to automatically add a new CallHistory record to my Contact; whenever the contact is modified, a new record needs to be added to the child table CallHistory. So I did it in InsertOrUpdate of the Repository but it feels dirty, would be nice if I could do it at a higher level as now I have to set the current user from the database. Again here the problem is that I need to fetch the user from the database to create a CallHistory record (SalesRep = User).

    The code in my Repository does 2 things now, 1, it created an audit entry on the object when it is created or updated and, 2, it also created a CallHistory entry whenever the Contact is updated:

    ContactRepository.SetCurrentUser(User).InsertOrUpdate(contact)
    

    In order to have the user in the Repository context for:

        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 somehow inject the windows user into the SaveChanges override in the DbContext and is there also a way to fetch a User from the database based on windows logon id so I can set the SalesRep on my CallHistory (see above code)?

    Here is my Action on controller on MVC app:

    [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);
    }
    
  • Northstrider
    Northstrider about 11 years
    the SamAccountName is changeable therefore not a good identifier. I use the active directory guid that all objects get instead