Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EntityFrameworkCore - audit log row per changed property #323

Closed
mherberth opened this issue Aug 12, 2020 · 2 comments
Closed

EntityFrameworkCore - audit log row per changed property #323

mherberth opened this issue Aug 12, 2020 · 2 comments
Assignees

Comments

@mherberth
Copy link

Hi,
I'm trying to get the EntityFramework.Core extension to write an AuditLog entry per changed property.

For this purpose I've overidden the EntityFrameworkDataProvider.InsertEvent with a custom DataProvider with Issue #195 as inspiration.
The problem is, using DbContextHelper.Core.CreateAuditEvent to create a new EntityFrameworkEvent returns null.
The reason seems to be, at this point in the code execution DbContextHelper.GetModifiedEntries determines all EF Entries have State.Unmodified, even if they are clearly included in the EventEntry changes.
Trying to circumvent CreateAuditEvent by manually creating the contents is impossible due to private/internal properties.

Maybe there is an alternative solution to this problem I'm not seeing, open to all suggestions.

    public class AuditLog
    {
        public int Id { get; set; }
        public string Description { get; set; }
        public string OldValue { get; set; }
        public string NewValue { get; set; }
        public string PropertyName { get; set; }
        public DateTime AuditDateTime { get; set; }
        public Guid? AuditIssuerUserId { get; set; }
        public string AuditAction { get; set; }
        public string TableName { get; set; }
        public int TablePK { get; set; }
    }

            Audit.Core.Configuration.Setup()
                .UseCustomProvider(new CustomEntityFrameworkDataProvider(x => x
                    .AuditEntityAction<AuditLog>((ev, ent, auditEntity) =>
                    {
                        auditEntity.AuditDateTime = DateTime.Now;
                        auditEntity.AuditAction = ent.Action;
                        foreach(var change in ent.Changes)
                        {
                            auditEntity.OldValue = change.OriginalValue.ToString();
                            auditEntity.NewValue = change.NewValue.ToString();
                            auditEntity.PropertyName = change.ColumnName;
                        }
                    }
        public class CustomEntityFrameworkDataProvider : EntityFrameworkDataProvider
        {
            public override object InsertEvent(AuditEvent auditEvent)
            {
                var auditEventEf = auditEvent as AuditEventEntityFramework;
                if (auditEventEf == null)
                    return null;

                object result = null;
                foreach (var entry in auditEventEf.EntityFrameworkEvent.Entries)
                {
                    if (entry.Changes == null || entry.Changes.Count == 0)
                        continue;

                    foreach (var change in entry.Changes)
                    {
                        var contextHelper = new DbContextHelper();
                        var newEfEvent = contextHelper.CreateAuditEvent((IAuditDbContext)auditEventEf.EntityFrameworkEvent.GetDbContext());
                        if (newEfEvent == null)
                            continue;
                        newEfEvent.Entries = new List<EventEntry>() { entry };
                        entry.Changes = new List<EventEntryChange> { change };
                        auditEventEf.EntityFrameworkEvent = newEfEvent;
                        result = base.InsertEvent(auditEvent);

                    }
                }
                return result;
            }
        }
@thepirat000
Copy link
Owner

thepirat000 commented Aug 12, 2020

You don't need to call CreateAuditEvent, you should be able to handle only the Changes property on the original event, like this:

public override object InsertEvent(AuditEvent auditEvent)
{
    var auditEventEf = auditEvent as AuditEventEntityFramework;
    if (auditEventEf == null)
        return null;

    object result = null;
    foreach (var entry in auditEventEf.EntityFrameworkEvent.Entries)
    {
        if (entry.Changes == null || entry.Changes.Count == 0)
            continue;

        // Call base.InsertEvent for each change
        var originalChanges = entry.Changes;
        foreach (var change in originalChanges)
        {
            entry.Changes = new List<EventEntryChange>() { change };
            result = base.InsertEvent(auditEvent);
        }
        entry.Changes = originalChanges;

    }
    return result;
}

Notes:

  • This could impact performance, since it will trigger an insert to the database for each column change.
  • If you plan to use async calls to DbContext.SaveChangesAsync, you should also implement the InsertEventAsync method on your CustomDataProvider
  • The Changes property is only available for Updates, so if you also want to audit Inserts and Deletes, you'll need to add the logic to get the column values from the ColumnValues property on the event

@mherberth
Copy link
Author

This works.
Thanks for the quick reply!

@thepirat000 thepirat000 self-assigned this Feb 19, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants