При работе с данными мои клиенты часто спрашивали меня: «Кто изменил эту запись?». Я всегда добавляю некоторую контрольную информацию ко всем своим объектам (моделям / таблицам), чтобы узнать, кто добавил или изменил часть данных и когда она была изменена в последний раз.

Давайте начнем с создания интерфейса IEntity, который требует, чтобы мои проверяемые объекты имели следующие поля:

  • Id - это мой первичный ключ. Я стараюсь сохранить свой первичный ключ в базовой сущности, потому что все сущности всегда будут иметь первичный ключ (даже объекты отношения «многие ко многим», но обязательно определите свой составной ключ в DbContext). По соглашению Entity Framework распознает свойство с именем Id или ‹имя типа› Id в качестве первичного ключа сущности.
  • CreatedBy - свойство типа string, определяющее, кто создал эту запись.
  • CreatedOnUtc - свойство типа DateTime, которое определяет, когда была создана эта запись.
  • LastModifiedBy - свойство типа string, определяющее, кто последним обновил эту запись.
  • LastModifiedOnUtc - свойство обнуляемого типа DateTime? , который определяет, когда эта запись была обновлена ​​в последний раз.
  • IPAddress - свойство типа string, в котором хранится IP-адрес текущего пользователя, хотя для меня это находится в серой зоне морали. Я просто хочу показать здесь, как это технически реализовано.
  • IsDeleted - свойство типа bool для включения мягкого удаления для вашей сущности. Вы можете опустить это свойство, если это не требуется для вашего бизнес-сценария.
public interface IEntity
{
    object Id { get; set; }
    public string CreatedBy { get; set; }
    public DateTime CreatedOnUtc { get; set; }
    public string LastModifiedBy { get; set; }
    public DateTime? LastModifiedOnUtc { get; set; }
    public string IPAddress { get; set; }
    public bool IsDeleted { get; set; }
}

Затем создайте класс BaseEntity, реализующий IEntity. Базовая сущность помечена как абстрактный класс, потому что вам никогда не потребуется создавать ее экземпляр. Он просто служит базовым классом для ваших реальных сущностей.

public abstract class BaseEntity<T> : IEntity
{
    [Required]
    public T Id { get; set; }

    object IEntity.Id
    {
        get { return Id; }
        set { }
    }

    [Required]
    public string CreatedBy { get; set; }

    [Required]
    public DateTime CreatedOnUtc { get; set; }

    public string LastModifiedBy { get; set; }

    public DateTime? LastModifiedOnUtc { get; set; }

    public string IPAddress { get; set; }

    public bool IsDeleted { get; set; }
}

Обратите внимание на общий тип T. Идея состоит в том, чтобы позволить каждой сущности определять, какой тип первичного ключа ей нужен, то есть GUID, int и т. Д. Для простоты вы можете пропустить общий тип и использовать любой тип по вашему выбору.

Излишне говорить, что все даты указаны в формате UTC.

Позвольте вашим сущностям (или сущностям, для которых вам нужна информация аудита) наследовать от BaseEntity ‹T› при реализации IEntity. Это гарантирует, что ваша сущность всегда будет нуждаться в ваших свойствах информации аудита.

public class Book : BaseEntity<int>, IEntity
{
    [Required]
    public string Title { get; set; }

    [Required]
    public string ISBN { get; set; }

    public int Pages { get; set; }
}

Ты почти там. Пока что вы настроили свои модели для включения свойств аудита. Теперь давайте воспользуемся ChangeTracker от Entity Framework, чтобы устанавливать эти свойства при каждом сохранении. Для этого мы переопределяем метод SaveChangesAsync в DbContext приложения.

public class ApplicationDbContext : DbContext
{
    private const string appUser = "SampleApplication";
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IHttpContextAccessor httpContextAccessor = null) : base(options)
    {
        if (httpContextAccessor != null)
        {
            _httpContextAccessor = httpContextAccessor;
        }
    }

    public DbSet<Book> Books { get; set; }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        AddAuditInfo();

        return await base.SaveChangesAsync(cancellationToken);
    }

    private void AddAuditInfo()
    {
        var entities = ChangeTracker.Entries<IEntity>().Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);

        var utcNow = DateTime.UtcNow;
        var user = _httpContextAccessor?.HttpContext?.User?.Identity?.Name ?? appUser;
        var ipAddress = _httpContextAccessor?.HttpContext?.Connection?.RemoteIpAddress?.ToString();

        foreach (var entity in entities)
        {
            if (entity.State == EntityState.Added)
            {
                entity.Entity.CreatedOnUtc = utcNow;
                entity.Entity.CreatedBy = user;
            }

            if (entity.State == EntityState.Modified)
            {
                entity.Entity.LastModifiedOnUtc = utcNow;
                entity.Entity.LastModifiedBy = user;
            }

            entity.Entity.IPAddress = ipAddress;
        }
    }
}

Здесь мы устанавливаем значения для CreatedOnUtc и CreatedBy для тех сущностей, которые добавляются / создаются, и мы устанавливаем значения для LastModifiedOnUtc и LastModifiedBy для тех сущностей, которые обновляются.

Наконец, не забудьте настроить HttpContextAccessor в Startup.cs, чтобы эта служба была доступна для внедрения в качестве зависимости в ваш DbContext.

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpContextAccessor();
}

И вот, надеюсь, у вас есть простое и элегантное решение для аудита ваших моделей. Когда вы создаете или обновляете объект, информация аудита теперь будет добавлена ​​и сохранена в базе данных.

Полный исходный код доступен на моем Github.