EF Core 更改跟踪 - 原始值和更改值的问题
EF Core change tracking - issue with original values and altered values
我的 Net Core API 配置了 .net Core 2.0 和 EF Core 2.0。它包含存储库模式架构。
现在,我正在尝试使用 EF 更改跟踪器为每个保存更改实施审核日志。
My Issue : Whenever I tries to add a log for edit/modification endpoint, the original value and current value remain same and it's newly updated value. so in that way I am not able to track the modification or a change.
这是我的 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();
}
最后是我的带有放置终点的控制器。
[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));
}
}
我不确定我哪里做错了,我试着在 SO 中检查这个案例,但没有成功。
任何帮助将不胜感激,谢谢。
我想我看到了您的代码的问题。在您的控制器中:
//map all the properties and commit
var entity = mapper.Map<Item>(transactionItemDto);
var updatedItem = repository.Update(entity);
repository.Commit();
在该代码中,您使用 DTO 并将其映射到 Item 的新实例。 Item 的新实例知道 nothing 当前数据库值,这就是为什么您看到 OriginalValue 和 CurrentValue 的新值相同。
如果您重复使用在这一行中获得的 Item 项目变量:
Item item = repository.GetSingle(transactionItemDto.Id); //find entity first
请注意,您需要获取启用跟踪的实体,而不是您的存储库 GetSingle 如何使用 AsNoTracking 进行跟踪。如果您使用该项目(现在具有 original/current 数据库值)并将您的 transactionItemDto 属性映射到它,如下所示:
var entityToUpdate = mapper.Map<ItemDto, Item>(transactionItemDto);
然后,当您调用 repository.Update 方法并将其传递给 entityToUpdate 时,我相信您会看到正确的 before/after 值。
。
.
.
.
旧的(错误的)答案我最初发布:
在您的 ApplicationContext 代码中,您有以下循环
foreach (var prop in change.OriginalValues.Properties)
我相信这就是导致您的原始 value/current 值相同的原因,因为您正在遍历原始值属性。尝试将该循环更改为:
foreach (var prop in change.Properties)
然后,尝试通过 prop 变量读取每个 属性 的值,如下所示:
var currentValue = prop.CurrentValue;
var originalValue = prop.OriginalValue;
编辑:啊 - 我现在看到在您的代码中您正试图从 change.OriginalValues 集合中读取原始值,所以我认为这不会有帮助。
我没有使用存储库模式,但我已经为 EF Core 2.1 实现了一个非常相似的审计日志。我遍历 entity framework 更改跟踪器正在跟踪的更改列表,并记录它们。
我注意到,当我想更新一个实体时,有两种方法可以做到。一种是我从数据库中读取现有实体,重新分配变量,然后保存。第二种方式是简单地创建一个对象,将其附加到数据库上下文,并将我要更新的 属性 设置为已修改状态。当我这样做时,我的审计将无法对原始值起作用,因为原始值实际上从未从数据库中读取过。
示例:
//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
}
有两种方法:
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();
我的 Net Core API 配置了 .net Core 2.0 和 EF Core 2.0。它包含存储库模式架构。
现在,我正在尝试使用 EF 更改跟踪器为每个保存更改实施审核日志。
My Issue : Whenever I tries to add a log for edit/modification endpoint, the original value and current value remain same and it's newly updated value. so in that way I am not able to track the modification or a change.
这是我的 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();
}
最后是我的带有放置终点的控制器。
[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));
}
}
我不确定我哪里做错了,我试着在 SO 中检查这个案例,但没有成功。 任何帮助将不胜感激,谢谢。
我想我看到了您的代码的问题。在您的控制器中:
//map all the properties and commit
var entity = mapper.Map<Item>(transactionItemDto);
var updatedItem = repository.Update(entity);
repository.Commit();
在该代码中,您使用 DTO 并将其映射到 Item 的新实例。 Item 的新实例知道 nothing 当前数据库值,这就是为什么您看到 OriginalValue 和 CurrentValue 的新值相同。
如果您重复使用在这一行中获得的 Item 项目变量:
Item item = repository.GetSingle(transactionItemDto.Id); //find entity first
请注意,您需要获取启用跟踪的实体,而不是您的存储库 GetSingle 如何使用 AsNoTracking 进行跟踪。如果您使用该项目(现在具有 original/current 数据库值)并将您的 transactionItemDto 属性映射到它,如下所示:
var entityToUpdate = mapper.Map<ItemDto, Item>(transactionItemDto);
然后,当您调用 repository.Update 方法并将其传递给 entityToUpdate 时,我相信您会看到正确的 before/after 值。
。 . . .
旧的(错误的)答案我最初发布: 在您的 ApplicationContext 代码中,您有以下循环
foreach (var prop in change.OriginalValues.Properties)
我相信这就是导致您的原始 value/current 值相同的原因,因为您正在遍历原始值属性。尝试将该循环更改为:
foreach (var prop in change.Properties)
然后,尝试通过 prop 变量读取每个 属性 的值,如下所示:
var currentValue = prop.CurrentValue;
var originalValue = prop.OriginalValue;
编辑:啊 - 我现在看到在您的代码中您正试图从 change.OriginalValues 集合中读取原始值,所以我认为这不会有帮助。
我没有使用存储库模式,但我已经为 EF Core 2.1 实现了一个非常相似的审计日志。我遍历 entity framework 更改跟踪器正在跟踪的更改列表,并记录它们。
我注意到,当我想更新一个实体时,有两种方法可以做到。一种是我从数据库中读取现有实体,重新分配变量,然后保存。第二种方式是简单地创建一个对象,将其附加到数据库上下文,并将我要更新的 属性 设置为已修改状态。当我这样做时,我的审计将无法对原始值起作用,因为原始值实际上从未从数据库中读取过。
示例:
//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
}
有两种方法:
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();