首页 > 解决方案 > EF Core - 如何使用值对象审计跟踪

问题描述

我正在尝试对 Entity Framework Core 中的一系列类实施审计跟踪(跟踪更改的内容、时间和人员)。

我当前的实现依赖于重写 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);
}

这简单、干净且非常易于使用;任何需要审计跟踪的实体只需要继承AuditableEntity.

但是,这种方法有一个严重的限制:它无法捕获对导航属性所做的更改。

我们的实体充分利用了值对象,例如电子邮件地址:

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");

现在,如果用户更改了该实体的电子邮件地址,则该实体的状态永远不会更改为已修改。EF Core 似乎将 ValueObjects 作为导航属性处理,它们是单独处理的。

所以我认为有几个选择:

  1. 停止使用 ValueObjects 作为实体属性。我们仍然可以将它们用作实体构造函数参数,但这会导致其余代码的级联复杂性。它还会降低对数据有效性的信任。

  2. 停止使用 SaveChangesAsync 并构建我们自己的审核处理。同样,这会导致架构更加复杂,并且可能会降低性能。

  3. ChangeTracker 的一些奇怪的黑客行为 - 这听起来很冒险,但理论上可能有效

  4. 还有什么,什么?

标签: c#entity-framework-core

解决方案


如果您将值对象映射到数据库中的单个列(例如,电子邮件地址存储在文本列中),您可能可以使用转换器:

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

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

这应该适用于您的更改跟踪代码。


推荐阅读