简体   繁体   中英

EF Core - how to audit trail with value objects

I'm trying to implement an Audit Trail (track what changed, when and by whom) on a selection of classes in Entity Framework Core.

My current implementation relies on overriding OnSaveChangesAsync:

public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) 
{
    var currentUserFullName = _userService.CurrentUserFullName!;

    foreach (var entry in ChangeTracker.Entries<AuditableEntity>())  
    {
        switch (entry.State) 
        {
            case EntityState.Added:
                    entry.Entity.CreatedBy = currentUserFullName;
                    entry.Entity.Created = _dateTime.Now;
                    break;

            case EntityState.Modified:
                    var originalValues = new Dictionary<string, object?>();
                    var currentValues = new Dictionary<string, object?>();

                    foreach (var prop in entry.Properties.Where(p => p.IsModified)) 
                    {
                        var name = prop.Metadata.Name;
                        originalValues.Add(name, prop.OriginalValue);
                        currentValues.Add(name, prop.CurrentValue);
                    }
                    entry.Entity.LastModifiedBy = currentUserFullName;
                    entry.Entity.LastModified = _dateTime.Now;

                    entry.Entity.LogEntries.Add(
                        new EntityEvent(
                            _dateTime.Now,
                            JsonConvert.SerializeObject(originalValues),
                            JsonConvert.SerializeObject(currentValues),
                            currentUserFullName));
                    break;
            }
        }

        return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}

This is simple, clean and very easy to use; any entity that needs an audit trail only needs to inherit AuditableEntity .

However, there is a severe limitation to this approach: it cannot capture changes made to navigation properties.

Our entities make good use of Value Objects, such as an EmailAddress:

public class EmailAddress : ValueObjectBase
{
    public string Value { get; private set; } = null!;

    public static EmailAddress Create(string value) 
    {
        if (!IsValidEmail(value)) 
        {
            throw new ArgumentException("Incorrect email address format", nameof(value));
        }

        return new EmailAddress {
            Value = value
        };
    }
}

... Entity.cs

public EmailAddress Email { get; set; }   

... Entity EF configuration
entity.OwnsOne(e => e.Email);

... Updating
entity.Email = EmailAddress.Create("e@mail.com");

Now if the user changes an email address of this entity, the State of the Entity is never changed to modified. EF Core seems to handle ValueObjects as Navigation Properties, which are handled separately.

So I think there are a few options:

  1. Stop using ValueObjects as entity properties. We could still utilize them as entity constructor parameters, but this would cause cascading complexity to the rest of the code. It would also diminish the trust in the validity of the data.

  2. Stop using SaveChangesAsync and build our own handling for auditing. Again, this would cause further complexity in the architecture and probably be less performant.

  3. Some weird hackery to the ChangeTracker - this sounds risky but might work in theory

  4. Something else, what?

In the case where you value objects are mapped to a single column in the database (eg an email address is stored in a text column) you might be able to use converters instead:

var emailAddressConverter = new ValueConverter<EmailAddress, string>(
    emailAddress => emailAddress.Value,
    @string => EmailAddress.Create(@string));

modelBuilder.Entity<User>()
    .Property(user => user.Email)
    .HasConversion(emailAddressConverter);

This should work well with your change tracking code.

The problem when audit logging value objects was how to find the previous values for the property. The added values can be found for instance with IsOwned metadata as Jeremy suggested but previous deleted properties can not be found this way.

Deleted property can be found by first querying for all deleted objects of the same type as the added property. Then we can find matching object by comparing the foreign keys.

// Find corresponding Deleted value objects of the same type
var deleted = changeTracker.Entries().Where(a => a.State == EntityState.Deleted 
    && a.Metadata.ClrType.Equals(added.Metadata.ClrType));
:
// Foreign keys must match
deletedProp.GetContainingForeignKeys().Contains(added.Metadata.ForeignKey)

Please have look at the example project on github: https://github.com/ovirta/ValueObjectAuditing

There's probably a more efficient method, but you can enumerate the set of owned records;

entry.References.Where(r =>
    r.TargetEntry != null
    && r.TargetEntry.State == EntryState.Modified
    && r.TargetEntry.Metadata.IsOwned())

Where the parent object is either Unchanged or Modified, then track their changes too.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM