thepirat000 / Audit.NET

An extensible framework to audit executing operations in .NET and .NET Core.
MIT License
2.23k stars 322 forks source link

What Id can I use when correlating changes across entities. #196

Closed VictorioBerra closed 5 years ago

VictorioBerra commented 5 years ago

I am trying to gather up all audit rows that happened as a result of the same request. I am not using transactions at the moment, but my DbContext is a scoped dependency so I can I rely on ConnectionId?

Or, should I be passing a custom CorrelationId down all my layers every time?

Should I maybe add transactions to all my requests so I have the TransactionId? Or will that cause me great pain when it comes to scoped transactions/distributed transactions?

VictorioBerra commented 5 years ago

I am using this library https://github.com/stevejgordon/CorrelationId so I went ahead and did something like this:

Audit.Core.Configuration.AddCustomAction(ActionType.OnScopeCreated, scope =>
{
    var svcProvider = services.BuildServiceProvider();
    var currentUserAccessor = svcProvider.GetRequiredService<ICurrentUserAccessor>();
    var currentCorrelationAccessor = svcProvider.GetRequiredService<ICorrelationContextAccessor>();

    scope.SetCustomField(Domain.Constants.Auditing.IdentityName, currentUserAccessor.GetCurrentUsername() ?? "Anonymous");
    scope.SetCustomField(Domain.Constants.Auditing.CorrelationId, currentCorrelationAccessor.CorrelationContext.CorrelationId);
});

Now I know all specific auditing records happened during the same request. So I can gather up all audit rows with that same correlationId.

This still feels brittle though. Maybe one single request might not always be enough to determine that a bunch of differennt audit rows are related?

thepirat000 commented 5 years ago

It looks good to me.

If you are in a ASP.NET Core Web API, you could use the HttpContext.TraceIdentifier value (a unique value per request). So you can add this correlation ID to the events via a Custom Action.

But I would recommend to also activate the audit logging for the controller actions(s) with Audit.WebApi library. It will generate audit events for each call to your API, including the TraceIdentifier.

Take a look at the web api template provided. You can install the template with:

dotnet new -i Audit.WebApi.Template

And then you can generate a project that uses both Audit.WebApi and Audit.EntityFramework both including the correlation:

dotnet new webapiaudit -E

VictorioBerra commented 5 years ago

@thepirat000 Thanks a lot I am exploring this now. So when I add stuff to SetCustomField via the CustomAction that same scope is accessible in my AuditEntityAction? I am a little new to how all this scope stuff works all throughout the Audit libraries.

VictorioBerra commented 5 years ago

Can you chain? IE:

Audit.Core.Configuration.Setup()
    .UseEntityFramework(x => {
        // Run this for all audits
        x.AuditTypeMapper(t => typeof(IAudit))
            .AuditEntityAction<IAudit>((auditEvent, eventEntry, auditEntity) =>
            {
                var entityFrameworkEvent = auditEvent.GetEntityFrameworkEvent();

                IEntity entity = eventEntry.GetEntry().Entity as IEntity;
                if (entity != null)
                {
                    entity.Id = 0;
                }

            });

        x.AuditTypeNameMapper(typeName => "Audit_" + typeName)
            .AuditEntityAction<IAudit>((auditEvent, eventEntry, auditEntity) =>
            {
                var entityFrameworkEvent = auditEvent.GetEntityFrameworkEvent();                      
                auditEntity.Action = eventEntry.Action;
                auditEntity.AuditDate = DateTime.UtcNow;
            });
    });
thepirat000 commented 5 years ago

About the configuration, you can only have one entity mapper, so you should call one of the three: AuditTypeMapper, AuditTypeNameMapper or AuditTypeExplicitMapper.

For the first two, you can attach only one AuditEntityAction. For the explicit mapper, you can configure one AuditEntityAction per table mapping.

You can see some examples here.

Also note the Custom Actions are globally applied to all the events but the AuditEntityAction is executed for each entry (each table affected) on the EF events.

Specifically for the Entity Framework events, it will work like this:

1- Execute the OnScopeCreated custom actions for the event. 2- Save the DbContext changes 3- Execute the AuditEntityAction for each entry on the event 4- Execute the OnEventSaving custom actions for the event 5- Save the Audit DbContext changes 6- Execute the OnEventSaved custom actions for the event

VictorioBerra commented 5 years ago

@thepirat000 Can I get access to the entity(s) being saved in OnScopeCreated? I want to modify them.

Is there a better way than this?

        var entities = ChangeTracker.Entries().Where(x => x.Entity is BaseEntity && (x.State == EntityState.Added || x.State == EntityState.Modified));
thepirat000 commented 5 years ago

Yes, you can use the GetEntityFrameworkEvent extension to get the EF part of the event:

using Audit.EntityFramework;

Audit.Core.Configuration.AddCustomAction(ActionType.OnScopeCreated, scope =>
{
    EntityFrameworkEvent efEvent = scope.GetEntityFrameworkEvent();
    //efEvent.Entries[0].Changes[0].OriginalValue
}
VictorioBerra commented 5 years ago

@thepirat000 Can we get a constant/enum for the StateName instead of a string so we can switch on it?

https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.EntityFramework/DbContextHelper.cs#L113-L129

I wish I could do this:

foreach (var entry in efEvent.Entries.Where(x => x.Action == EfAuditAction.Added))
{
    // ...
}
thepirat000 commented 5 years ago

It could be, I will take note and maybe change it to an enum in the next version, but that will be a breaking change.

Anyway nothing stops you from switching on the string:

public static class EfAuditAction
{
    public static string Added { get; } = "Insert";
    public static string Deleted { get; } = "Delete";
    public static string Modified { get; } = "Update";
}
VictorioBerra commented 5 years ago

@thepirat000 Also, looks like "Changes" is only available for update operations.

What I am trying to accomplish is updating some metadata on the entities themselves:

        public override void OnScopeCreated(AuditScope auditScope)
        {
            var currentUsername = (string)auditScope.Event.CustomFields[Domain.Constants.Auditing.IdentityName];

            var entities = ChangeTracker.Entries().Where(x => x.Entity is IAuditable && (x.State == EntityState.Added || x.State == EntityState.Modified));
            foreach (var entity in entities)
            {
                if (entity.State == EntityState.Added)
                {
                    ((IAuditable)entity.Entity).CreatedOn = DateTime.UtcNow;
                    ((IAuditable)entity.Entity).CreatedBy = currentUsername;
                }

                ((IAuditable)entity.Entity).UpdatedOn = DateTime.UtcNow;
                ((IAuditable)entity.Entity).CreatedBy = currentUsername;
            }
        }

Is there an easy way to change the CreatedOn and CreatedBy through the efEvent.Entries?

thepirat000 commented 5 years ago

Yes, Changes is only available for Update operations. And I guess you don't need to use the ChangeTracker. This should work:

public override void OnScopeCreated(AuditScope auditScope)
{
    var currentUsername = (string)auditScope.Event.CustomFields[Domain.Constants.Auditing.IdentityName];

    var entities = auditScope.GetEntityFrameworkEvent()
        .Entries.Where(x => x.Action == "Insert" || x.Action == "Update");
    foreach (var entity in entities)
    {
        // entity.GetEntry().CurrentValues, etc...
        if (entity.Action == "Insert")
        {
            ((IAuditable)entity.Entity).CreatedOn = DateTime.UtcNow;
            ((IAuditable)entity.Entity).CreatedBy = currentUsername;
        }

        ((IAuditable)entity.Entity).UpdatedOn = DateTime.UtcNow;
        ((IAuditable)entity.Entity).CreatedBy = currentUsername;
    }
}

Also note you can access the EntityFramework's EventEntry by calling .GetEntry() on each of the audit event entry.

VictorioBerra commented 5 years ago

Hmm, I get 'Object reference not set to an instance of an object.' on ((IAuditable)entity.Entity). It is saying entry.Entity is null.

VictorioBerra commented 5 years ago

This works. Any idea why?

if (entity.Action == "Insert")
{
    ((IAuditable)entity.GetEntry().Entity).CreatedOn = DateTime.UtcNow;
    ((IAuditable)entity.GetEntry().Entity).CreatedBy = currentUsernameString;
}

((IAuditable)entity.GetEntry().Entity).UpdatedOn = DateTime.UtcNow;
((IAuditable)entity.GetEntry().Entity).CreatedBy = currentUsernameString;
thepirat000 commented 5 years ago

You have to set the IncludeEntityObjects to true on the settings. By default the Entity is not included on the audit event. Or you can just access as you did, with the GetEntry().Entity.

VictorioBerra commented 5 years ago

Ah but GetEntry() lets you force your way around that? Seems to me like its faster to call GetEntry() as needed.

thepirat000 commented 5 years ago

if you don't need the Entity object on your audits, I think you are good with the GetEntry()

thepirat000 commented 5 years ago

for further discussion we can better use the gitter chat channel: https://gitter.im/Audit-NET/Lobby