Отслеживание изменений EF Core — проблема с исходными и измененными значениями

У меня есть Net core API, настроенный на .net core 2.0 и EF core 2.0. он содержит архитектуру шаблона репозитория.

Теперь я пытаюсь реализовать журнал аудита для каждого изменения сохранения, используя средство отслеживания изменений 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();

}

И, наконец, мой контроллер с конечной точкой для ввода.

[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, но не повезло. любая помощь будет оценена, спасибо.


person Bharat    schedule 09.10.2019    source источник


Ответы (2)


Кажется, я вижу проблему с вашим кодом. В вашем контроллере:

    //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 = repository.GetSingle(transactionItemDto.Id); //find entity first

Обратите внимание, что вам нужно будет получить объект с отслеживанием, а не как ваш репозиторий GetSingle делает это с AsNoTracking. Если вы используете этот элемент (который теперь имеет исходные/текущие значения базы данных) и сопоставляете с ним свои свойства transactionItemDto следующим образом:

var entityToUpdate = mapper.Map<ItemDto, Item>(transactionItemDto);

Затем, когда вы вызываете свой метод репозитория. Обновление, передавая его 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, поэтому я не думаю, что это поможет.

person G_P    schedule 09.10.2019
comment
Я все еще получаю только новые значения, я обновил свой код и использовал свойства непосредственно из изменений. - person Bharat; 09.10.2019
comment
да, я этого боялся, извините - см. мою правку внизу моего ответа - сначала я этого не заметил - person G_P; 09.10.2019
comment
Конечно. нет проблем. Спасибо за помощь. Я попробую что-нибудь еще. - person Bharat; 09.10.2019
comment
@Bharat Я сделал еще одно обновление - кажется, я нашел проблему - person G_P; 10.10.2019
comment
вы правы, мой друг, проблема была с этим методом GetSingle() в базовом репо, где я использовал этот ASNoTracking(). Благодарим вас за то, что уделили время изучению этого вопроса. Это действительно помогло мне. - person Bharat; 10.10.2019

Я не использую шаблон репозитория, но реализовал очень похожий журнал аудита для 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
}
person Matt H    schedule 09.10.2019
comment
попробовал это, но не работает. Я имею в виду, что аудит по-прежнему принимает новое значение или как исходное, так и текущее значение. - person Bharat; 09.10.2019
comment
спасибо за пример вашего кода журнала аудита, я могу его использовать. - person Bharat; 09.10.2019
comment
@Bharat, вы видели ту часть, о которой я упоминал, о том, как загружать данные из базы данных при настройке объекта? Когда вы просто вызываете context.Update(...), он просто помечает все свойства как обновленные, не отслеживая, какими были старые значения. - person Matt H; 10.10.2019
comment
да, я тоже проверил это и попытался загрузить его, как вы предложили. но, наконец, проблема была в методе репозитория, который я использую, и это GetSingle(). в любом случае спасибо за ваше время. - person Bharat; 10.10.2019