问题
I have Net core API configured with .net core 2.0 and EF core 2.0. it contains repository pattern architecture.
Now, I am trying to implement Audit log for each save change using EF change tracker.
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.
Here is my ApplicationContext file where I have overridden save call.
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();
}
}
Here is my base repository.
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();
}
And Lastly my controller with end point for 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));
}
}
I am not sure where I am doing any mistake, I tried to check this case in SO, but no luck. any help will be appreciated, thanks.
回答1:
I think I see the issue with your code. In your controller:
//map all the properties and commit
var entity = mapper.Map<Item>(transactionItemDto);
var updatedItem = repository.Update(entity);
repository.Commit();
In that code you are taking your DTO and mapping it to a new instance of Item. That new instance of Item knows nothing of the current database values, that is why you are seeing the same new values for both OriginalValue and CurrentValue.
If you reuse the Item item variable that you get in this line:
Item item = repository.GetSingle(transactionItemDto.Id); //find entity first
Note, you'll need to get the entity with tracking on however, vs how your repository GetSingle does it with AsNoTracking. If you use that item (which now has the original/current database values) and map your transactionItemDto properties onto it like this:
var entityToUpdate = mapper.Map<ItemDto, Item>(transactionItemDto);
Then, when you call your repository.Update method passing it entityToUpdate, I believe you'll see the correct before/after values.
. . . .
Old (wrong) answer I originally posted: In your ApplicationContext code you have the following loop
foreach (var prop in change.OriginalValues.Properties)
I believe that is what is causing your original value/current values to be the same because you are looping over the original values properties. Try changing that loop to:
foreach (var prop in change.Properties)
Then, try reading the values off each property via the prop variable like so:
var currentValue = prop.CurrentValue;
var originalValue = prop.OriginalValue;
EDIT: Ah - I see now that in your code you are trying to read the original value from the change.OriginalValues collection, so I don't think this is going to help.
回答2:
I am not using the repository pattern, but I have implemented a very similar audit log for EF Core 2.1. I loop through the list of changes that are being tracked by the entity framework change tracker, and log them.
What I have noticed is that when I want to update an entity, there are two ways to do it. One is that I read the existing entity from the database, reassign the variable, then save it. The second way is to simply create an object, attach it to the database context, and set the property I want to update to the modified state. When I do this, my auditing won't work for the original value, since the original value was never actually read from the database.
examples:
//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();
Here is the important bit of my auditing code for example
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
}
来源:https://stackoverflow.com/questions/58299469/ef-core-change-tracking-issue-with-original-values-and-altered-values