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);
}
"Audit trail" data In our audit trail table, we use the fields as follows. "KeyFieldID" stores a link between the Person-SampleData.ID field. "AuditActionTypeENUM" tells us what type of audit record this is (create,edit,delete..). "DateTimeStamp" gives us a point in time when the event occurred.
Now, when you add a new Student and call SaveChanges() method, EF will assign a newly generated id to the StudentID property. EF execute each INSERT command followed by SELECT scope_identity() statement. SCOPE_IDENTITY returns the last identity value inserted into an identity column in the same scope.
Entity Framework Core (EF Core) interceptors enable interception, modification, and/or suppression of EF Core operations. This includes low-level database operations such as executing a command, as well as higher-level operations, such as calls to SaveChanges.
An audit trail is a step-by-step record by which accounting, trade details, or other financial data can be traced to their source. Audit trails are used to verify and track many types of transactions, including accounting transactions and trades in brokerage accounts.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With