Перехоплювачі в Entity Framework Core
Під час розробки програмного забезпечення, інколи виникає потреба автоматично модифікувати певні дії, для прикладу автоматично додавати заголовки до HTTP запитів від клієнта до сервера, виконувати певну логіку до чи після певної дії користувача і т.д., зберігати інформацію про ці дії, тощо. Це все можна робити напряму в тій частині коду яка вам потрібна, але якщо у вас великий проект, то це буде його засмічувати та знову ж таки це буде дублювання коду, а це те що ми не хочемо мати. Так от, щоб автоматизувати даний процес використовують перехоплювачів (interceptors). Що таке перехоплювач (interceptor)? - це механізм, який дозволяє перехоплювати певні дії, має можливість вносити в них зміни і повернути результат. Перехоплювачі можуть використовуватись для реалізації різних задач, для прикладу логування, перевірка на коректність, зміна значень, і.д..
Які перехоплювачі існують в Entity Framework Core?
В EF-Core існують декілька перехоплювачів, всі вони наслідують інтерфейс IIinterceptor
який використовувався як базовий для всіх інших інтерфейсів перехоплювачів.
ISaveChangesInterceptor
- використовується для перехоплення операції збереження даних.IDbCommandInterceptor
- використовується для перехоплення команди до БД і з можливістю їх змінювати.IDbConnectionInterceptor
- використовується для перехоплення операцій, пов’язаних із з’єднанням до БД DbConnection.IDbTransactionInterceptor
- використовується для перехоплення операцій, пов’язаних із DbTransaction.
Ми будемо використовувати ISaveChangesInterceptor
із за допомогою якого будемо зберігати додаткову інформацію в БД кожний раз як тільки дані в наших таблицях будуть оновлюватися або створюватися.
Перехоплювачі можуть бути зареєстровані в Entity Framework Core за допомогою методу AddInterceptors
, який викликається в методі OnConfiguring
вашого контексту БД DbContext
. За допомогою цього методу можна зареєструвати один або кілька перехоплювачів.
1
2
3
4
5
6
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.AddInterceptors(_entitySaveChangesInterceptor);
base.OnConfiguring(optionsBuilder);
}
Створення SaveChangesInterceptor перехоплювача
Для початку уявимо, що перед нами стоїть задача створити програму для збереження та редагування інформації про користувачів а також ми повинні мати інформацію хто останній робив зміни в нашій таблиці. Отже створимо просту таблицю Users
яка буде містити декілька колонок.
1
2
3
4
5
6
7
8
public class User
{
public int Id { get; set; }
public string FirstName { get; set; } = null!;
public string LastName { get; set; } = null!;
}
А також напишемо конфігурацію для нашої таблиці:
1
2
3
4
5
6
7
8
9
10
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.HasKey(x => x.Id);
builder.HasIndex(x => x.Id).IsUnique();
builder.Property(x => x.FirstName).HasMaxLength(250).IsRequired();
builder.Property(x => x.LastName).HasMaxLength(250).IsRequired();
}
}
Ми створили клас User
який буде представляти структуру нашої таблиці, і додали конфігурацію в якій сказали що Id
це наш первинний ключ, що він має бути унікальним, а також що у нас є ще дві колонки FirstName
та LastName
з максимальною довжиною в 250 символів і що вони обов’язкові для заповнення.
Наступним кроком нам потрібно створити ще один клас який буде представляти додаткові колонки для збереження історії змін.
1
2
3
4
5
6
7
8
public class ChangeTrackerEntity
{
public DateTime CreatedDate { get; set; }
public DateTime ModifiedDate { get; set; }
public int ModifierId { get; set; }
}
і також тепер потрібно щоб клас User
успадковував ChangeTrackerEntity
:
1
2
3
4
public class User : ChangeTrackerEntity
{
...
}
Отже ми створили клас який представляє структуру таблиці User
і також написали конфігурацію, прийшов час реалізувати перехоплювача. Для цього створимо клас новий EntitySaveChangesInterceptor
який буде успадковувати SaveChangesInterceptor
. SaveChangesInterceptor
- це стандартний клс EF Core який реалізовує інтерфейс ISaveChangesInterceptor
і надасть нам можливість перехопити всі дії які пов’язані із модифікацією даних в БД.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class EntitySaveChangesInterceptor : SaveChangesInterceptor
{
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
UpdateEntities(eventData.Context);
return base.SavingChanges(eventData, result);
}
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData,
InterceptionResult<int> result, CancellationToken cancellationToken = default)
{
UpdateEntities(eventData.Context);
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
private void UpdateEntities(DbContext? context)
{
if (context is null)
{
return;
}
foreach (var entry in context.ChangeTracker.Entries<ChangeTrackerEntity>())
{
if (entry.State is EntityState.Added)
{
entry.Entity.ModifierId = this.userService.GetUserId();
entry.Entity.CreatedDate = DateTime.UtcNow;
}
if (entry.State is EntityState.Added or EntityState.Modified || entry.HasChangedOwnedEntities())
{
entry.Entity.ModifierId = this.userService.GetUserId();
entry.Entity.ModifiedDate = DateTime.UtcNow;
}
}
}
}
public static class Extensions
{
public static bool HasChangedOwnedEntities(this EntityEntry entry) =>
entry.References.Any(r =>
r.TargetEntry != null &&
r.TargetEntry.Metadata.IsOwned() &&
r.TargetEntry.State is EntityState.Added or EntityState.Modified);
}
І так наш клас успадковує SaveChangesInterceptor
а також перевизначає два методи SavingChanges
і SavingChangesAsync
, вони, в свою чергу, роблять теж саме, перехоплюють модифікацію даних, один синхронно інший асинхронно.
Будьте уважні, тому що,
SaveChangesInterceptor
також має методиSavedChanges
таSavedChangesAsync
, які будуть викликані тільки тоді коли дані вже збережені і в цьому випадку ви не зможете відстежити контекст змін.
У нас також є UpdateEntities
метод який отримує всі сутності типу ChangeTrackerEntity
, які відстежуються контекстом (в нашому випадку тільки таблиця User
, але ви можете використовувати клас ChangeTrackerEntity
для інших таблиць і поведінка буде ідентичною), а також перевіряє чи стан сутності Added
(додати нові дані) або Modified
(змінити вже існуючі). Також ми маємо метод розширень HasChangedOwnedEntities
який перевіряє, чи були змінені або додані власні сутності (owned entities) для конкретної EntityEntry в контексті EF Core. Він перевіряє, чи є в EntityEntry хоча б один Reference, що вказує на owned entity. І після цього для кожного такого Reference перевіряється, чи відбулися зміни в цій owned entity, шляхом перевірки, чи TargetEntry вказує на сутність (entity), яка була додана або змінена (State is EntityState.Added або EntityState.Modified). Якщо метод повертає true, це означає, що одна або кілька власних сутностей були додані або змінені в поточній EntityEntry.
Також ми маємо
entry.Entity.ModifierId = this.userService.GetUserId();
- тут повинна бути логіка яка отримує Id вашого поточного активного користувача, ви можете використовувати DI щоб додати будь які сервіси до перехоплювача.
Останнім кроком вам потрібно зареєструвати EntitySaveChangesInterceptor
сервіс в контейнері залежностей (dependency injection container).
1
services.AddScoped<EntitySaveChangesInterceptor>();
Якщо ми запустимо наш додаток і викличемо метод SaveChangesAsync
або SaveChanges
при додаванні або модифікації даних в таблиці User
, то також автоматично будуть заповненні колонки CreatedDate
, ModifiedDate
та ModifierId
з відповідними даними.
Висновок
Ми розібралися що таке перехоплювачі і для чого вони потрібні, також як з їх допомогою можна з легкістю реалізувати різного роду логіки і головне не використовуючи безліч дубльовано коду та надлишкових залежностей.