简体   繁体   English

EF Core - 如何使用值对象审计跟踪

[英]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.我正在尝试对 Entity Framework Core 中的选择类实施审计跟踪(跟踪更改的内容、时间和更改者)。

My current implementation relies on overriding OnSaveChangesAsync:我当前的实现依赖于覆盖 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 .任何需要审计跟踪的实体只需要继承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:我们的实体很好地利用了值对象,例如 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. EF Core 似乎将 ValueObjects 作为导航属性处理,这些属性是单独处理的。

So I think there are a few options:所以我认为有几个选择:

  1. Stop using ValueObjects as entity properties.停止使用 ValueObjects 作为实体属性。 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.停止使用 SaveChangesAsync 并构建我们自己的审计处理。 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 ChangeTracker 的一些奇怪的技巧 - 这听起来有风险,但理论上可能有效

  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.例如,可以使用 Jeremy 建议的 IsOwned 元数据找到添加的值,但无法通过这种方式找到以前删除的属性。

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请查看 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.如果父对象未更改或已修改,则也跟踪它们的更改。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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