Search code examples
c#audit.netaudit.net.sqlserver

How to log Update<T>(IList<T>) action as multiple audit records using Audit.net


I have to update bulk records in a table. I did it as following.

BaseRepository.cs

    public IList<T> Update<T>(IList<T> instance) where T : class
    {
        IList<T> insertedItems = new List<T>();
        int totalCount = instance.Count;
        int count = 0;

        foreach (var item in instance)
        {
            insertedItems.Add(Update(item, count == (totalCount - 1)));
            count++;
        }

        try
        {
            context.SaveChanges();
        }
        catch (DbEntityValidationException ex)
        {
            //HandleDbEntityValidationException(ex);
        }
        //
        return insertedItems;
    }

TestMethod

    public bool TestUpdate()
    {
        try
        {
            List<Test> list = new List<Test>();
            Test test1 = new Test();
            test1.Id = 3;
            test1.Message = "test string updated 40";
            Test test2 = new Test();
            test2.Id = 4;
            list.Add(test1); list.Add(test2);
            test2.Message = "test string updated 7";
            _repository.Update<Test>(list);
            this.unitOfWork.Save();
        }
        catch (Exception ex)
        {

        }
        finally
        {
            this.unitOfWork.Dispose();
        }
        return true;
    }

Then I wanted to use Audit.net for log audits. I did as following...

Global.asax.cs

    Audit.Core.Configuration.DataProvider = new SqlDataProvider()
        {
            ConnectionString = "Data Source=FTD-NB-MADHARA;Initial Catalog=TestAuditTrail;User ID=sctavp_user;Password=welcome@123;MultipleActiveResultSets=true",
            Schema = "dbo",
            TableName = "Event",
            IdColumnName = "EventId",
            JsonColumnName = "JsonData",

            CustomColumns = new List<CustomColumn>()
        {
            new CustomColumn("UserName", ev=> ev.Environment.UserName.ToString()),
            new CustomColumn("MachineName", ev=> ev.Environment.MachineName.ToString()),
            new CustomColumn("DomainName", ev=> ev.Environment.DomainName.ToString()),
            new CustomColumn("ModuleName", ev => "AuditTrail"),
            new CustomColumn("CallingMethod", ev=> ev.Environment.CallingMethodName.ToString()),
            new CustomColumn("DatabaseName", ev=> ev.GetEntityFrameworkEvent().Database.ToString()),
            new CustomColumn("SchemaName", ev=> ev.GetEntityFrameworkEvent().Entries[0].Schema.ToString()),
            new CustomColumn("TableName", ev=> ev.GetEntityFrameworkEvent().Entries[0].Table.ToString()),
            new CustomColumn("Action", ev=> ev.GetEntityFrameworkEvent().Entries[0].Action.ToString()),}};

ContextChanges

    private static DbContextHelper _helper = new DbContextHelper();
    private readonly IAuditDbContext _auditContext;

    public AuditTrailContext() : base("ApplicationDatabase")
    {
        //AuditDataProvider = new NullDataProvider();
        _auditContext = new DefaultAuditContext(this);
        _helper.SetConfig(_auditContext);
        
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Add(new DecimalPrecisionAttributeConvention());
    }

    #region without AuditDbContext
    public override int SaveChanges()
    {
        return _helper.SaveChanges(_auditContext, () => base.SaveChanges());
    }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
    {
        return await _helper.SaveChangesAsync(_auditContext, () => base.SaveChangesAsync(cancellationToken));
    }

Result

JsonData field

    {"EventType":"DefaultAuditContext","Environment":{"UserName":"MadharaU","MachineName":"FTD-NB-MADHARA","DomainName":"BRANDIXLK","CallingMethodName":"Rocky.AuditService.Data.EntityManager.BaseRepository.Update()","AssemblyName":"Rocky.AuditService.Data.EntityManager, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null","Culture":"en-US"},"StartDate":"2021-02-23T03:52:47.0005326Z","EndDate":"2021-02-23T03:52:47.0371697Z","Duration":37,"EntityFrameworkEvent":{"Database":"AuditTrail","Entries":[{"Schema":"dbo","Table":"Test","Action":"Update","PrimaryKey":{"Id":3},"Changes":[{"ColumnName":"Message","OriginalValue":"test string updated 39","NewValue":"test string updated 40"}],"ColumnValues":{"Id":3,"Message":"test string updated 40"},"Valid":true,"ValidationResults":[]},{"Schema":"dbo","Table":"Test","Action":"Update","PrimaryKey":{"Id":4},"Changes":[{"ColumnName":"Message","OriginalValue":"test string updated 6","NewValue":"test string updated 7"}],"ColumnValues":{"Id":4,"Message":"test string updated 7"},"Valid":true,"ValidationResults":[]}],"Result":2,"Success":true}}

Now my question is, I have updated two records at the same time. Audit.NET has logged both as a single record in the audit table. Is there any method to insert log details for these two updates separately.


Solution

  • You have at least two options

    Use the Entity Framework Data Provider

    If you can map the audit log table to an Entity Framework DbContext (it could be the same DbContext you are auditing or a different one), then you could use the EntityFramework Data Provider instead of the SQL Data Provider.

    For example, assuming you have an AuditLog table mapped to a DbContext:

    public class AuditLog
    {
        public int Id { get; set; }
        public DateTime Date { get; set; }
        public string User { get; set; }
        public string Table { get; set; }
        public string JsonData { get; set; }
    }
    public class LogsDbContext : DbContext
    {
        public DbSet<AuditLog> AuditLogs { get; set; }
        //...
    }
    

    Then you can setup the EF data provider to log each audited entry to a record on the AuditLog table:

    Audit.Core.Configuration.Setup()
        .UseEntityFramework(config => config
            .UseDbContext<LogsDbContext>()
            .AuditTypeMapper(_ => typeof(AuditLog))
            .AuditEntityAction<AuditLog>((ev, entry, auditLog) =>
            {
                auditLog.Date = DateTime.UtcNow;
                auditLog.Table = entry.Table;
                auditLog.User = ev.Environment.UserName;
                auditLog.JsonData = entry.ToJson();
            })
            .IgnoreMatchedProperties(true));
    

    Use a custom SQL Data Provider

    Inherit from SqlDataProvider, override the Insert/InsertAsync to trigger the save for each entity entry:

    public class SingleSqlProvider : SqlDataProvider
    {
        public SingleSqlProvider(Action<ISqlServerProviderConfigurator> config) : base(config) { }
    
        public override object InsertEvent(AuditEvent auditEvent)
        {
            var efEvent = auditEvent as AuditEventEntityFramework;
            object lastId = null;
            if (efEvent != null)
            {
                foreach (var entry in efEvent.EntityFrameworkEvent.Entries)
                {
                    var clone = AuditEvent.FromJson<AuditEventEntityFramework>(auditEvent.ToJson());
                    clone.EntityFrameworkEvent.Entries.Clear();
                    clone.EntityFrameworkEvent.Entries.Add(entry);
                    lastId = base.InsertEvent(clone);
                }
            }
            else
            {
                return base.InsertEvent(auditEvent);
            }
            return lastId;
        }
    
        public async override Task<object> InsertEventAsync(AuditEvent auditEvent)
        {
            // same but Async...
        }
    }
    
    

    Then the setup could be:

    Audit.Core.Configuration.Setup()
        .UseCustomProvider(new SingleSqlProvider(config => config
            .ConnectionString("...")
            .TableName("...")));