Kto to zmienił i kiedy? „Czysty” sposób aktualizacji kolumn audytowych w bazie danych SQL

Krzysztof Waśko

W świecie rozwoju oprogramowania ważne jest utrzymanie śladu audytowego zmian dokonywanych w danych. Powszechnym podejściem do implementacji tej funkcjonalności jest automatyczna aktualizacja kolumn audytowych, np. CreatedBy, UpdatedBy, CreatedDate i UpdatedDate, w tabelach bazy danych. Pomocna jest wtedy funkcja ChangeTracker w Entity Framework Core. Jak skonfigurować automatyczne wypełnianie kolumn audytowych podczas tworzenia i aktualizowania encji w aplikacjach .NET?

Po co nam ślad audytowy?

Utrzymanie śladu audytowego zmian dokonywanych w danych nie tylko pomaga nam zidentyfikować potencjalne problemy związane z bezpieczeństwem. Pozwala ono też monitorować wydajność i utrzymywać historię zmian w celach debugowania.

Warunki wstępne

Jeśli chcesz korzystać z przykładów przedstawionych w tym artykule, musisz spełniać następujące warunki:

  1. Mieć podstawową wiedzę z zakresu języka C# i platformy .NET Core
  2. Znać się na EntityFrameworkCore
  3. Mieć zainstalowany Microsoft SQL Server lub dowolny inny obsługiwany system bazodanowy

Konfiguracja kontekstu bazy danych

Najpierw stwórz klasę bazową dla Twoich encji zawierającą kolumny audytowe. Może być ona dziedziczona przez wszystkie encje w Twojej aplikacji, które potrzebują informacji audytowych. Jak to zrobić? Spójrz:

public abstract class AuditableEntity
{
    public string CreatedBy { get; set; }
    public DateTime CreatedDate { get; set; }
    public string UpdatedBy { get; set; }
    public DateTime? UpdatedDate { get; set; }
}

Teraz utwórz prostą encję, która dziedziczy po klasie bazowej AuditableEntity:

public class Product : AuditableEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Następnie musisz utworzyć klasę kontekstu bazy danych dziedziczącą po DbContext. W tej klasie nadpiszesz metody SaveChanges i SaveChangesAsync, aby automatycznie aktualizować kolumny audytowe przed zapisaniem zmian w bazie danych. Zobacz:

public class ApplicationDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }

    public override int SaveChanges()
    {
        UpdateAuditColumns();
        return base.SaveChanges();
    }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
    {
        UpdateAuditColumns();
        return await base.SaveChangesAsync(cancellationToken);
    }

    private void UpdateAuditColumns()
    {
        // Code to update audit columns will go here.
    }
}

Aktualizacja kolumn audytowych przy użyciu ChangeTracker

Metoda UpdateAuditColumns będzie wykorzystywać ChangeTracker EF Core do określenia, które encje zostały dodane lub zmodyfikowane. Dla każdej takiej encji zaktualizujesz odpowiadające kolumny audytowe. Jak to zrobić? Popatrz:

private void UpdateAuditColumns()
{
    var currentUserId = "JanKowalski"; // This should be replaced with the actual user ID/Name from the user context.
    var now = DateTime.UtcNow;

    foreach (var entry in ChangeTracker.Entries<AuditableEntity>())
    {
        switch (entry.State)
        {
            case EntityState.Added:
                entry.Entity.CreatedBy = currentUserId;
                entry.Entity.CreatedDate = now;
                break;

            case EntityState.Modified:
                entry.Entity.UpdatedBy = currentUserId;
                entry.Entity.UpdatedDate = now;
                break;
        }
    }
}

W powyższym fragmencie kodu najpierw pobierasz identyfikator bieżącego użytkownika. Zazwyczaj realizuje się to przy użyciu kontekstu użytkownika lub tożsamości dostarczonej przez mechanizmy uwierzytelniania i autoryzacji aplikacji. Dzięki temu poprawne informacje użytkownika zostaną przechowane w kolumnach audytowych dla każdej zmiany danych. W celach demonstracyjnych użyjemy wartości hardcoded dla użytkownika „JanKowalski”.

Następnie przejdź pętlą przez wszystkie encje śledzone przez ChangeTracker i sprawdź ich EntityState. Jeśli encja jest dodawana, aktualizujesz kolumny CreatedBy i CreatedDate. Jeśli jest modyfikowana, aktualizujesz kolumny UpdatedBy i UpdatedDate. Zauważ, że używamy DateTime.UtcNow do przechowywania bieżącej daty i czasu w formacie UTC. Praktykę tę zaleca się, aby uniknąć problemów związanych z różnicami czasu i czasem letnim.

Korzystanie ze zaktualizowanego ApplicationDbContext

Udało się skonfigurować ApplicationDbContext do automatycznego aktualizowania kolumn audytowych? Teraz możesz używać go w swojej aplikacji do tworzenia i aktualizowania encji.

Przyjmijmy, że masz prostą aplikację konsolową, która dodaje nowy produkt i aktualizuje istniejący. Jak mogłoby to wyglądać? Spójrz:

class Program
{
    static void Main(string[] args)
    {
        var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
        optionsBuilder.UseSqlServer("Server=localhost;Database=MyDatabase;Trusted_Connection=True;");

        using (var context = new ApplicationDbContext(optionsBuilder.Options))
        {
            // Add a new product
            var newProduct = new Product { Name = "Laptop", Price = 1200m };
            context.Products.Add(newProduct);
            context.SaveChanges();

            Console.WriteLine("New product added with ID: {0}", newProduct.Id);

            // Update an existing product
            var existingProduct = context.Products.First();
            existingProduct.Price = 1900m;
            context.SaveChanges();

            Console.WriteLine("Updated product with ID: {0}", existingProduct.Id);
        }
    }
}    

W tym przykładzie, po wywołaniu SaveChanges na naszym ApplicationDbContext, kolumny audytowe dla nowego i istniejącego produktu zostaną zaktualizowane automatycznie.

Alternatywne podejście, czyli użycie Interceptora

Korzystanie z ChangeTracker jest prostą i skuteczną metodą automatycznego aktualizowania kolumn audytowych. Podobny rezultat można jednak osiągnąć również przy użyciu funkcji Interceptor dostępnej w Entity Framework Core 3.0 i jego nowszych wersjach. Interceptory pozwalają przechwycić i zmodyfikować polecenia SQL generowane przez EF Core przed ich wysłaniem do bazy danych. W tej sekcji pokażę Ci, jak używać Interceptora do aktualizacji kolumn audytowych dla naszych encji.

Najpierw utwórz klasę interceptorów implementującą interfejs ISaveChangesInterceptor:

public class AuditSaveChangesInterceptor : ISaveChangesInterceptor
{
    public async ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        var context = eventData.Context as ApplicationDbContext;
        context.UpdateAuditColumns();
        return result;
    }

    public InterceptionResult<int> SavingChanges(
        DbContextEventData eventData,
        InterceptionResult<int> result)
    {
        var context = eventData.Context as ApplicationDbContext;
        context.UpdateAuditColumns();
        return result;
    }

    // Other interface methods can be left empty or throw a NotImplementedException if not required
    // ...
}

W klasie AuditSaveChangesInterceptor implementujesz metody SavingChangesAsync i SavingChanges, które są odpowiednio wywoływane przed SaveChanges i SaveChangesAsync w ApplicationDbContext. Wywołujesz metodę UpdateAuditColumns z klasy ApplicationDbContext, aby zaktualizować kolumny audytowe.

Gotowe? Pora zaktualizować klasę ApplicationDbContext, aby zarejestrować Interceptor:

public class ApplicationDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.AddInterceptors(new AuditSaveChangesInterceptor());
        base.OnConfiguring(optionsBuilder);
    }

    // The rest of the ApplicationDbContext class remains the same
    // ...
}

W OnConfiguring używamy metody AddInterceptors do zarejestrowania niestandardowego AuditSaveChangesInterceptor.

Z tym podejściem kolumny audytowe będą się automatycznie aktualizować za każdym razem, gdy wywołasz SaveChanges lub SaveChangesAsync na instancjach ApplicationDbContext. Zapewnia to alternatywę dla nadpisywania metod SaveChanges/SaveChangeAsync na ApplicationDbContext i pozwala Ci wybrać podejście, które najlepiej odpowiada wymaganiom Twojej aplikacji.

Podsumowanie

Wiesz już, jak automatycznie aktualizować kolumny audytowe w bazie danych SQL, korzystając z ChangeTracker w Entity Framework Core. Implementując tę funkcjonalność w sposób czytelny i ponownie używalny, możesz zapewnić, że Twoja aplikacja utrzymuje wiarygodny ślad audytowy zmian w danych. Wpływa to korzystnie na bezpieczeństwo, wydajność i utrzymanie.

Korzystanie z ChangeTracker w Entity Framework Core dostarcza potężnego i „czystego” sposobu zarządzania kolumnami audytowymi. W efekcie programiści mogą skupić się na budowaniu podstawowej funkcjonalności aplikacji – bez konieczności ręcznej aktualizacji tych kolumn.

Bibliografia

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami