EF Core变更跟踪 - 原始值和更改后的值存在问题

5
我已经配置了.Net Core 2.0和EF Core 2.0,使用存储库模式体系结构的Net核心API。现在,我正在尝试使用EF更改跟踪器为每个保存更改实施审核日志。
问题是:每当我尝试为编辑/修改端点添加日志时,原始值和当前值保持不变,它是新更新的值。所以在这种情况下,我无法跟踪修改或更改。
以下是我的ApplicationContext文件,在其中覆盖了保存调用。
 public class ApplicationContext : DbContext
{
    public ApplicationContext(DbContextOptions options) : base(options: options) { }

    public DbSet<Item> Item { get; set; }
    public DbSet<ChangeLog> ChangeLog { get; set; }        

    public override int SaveChanges()
    {
        var modifiedEntities = ChangeTracker.Entries();

        foreach (var change in modifiedEntities)
        {
            var entityType = change.Entity.GetType().Name;
            if (entityType == "LogItem")
                continue;

            if (change.State == EntityState.Modified)
            {
                foreach (var prop in change.OriginalValues.Properties)
                {
                    var id = change.CurrentValues["Id"].ToString();

                    //here both originalValue and currentValue  are same and it's newly updated value 
                    var originalValue = change.OriginalValues[prop]?.ToString();
                    var currentValue = change.CurrentValues[prop]?.ToString();
                    if (originalValue != currentValue)
                    {
                        ChangeLog.Add(
                            new ChangeLog()
                            {
                                CreationDateTime = DateTime.Now,
                                CreationUserId = 1,
                                Log = $"Edited item named {prop.Name} in {entityType} Id {id}.",
                                OldValue = originalValue,
                                NewValue = currentValue,
                                TableName = entityType,
                                FieldName = prop.Name
                            }
                        );
                    }
                }
            }
        }
        return base.SaveChanges();
    }
}

这是我的基础代码库。
public class EntityBaseRepository<T> : IEntityBaseRepository<T> where T : class, IFullAuditedEntity, new()
{
    private readonly ApplicationContext context;

    public EntityBaseRepository(ApplicationContext context)
    {
        this.context = context;
    }

    public virtual T GetSingle(int id) => context.Set<T>().AsNoTracking().FirstOrDefault(x => x.Id == id);

    public virtual T Add(T entity) => Operations(entity: entity, state: EntityState.Added);

    public virtual T Update(T entity) => Operations(entity: entity, state: EntityState.Modified);

    public virtual T Delete(T entity) => Operations(entity: entity, state: EntityState.Deleted);

    public virtual T Operations(T entity, EntityState state)
    {
        EntityEntry dbEntityEntry = context.Entry<T>(entity);

        if (state == EntityState.Added)
        {
            entity.CreationDateTime = DateTime.UtcNow;
            entity.CreationUserId = 1;

            context.Set<T>().Add(entity);
            dbEntityEntry.State = EntityState.Added;
        }
        else if (state == EntityState.Modified)
        {
            entity.LastModificationDateTime = DateTime.UtcNow;
            entity.LastModificationUserId = 1;

            //var local = context.Set<T>().Local.FirstOrDefault(entry => entry.Id.Equals(entity.Id));
            //if (local != null)
            //{
            //    context.Entry(local).State = EntityState.Detached;
            //}

            dbEntityEntry.State = EntityState.Modified;
        }
        else if (state == EntityState.Deleted)
        {
            entity.DeletionFlag = true;
            entity.DeletionUserId = 1;
            entity.DeletionDateTime = DateTime.UtcNow;

            dbEntityEntry.State = EntityState.Modified;
        }

        return entity;
    }

    public virtual void Commit() => context.SaveChanges();

}

最后是我的带有put端点的控制器。
[Produces("application/json")]
[Route("api/Item")]
public class ItemController : Controller
{
    private readonly IItemRepository repository;
    private readonly IChangeLogRepository changeLogRepository;
    private readonly IMapper mapper;

    public ItemController(IItemRepository repository, IChangeLogRepository _changeLogRepository, IMapper mapper)
    {
        this.repository = repository;
        this.changeLogRepository = _changeLogRepository;
        this.mapper = mapper;
    }

    [HttpPut]
    public IActionResult Put([FromBody]ItemDto transactionItemDto)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        if (transactionItemDto.Id <= 0)
        {
            return new NotFoundResult();
        }

        Item item = repository.GetSingle(transactionItemDto.Id); //find entity first

        if (item == null)
        {
            return new NotFoundResult();
        }

        //map all the properties and commit
        var entity = mapper.Map<Item>(transactionItemDto);
        var updatedItem = repository.Update(entity);
        repository.Commit();

        return new OkObjectResult(mapper.Map<Item, ItemDto>(source: updatedItem));
    }
}

我不确定我的做法是否有错误,我在Stack Overflow上尝试查找此问题,但没有运气。 非常感谢您的帮助。

3个回答

4
我没有使用仓储模式,但我已经为EF Core 2.1实现了一个非常类似的审计日志。我循环遍历实体框架更改跟踪器正在跟踪的更改列表,并记录它们。
我注意到当我想要更新实体时,有两种方法可以做到。一种是从数据库中读取现有实体,重新分配变量,然后保存它。第二种方法是简单地创建一个对象,将其附加到数据库上下文,并将要更新的属性设置为修改状态。当我这样做时,我的审计不会对原始值起作用,因为原始值实际上从数据库中没有读取。
示例:
//auditing works fine
var myEntity = await db.MyEntity.FindAsync(entityId);
myEntity.Property = newValue;
await db.SaveChangesAsync();

//auditing can't track the old value
var myEntity = new MyEntity();
db.Attach(myEntity);
myEntity.Property = newValue;
await db.SaveChangesAsync();

下面是我审核代码的重要部分的示例

foreach (var entity in db.ChangeTracker.Entries())
{
    if(entity.State == EntityState.Detached || entity.State == EntityState.Unchanged)
    {
        continue;
    }

    var audits = new List<Audit>();

    //the typeId is a string representing the primary keys of this entity.
    //this will not be available for ADDED entities with generated primary keys, so we need to update those later
    string typeId;

    if (entity.State == EntityState.Added && entity.Properties.Any(prop => prop.Metadata.IsPrimaryKey() && prop.IsTemporary))
    {
        typeId = null;
    }
    else
    {
        var primaryKey = entity.Metadata.FindPrimaryKey();
        typeId = string.Join(',', primaryKey.Properties.Select(prop => prop.PropertyInfo.GetValue(entity.Entity)));
    }

    //record an audit for each property of each entity that has been changed
    foreach (var prop in entity.Properties)
    {
        //don't audit anything about primary keys (those can't change, and are already in the typeId)
        if(prop.Metadata.IsPrimaryKey() && entity.Properties.Any(p => !p.Metadata.IsPrimaryKey()))
        {
            continue;
        }

        //ignore values that won't actually be written
        if(entity.State != EntityState.Deleted && entity.State != EntityState.Added && prop.Metadata.AfterSaveBehavior != PropertySaveBehavior.Save)
        {
            continue;
        }

        //ignore values that won't actually be written
        if (entity.State == EntityState.Added && prop.Metadata.BeforeSaveBehavior != PropertySaveBehavior.Save)
        {
            continue;
        }

        //ignore properties that didn't change
        if(entity.State == EntityState.Modified && !prop.IsModified)
        {
            continue;
        }

        var audit = new Audit
        {
            Action = (int)entity.State,
            TypeId = typeId,
            ColumnName = prop.Metadata.SqlServer().ColumnName,
            OldValue = (entity.State == EntityState.Added || entity.OriginalValues == null) ? null : JsonConvert.SerializeObject(prop.OriginalValue),
            NewValue = entity.State == EntityState.Deleted ? null : JsonConvert.SerializeObject(prop.CurrentValue)
        };
    }

    //Do something with audits
}

尝试了这个,但是没有起作用。我的意思是审计仍然采用新值或原始值和当前值。 - Bharat
谢谢你提供审计日志代码的示例,我可以使用它。 - Bharat
1
@Bharat,你看到我提到的关于在设置实体时如何从数据库加载数据的部分了吗?当你只调用context.Update(...)时,它仅将所有属性标记为“已更新”,而不跟踪旧值是什么。 - Matt H
是的,我也检查过了,并尝试按照您的建议加载它。但最后问题出在我使用的存储库方法上,它是一个GetSingle()。无论如何,谢谢您的时间。 - Bharat

2

有两种方法可供选择:

var entry = _dbContext.Attach(entity); 
var updated = entry.CurrentValues.Clone(); 
entry.Reload(); 
entry.CurrentValues.SetValues(updated); 
entry.State = EntityState.Modified;
db.SaveChanges();

或者简单地执行以下操作:

var persondb = db.Persons.Find(person.id);
db.Entry(persondb).CurrentValues.SetValues(person); 
db.SaveChanges();

1
我认为我看到了您代码的问题。在您的控制器中:
    //map all the properties and commit
    var entity = mapper.Map<Item>(transactionItemDto);
    var updatedItem = repository.Update(entity);
    repository.Commit();

在这段代码中,你将DTO映射到一个新的Item实例中。这个新的Item实例不知道当前数据库的值,这就是为什么你会看到OriginalValue和CurrentValue都是相同的新值。
如果你重复使用在这一行获取的Item item变量:
Item item = repository.GetSingle(transactionItemDto.Id); //find entity first

请注意,您需要使用带有跟踪的实体来获取它,而不是使用AsNoTracking方法获取您的存储库中的单个实体。如果您使用该项(该项现在具有原始/当前数据库值),并像这样将transactionItemDto属性映射到它上面:
var entityToUpdate = mapper.Map<ItemDto, Item>(transactionItemDto);

然后,当您调用repository.Update方法并传递entityToUpdate时,我相信您将看到正确的更新前/后的值。

......

旧(错误的)答案我最初发布的是: 在您的ApplicationContext代码中,您有以下循环:

foreach (var prop in change.OriginalValues.Properties)

我认为这就是导致你的原始值和当前值相同的原因,因为你正在循环遍历原始值的属性。尝试将该循环更改为:
foreach (var prop in change.Properties)

然后,尝试通过prop变量读取每个属性的值,如下所示:
var currentValue = prop.CurrentValue;
var originalValue = prop.OriginalValue;

编辑:啊——我现在明白了,你在代码中试图从change.OriginalValues集合中读取原始值,所以我不认为这会有帮助。


我仍然只获取新值,我已经更新了我的代码并直接从变化中使用属性。 - Bharat
1
是的,我就怕这个,抱歉 - 请看我回答底部的编辑 - 我一开始没有注意到。 - G_P
没问题。谢谢帮忙。我会尝试其他方法。 - Bharat
@Bharat 我又做了一次更新 - 我想我找到了问题。 - G_P
你是对的,我的朋友。问题出在基础仓库的GetSingle()方法上,我使用了ASNoTracking()。谢谢你抽出时间来调查这个问题。这真的帮了我很多。 - Bharat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接