Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

EF Core how to implement audit log of changes to value objects

I am using EF Core/.NET Core 2.1, and following DDD. I need to implement an audit log of all changes to my entities, and have done so using code from this blog post (relevant code from this post included below). This code works and tracks changes to any properties, however when it logs changes to my value objects, it only lists the new values, and no old values.

Some code:

public class Item
{
    protected Item(){}

    //truncated for brevity

    public Weight Weight { get; private set; }
}

public class Weight : ValueObject<Weight>
{
    public WeightUnit WeightUnit { get; private set; }
    public double WeightValue { get; private set; }

    protected Weight() { }

    public Weight(WeightUnit weightUnit, double weight)
    {
        this.WeightUnit = weightUnit;
        this.WeightValue = weight;
    }
}

and the audit tracking code from my context class

public class MyContext : DbContext
{
    //truncated for brevity

    public override int SaveChanges(bool acceptAllChangesOnSuccess)
    {
        var auditEntries = OnBeforeSaveChanges();
        var result = base.SaveChanges(acceptAllChangesOnSuccess);
        OnAfterSaveChanges(auditEntries);
        return result;
    }

    private List<AuditEntry> OnBeforeSaveChanges()
    {
        if (!this.AuditingAndEntityTimestampingEnabled)
        {
            return null;
        }

        ChangeTracker.DetectChanges();
        var auditEntries = new List<AuditEntry>();
        foreach (var entry in ChangeTracker.Entries())
        {
            if (entry.Entity is Audit || entry.State == EntityState.Detached || entry.State == EntityState.Unchanged)
            {
                continue;
            }

            var auditEntry = new AuditEntry(entry)
            {
                TableName = entry.Metadata.Relational().TableName
            };
            auditEntries.Add(auditEntry);

            foreach (var property in entry.Properties)
            {
                if (property.IsTemporary)
                {
                    // value will be generated by the database, get the value after saving
                    auditEntry.TemporaryProperties.Add(property);
                    continue;
                }

                string propertyName = property.Metadata.Name;
                if (property.Metadata.IsPrimaryKey())
                {
                    auditEntry.KeyValues[propertyName] = property.CurrentValue;
                    continue;
                }

                switch (entry.State)
                {
                    case EntityState.Added:
                        auditEntry.NewValues[propertyName] = property.CurrentValue;
                        break;

                    case EntityState.Deleted:
                        auditEntry.OldValues[propertyName] = property.OriginalValue;
                        break;

                    case EntityState.Modified:
                        if (property.IsModified)
                        {
                            auditEntry.OldValues[propertyName] = property.OriginalValue;
                            auditEntry.NewValues[propertyName] = property.CurrentValue;
                        }
                        break;
                }
            }
        }

        // Save audit entities that have all the modifications
        foreach (var auditEntry in auditEntries.Where(_ => !_.HasTemporaryProperties))
        {
            Audits.Add(auditEntry.ToAudit());
        }

        // keep a list of entries where the value of some properties are unknown at this step
        return auditEntries.Where(_ => _.HasTemporaryProperties).ToList();
    }

}

Here is a screenshot of how the audit changes persist to the database. The non-value object properties on Item have their old/new values listed, where changes to value objects only list the new values:

enter image description here

Is there a way to get the previous values of the value objects?

UPDATE:

So, the reason the OldValues column is null for changes to my value objects is due to the State of the value object being Added when it has been changed. I added a call to the IsOwned() method to the switch statement, and try to grab the property.OriginalValue within like this:

                        case EntityState.Added:

                        if (entry.Metadata.IsOwned())
                        {
                            auditEntry.OldValues[propertyName] = property.OriginalValue;
                        }
                        auditEntry.NewValues[propertyName] = property.CurrentValue;
                        break;

However, this simply logs the current value that the value object is being updated to.

enter image description here

So the question still stands - is there any way to get the previous value of a value object using the EF Core ChangeTracker, or do I need to re-think my use of DDD Value Objects due to my audit requirement?

like image 281
G_P Avatar asked Jul 16 '18 19:07

G_P


People also ask

How does EF core detect changes?

EF Core change tracking works best when the same DbContext instance is used to both query for entities and update them by calling SaveChanges. This is because EF Core automatically tracks the state of queried entities and then detects any changes made to these entities when SaveChanges is called.

Is EF core faster than EF6?

EF Core 6.0 performance is now 70% faster on the industry-standard TechEmpower Fortunes benchmark, compared to 5.0. This is the full-stack perf improvement, including improvements in the benchmark code, the . NET runtime, etc. EF Core 6.0 itself is 31% faster executing queries.

How do you store audit logs?

As a general rule, storage of audit logs should include 90 days “hot” (meaning you can actively search/report on them with your tools) and 365 days “cold” (meaning log data you have backed up or archived for long-term storage). Store logs in an encrypted format. See our post on Encryption Policies for more information.

Should I use EF6 or EF core?

Keep using EF6 if the data access code is stable and not likely to evolve or need new features. Port to EF Core if the data access code is evolving or if the app needs new features only available in EF Core. Porting to EF Core is also often done for performance.


1 Answers

isstead of

auditEntry.OldValues[propertyName] = property.OriginalValue;

use

auditEntry.OldValues[propertyName] = entry.GetDatabaseValues().GetValue<object>(propertyName).ToString();
like image 66
mkavici Avatar answered Sep 18 '22 15:09

mkavici