С плюсами и минусами.

В этой статье мы рассмотрим 3 различных варианта, которые разработчики могут использовать для реализации функций аудита для сущностей предметной области (CreatedAt, ModifiedAt свойств/столбцов) в своих приложениях с помощью Entity Framework Core. Эта функциональность широко используется в приложениях .NET и сообщает, когда объект был создан или изменен (но не сообщает вам, какое свойство было изменено или старое/новое значение этого свойства).

Определение примера

Давайте определим пример, который мы позже расширим функциональностью аудита.

Вот простая модель домена User:

public class User
{
    public int Id { get; set; }
    public string Email { get; set; }
}

Теперь нам нужно реализовать DbContext:

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options)
    {
    }

    public DbSet<User> Users { get; set; }
}

Теперь давайте реализуем репозиторий для User сущностей с двумя основными операциями для сохранения пользователя и изменения электронной почты:

public interface IUsersRepository
{
    Task Create(User user);
    Task ChangeEmail(int userId, string email);
}

public class UsersRepository : IUsersRepository
{
    private AppDbContext _appDbContext;

    public UsersRepository(AppDbContext appDbContext)
        => _appDbContext = appDbContext;

   public async Task Create(User user)
   {
      _appDbContext.Add(user);

      await _appDbContext.SaveChangesAsync();
   }

   public async Task ChangeEmail(int userId, string email)
   {
      var user = _appDbContext.Users.SingleOrDefault(x => x.Id == userId);
      user.Email = email;

      await _appDbContext.SaveChangesAsync();
   }
}

Последний шаг — зарегистрировать все компоненты в классе Program:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IUsersRepository, UsersRepository>();

builder.Services.AddDbContext<AppDbContext>((serviceProvider, options) =>
{
    options.UseInMemoryDatabase("inMemoryDatabase");
});

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

👉 Поскольку вы дочитали до этого места, вам интересна тема репозиториев. Так что позвольте мне порекомендовать еще одну мою статью на эту тему:



#1 Наивная реализация аудита

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

Первым шагом может быть расширение модели User двумя новыми столбцами CreatedAt и ModifiedAt:

public class User
{
    public int Id { get; set; }
    public string Email { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? ModifiedAt { get; set; }
}

После расширения модели новыми столбцами мы можем внести изменения в репозиторий. Метод Create должен установить для свойства CreatedAt текущую дату, а метод ChangeEmail должен установить ModifiedAt.

public class UsersRepository : IUsersRepository
{
    private AppDbContext _appDbContext;

    public UsersRepository(AppDbContext appDbContext)
       => _appDbContext = appDbContext;

    public async Task Create(User user)
    {
       user.CreatedAt = DateTime.UtcNow; //Set the current date

       _appDbContext.Add(user);

       await _appDbContext.SaveChangesAsync();
    }

    public async Task ChangeEmail(int userId, string email)
    {
       var user = _appDbContext.Users.SingleOrDefault(x => x.Id == userId);
       user.Email = email;

       user.ModifiedAt = DateTime.UtcNow; //Set the current date 

       await _appDbContext.SaveChangesAsync();
    }
}

Этот подход решает нашу проблему сейчас, но он не является действительно расширяемым и ремонтопригодным. У него как минимум две проблемы:

  • Когда возникает необходимость сделать то же самое для другого объекта, например Product, разработчик может назвать свойства по-разному: ChangedOn, LastEdited, LastEditedUtc и т. д. Это приведет к несогласованности имен свойств, а также имен столбцов в базе данных.
  • Каждый метод репозитория отвечает за установку свойств аудита на некоторое значение. Это может привести к несоответствиям, т.е. вы можете установить UTC в одном репозитории и только местное время в другом. Кроме того, легко случайно вообще пропустить инициализацию.

Давайте посмотрим, как мы можем преодолеть эти проблемы дальше.

#2 Переопределение метода SaveChanges

Реализация аудита должна быть намного проще в сопровождении, чем приведенный выше пример для реальных проектов. Для этого нам нужно ввести интерфейс IAuditableEntity, который будет включать свойства аудита:

public interface IAuditableEntity
{
    public DateTime CreatedAt { get; set; }
    public DateTime? ModifiedAt { get; set; }
}

Обратите внимание, что это не обязательно должен быть интерфейс, вы также можете использовать абстрактный класс. Извлечение свойств в интерфейс или абстрактный класс позволит нам сделать их имена согласованными во всех объектах предметной области.

Следующим шагом является реализация этого интерфейса для User и любых других сущностей, которые должны иметь столбцы аудита в базе данных:

public class User : IAuditableEntity
{
    public int Id { get; set; }
    public string Email { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? ModifiedAt { get; set; }
}

Последний прием — переопределить метод SaveChangesAsync в классе AppDbContext и реализовать его следующим образом:

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options)
    {
    }

    public DbSet<User> Users { get; set; }

    public override Task<int> SaveChangesAsync
        (CancellationToken cancellationToken = default)
    {
        var entries = ChangeTracker.Entries<IAuditableEntity>();

        foreach (var entry in entries)
        {
            if (entry.State == EntityState.Added)
            {
                entry.Entity.CreatedAt = DateTime.UtcNow;
            }

            if (entry.State == EntityState.Modified)
            {
                entry.Entity.ModifiedAt = DateTime.UtcNow;
            }
        }

        return base.SaveChangesAsync(cancellationToken);
    }
}

Реализация довольно проста. При сохранении изменений он извлекает все объекты, реализующие интерфейс IAuditableEntity, и устанавливает свойства, подлежащие аудиту, на основе состояния объекта перед сохранением изменений в базе данных.

Обратите внимание, что это единственное место в приложении, где можно задать свойства аудита, больше нет необходимости делать это в репозиториях. Таким образом, в основном все проблемы, которые у нас были с наивной реализацией, решены. Однако есть еще один элегантный способ, который мы можем использовать вместо переопределения метода SaveChangesAsync.

# 3 Реализация перехватчика

Перехватчики Entity Framework Core можно использовать для реализации различных задач, таких как изменение SQL-запроса перед его отправкой в ​​базу данных, кэширование и, конечно же, аудит.

Первое, что нужно сделать, это разработать перехватчик с той же логикой, которая была у нас ранее в переопределенном методе SaveChangesAsync:

public class AuditableEntitiesInterceptor : SaveChangesInterceptor
{
    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        if (eventData.Context is null)
        {
            return base.SavingChangesAsync(eventData, result, cancellationToken);
        }

        var entries = eventData.Context.ChangeTracker.Entries<IAuditableEntity>();

        foreach (var entry in entries)
        {
            if (entry.State == EntityState.Added)
            {
                entry.Entity.CreatedAt = DateTime.UtcNow;
            }

            if (entry.State == EntityState.Modified)
            {
                entry.Entity.ModifiedAt = DateTime.UtcNow;
            }
        }

        return base.SavingChangesAsync(eventData, result, cancellationToken);
    }
}

Обратите внимание, что наш пользовательский перехватчик унаследован от SaveChangesInterceptor, предоставленного EF Core.

Также помните, что базовый класс предоставляет два метода с очень похожими именами SavingChangesAsync и SavedChangesAsync. В целях аудита нам нужно использовать метод SavingChangesAsync, потому что он вызывается перед сохранением изменений в базе данных.

Затем мы должны зарегистрировать перехватчик в DI-контейнере и прикрепить его к классу DbContext:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IUsersRepository, UsersRepository>();

builder.Services.AddSingleton<AuditableEntitiesInterceptor>(); //DI registration

builder.Services.AddDbContext<AppDbContext>((serviceProvider, options) =>
{
    var interceptor = serviceProvider
          .GetService<AuditableEntitiesInterceptor>();

    options
     .UseInMemoryDatabase("inMemoryDatabase")
     .AddInterceptors(interceptor); //Attaching it to the DB context
});

Вот и все. Для меня подход с перехватчиком лучше с точки зрения чистого кода, потому что логика аудита инкапсулирована в отдельный класс, и ваш AppDbContext не будет расти как в примере №2.

Заключение

Мы рассмотрели три различных способа реализации основных функций аудита в проектах .NET с помощью Entity Framework Core. Прежде чем применять одно из доступных решений, посмотрите, что лучше всего работает в вашем конкретном случае.

Спасибо за прочтение. Если вам понравилось то, что вы прочитали, ознакомьтесь с этой историей ниже:



Кроме того, подумайте о том, чтобы стать Medium Member.

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 💰 Бесплатный курс собеседования по программированию ⇒ Просмотреть курс
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу