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

Simple Tracking Changes #487

Closed
EliveltonRepolho opened this issue Apr 20, 2022 · 17 comments
Closed

Simple Tracking Changes #487

EliveltonRepolho opened this issue Apr 20, 2022 · 17 comments

Comments

@EliveltonRepolho
Copy link

I have a question about it if is possible to track like only Date and User Created/Modified for example, inside classes.. Not creating a full history change.. example:

public abstract class AuditEntity {
    public DateTime? CreatedDate { get; set; }
    public DateTime? ModifiedDate { get; set; }  
    public string? CreatedBy { get; set; }
    public string? ModifiedBy { get; set; }
}

public class SimpleTrackEntity : AuditEntity {
    public string? Description { get; set; }
}

I did not find something related if this is possible to do using this Audit.NET...

My use case is:
I should create a Trail Table, which I am using this Audit.NET to do it, but do want to set Date and User Created/Modified for some entities, so this way it is easy to get.

@thepirat000
Copy link
Owner

thepirat000 commented Apr 20, 2022

Please add more context.

Are you auditing changes on EF using Audit.EntityFramework?

@EliveltonRepolho
Copy link
Author

EliveltonRepolho commented Apr 20, 2022

Sorry, I am using Audit.EntityFramework.Core

Here is my Configs I am current using:

Audit.EntityFramework.Configuration.Setup()
    .ForContext<AppDbContext>(config => config
        .AuditEventType("EF:{context}"))
    .UseOptIn()
    ;

Audit.Core.Configuration.Setup()
    .UseEntityFramework(ef => ef
        .AuditTypeMapper(t => typeof(AuditTrail))
        .AuditEntityAction<AuditTrail>((ev, entry, entity) =>
        {
            entity.JsonData = entry.ToJson();
            entity.TablePk = entry.PrimaryKey.ToJson();
            
            entity.Action = entry.Action.ToString();
            
            entity.ModifiedDate = DateTime.UtcNow;
            entity.TableName = entry.Table;
            entity.EventType = ev.EventType.ToString();
            
            entity.HostUserName = ev.Environment.UserName;
            entity.IdentityUserName = ev.CustomFields.GetValueOrDefault(IdentityUserName, IdentityUserNameDefault)?.ToString();
        })
        .IgnoreMatchedProperties()
    );

Configuration.JsonSettings = new JsonSerializerOptions
{
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
};

What I am trying to do is to add an extra step, to set Date and User Created/Modified for classes extending AuditEntity

@thepirat000
Copy link
Owner

You can access the actual entity being modified from the audit action with entry.Entity. So I guess you could do something like this:

Audit.Core.Configuration.Setup()
    .UseEntityFramework(ef => ef
        .AuditTypeMapper(t => typeof(AuditTrail))
        .AuditEntityAction<AuditTrail>((ev, entry, entity) =>
        {
            if (entry.Entity is AuditEntity auditEntity)
            {
                entity.ModifiedDate = auditEntity.ModifiedDate;
            }
            ....

@EliveltonRepolho
Copy link
Author

My entry.Entity is null:

image

@EliveltonRepolho
Copy link
Author

Change to :

Audit.Core.Configuration.Setup()
    .UseEntityFramework(ef => ef
        .AuditTypeMapper(t => typeof(AuditTrail))
        .AuditEntityAction<AuditTrail>((ev, entry, entity) =>
        {
            if (entry.GetEntry().Entity is AuditEntity auditEntity)
            {
                entity.ModifiedDate = auditEntity.ModifiedDate;
            }
            ....

And Worked .. thanks a lot !!

@EliveltonRepolho
Copy link
Author

Actually .. doing this way... this became an infinity loop...

Like, if I insert a new Item, next will be the Update triggered by my changes to Entity.. and so on...

@thepirat000
Copy link
Owner

thepirat000 commented Apr 20, 2022

You need set IncludeEntityObjects setting to true for the entity to be included on the entry.Entity property.

Also try decorating with [AuditIgnore] attribute the entities (classes) that represent the Audits, so they are not tracked

@EliveltonRepolho
Copy link
Author

Nice, let's double check.. if I am doing something wrong.

Now I have entry.Entity being populated correctly...

Have decorated those:

[AuditIgnore]
public class AuditTrail { ... }

[AuditIgnore]
public abstract class AuditEntity { ... }

But still having infinity loop :/

@thepirat000
Copy link
Owner

I'm confused regarding your entities and configuration...

If you could create a minimal project that reproduces the issue I would be able to help

Seems like you are using the same DbContext for the tracked entities and the audit entities, which is fine, but if your DbContext is not inheriting from AuditDbContext, then that could provoke the loop since you will be auditing the audit.

@thepirat000
Copy link
Owner

If that's the case (AppDbContext not inheriting from AuditDbContext), another option is to implement IAuditBypass on your DbContext:

public class AppDbContext : DbContext, IAuditBypass
{
  public int SaveChangesBypassAudit()
  {
    return base.SaveChanges():
  }

  public Task<int> SaveChangesBypassAuditAsync()
  {
    return base.SaveChangesAsync();
  }
  // ...
}

In that way, any Audit entity will bypass the audited SaveChanges, and call the original DbContext's SaveChanges

@EliveltonRepolho
Copy link
Author

Created this sample project: https://github.com/EliveltonRepolho/Audit.NET.Trail.Simple.Logs

You can check the infinity loop here: https://github.com/EliveltonRepolho/Audit.NET.Trail.Simple.Logs/blob/main/MiniDemo/Program.cs#L37

And thanks a lot for your attention !

@thepirat000
Copy link
Owner

thepirat000 commented Apr 20, 2022

So you are trying to modify the actual entity, not the audit entity. But the Audit Entity Action takes place after the entity is modified and saved, so you cannot update the actual entity in the Audit Entity Action.

The auditEntity on line 37 represents the entity already saved, and you are changing its properties again (CreateDate, ModifiedDate) after it was saved, so EF will interpret that entity has an unsaved change.

I still don't get the AuditEntity vs AuditTrail difference... I'm assuming AuditEntity is just a normal entity, like employees and so on.

But, I think you can still do what you want at the AuditEvent level, from a Custom Action of type OnScopeCreated, since the scope is created before the SaveChanges takes place:

Audit.Core.Configuration.AddCustomAction(ActionType.OnScopeCreated, scope =>
{
    var efEvent = scope.GetEntityFrameworkEvent();
    foreach (var entry in efEvent.Entries)
    {
        var now = DateTime.UtcNow;
        if (entry.GetEntry().Entity is AuditEntity auditEntity)
        {
            if (entry.Action == "Insert")
            {
                auditEntity.CreatedDate = now;
            }
            auditEntity.ModifiedDate = now;
        }
    }
});

@EliveltonRepolho
Copy link
Author

Humm .. undestood now.

AuditTrail: is my changes log table, all tables will be logged here, so I can track Before and After changes
AuditEntity: is a simple abastract class so I can inherit Fields like Created/Modified Date/User and automatically set these fields in same place (Audit Config)

Will try your suggestion today.. Thanks a lot

@EliveltonRepolho
Copy link
Author

So you are trying to modify the actual entity, not the audit entity. But the Audit Entity Action takes place after the entity is modified and saved, so you cannot update the actual entity in the Audit Entity Action.

The auditEntity on line 37 represents the entity already saved, and you are changing its properties again (CreateDate, ModifiedDate) after it was saved, so EF will interpret that entity has an unsaved change.

I still don't get the AuditEntity vs AuditTrail difference... I'm assuming AuditEntity is just a normal entity, like employees and so on.

But, I think you can still do what you want at the AuditEvent level, from a Custom Action of type OnScopeCreated, since the scope is created before the SaveChanges takes place:

Audit.Core.Configuration.AddCustomAction(ActionType.OnScopeCreated, scope =>
{
    var efEvent = scope.GetEntityFrameworkEvent();
    foreach (var entry in efEvent.Entries)
    {
        var now = DateTime.UtcNow;
        if (entry.GetEntry().Entity is AuditEntity auditEntity)
        {
            if (entry.Action == "Insert")
            {
                auditEntity.CreatedDate = now;
            }
            auditEntity.ModifiedDate = now;
        }
    }
});

Just one thing about using this solution, I am still testing and reading the docs if has another config the must be done. But in the example I sent (https://github.com/EliveltonRepolho/Audit.NET.Trail.Simple.Logs)... I updated with your suggestion, but if for example I try to insert same Employee ID (Forcing violating Primary Key) we have an infinity loop now in this portion of code :/

@thepirat000
Copy link
Owner

thepirat000 commented Apr 21, 2022

The Audit.EntityFramework.Core will trigger the audit even if the original SaveChanges fails.
I'm not sure why it could be looping, but If you don't want to save audits when SaveChanges fails, you can change your AuditEntityAction to return a boolean depending on the Success field of the EF event:

https://stackoverflow.com/a/71700459/122195

@thepirat000
Copy link
Owner

thepirat000 commented Apr 22, 2022

Also, the recommendation is to set up a different instance of your DbContext for the audit entities by calling .UseDbContext()

Audit.Core.Configuration.Setup()
    .UseEntityFramework(ef => ef
        .UseDbContext<AppDbContext>() 
        ...);

Check this #168 (comment)

@EliveltonRepolho
Copy link
Author

thanks for the recommendation too, I've implemented and now everything looks good.

Closing this issue .. will leave this example available if might be useful for someone else.

Thanks a lot for helping with it !

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