Interceptory

Przechwytniki platformy Entity Framework Core (EF Core) umożliwiają przechwytywanie, modyfikowanie i/lub pomijanie operacji platformy EF Core. Obejmuje to operacje bazy danych niskiego poziomu, takie jak wykonywanie polecenia, a także operacje wyższego poziomu, takie jak wywołania do funkcji SaveChanges.

Interceptory różnią się od rejestrowania i diagnostyki tym, że umożliwiają modyfikowanie lub pomijanie przechwyconej operacji. Rejestrowanie proste lub Microsoft.Extensions.Logging to lepsze opcje do wyboru na potrzeby rejestrowania.

Interceptory są rejestrowane dla poszczególnych wystąpień obiektu DbContext podczas konfigurowania kontekstu. Użyj odbiornika diagnostycznego, aby uzyskać te same informacje ale dla wszystkich wystąpień obiektu DbContext w procesie.

Rejestrowanie przechwytujących

Przechwytywanie są rejestrowane przy użyciu AddInterceptors podczas konfigurowania wystąpienia DbContext. Jest to często wykonywane w przesłonięcie .DbContext.OnConfiguring Przykład:

public class ExampleContext : BlogsContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.AddInterceptors(new TaggedQueryCommandInterceptor());
}

Alternatywnie AddInterceptors można wywołać jako część AddDbContext lub podczas tworzenia DbContextOptions wystąpienia w celu przekazania do konstruktora DbContext.

Napiwek

Funkcja OnConfiguring jest nadal wywoływana, gdy jest używany element AddDbContext lub wystąpienie DbContextOptions jest przekazywane do konstruktora DbContext. Dzięki temu idealnie nadaje się do zastosowania konfiguracji kontekstu niezależnie od sposobu konstruowania obiektu DbContext.

Przechwytniki są często bezstanowe, co oznacza, że pojedyncze wystąpienie przechwytywania może być używane dla wszystkich wystąpień dbContext. Przykład:

public class TaggedQueryCommandInterceptorContext : BlogsContext
{
    private static readonly TaggedQueryCommandInterceptor _interceptor
        = new TaggedQueryCommandInterceptor();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.AddInterceptors(_interceptor);
}

Każde wystąpienie przechwytywania musi implementować co najmniej jeden interfejs pochodzący z IInterceptorklasy . Każde wystąpienie powinno być zarejestrowane tylko raz, nawet jeśli implementuje wiele interfejsów przechwytywania; Program EF Core będzie kierować zdarzenia dla każdego interfejsu odpowiednio.

Przechwytywanie bazy danych

Uwaga

Przechwytywanie bazy danych jest dostępne tylko dla dostawców relacyjnych baz danych.

Przechwytywanie bazy danych niskiego poziomu jest podzielone na trzy interfejsy pokazane w poniższej tabeli.

Interceptor Przechwycone operacje bazy danych
IDbCommandInterceptor Tworzenie poleceń Wykonywanie poleceń
: błędy

polecenia Dysponowanie polecenia DbDataReader polecenia
IDbConnectionInterceptor Błędy otwierania i zamykania połączeń
Połączenie ion
IDbTransactionInterceptor Tworzenie transakcji
przy użyciu istniejących transakcji Zatwierdzanie transakcji
Wycofywanie transakcji

Tworzenie i używanie błędów transakcji punktów zapisywania

Klasy DbCommandInterceptorbazowe , DbConnectionInterceptori DbTransactionInterceptor zawierają implementacje bez operacji dla każdej metody w odpowiednim interfejsie. Użyj klas bazowych, aby uniknąć konieczności implementowania nieużywanych metod przechwytywania.

Metody dla każdego typu przechwytywania są w parach, z pierwszym wywoływanym przed uruchomieniem operacji bazy danych, a drugi po zakończeniu operacji. Na przykład DbCommandInterceptor.ReaderExecuting jest wywoływana przed wykonaniem zapytania i DbCommandInterceptor.ReaderExecuted jest wywoływana po wysłaniu zapytania do bazy danych.

Każda para metod ma zarówno synchronizację, jak i odmiany asynchroniczne. Umożliwia to asynchroniczne operacje we/wy, takie jak żądanie tokenu dostępu, w ramach przechwycenia operacji asynchronicznej bazy danych.

Przykład: przechwytywanie poleceń w celu dodania wskazówek dotyczących zapytań

Napiwek

Przykład przechwytywania poleceń można pobrać z usługi GitHub.

Element IDbCommandInterceptor może służyć do modyfikowania bazy danych SQL przed wysłaniem go do bazy danych. W tym przykładzie pokazano, jak zmodyfikować język SQL w celu uwzględnienia wskazówki dotyczącej zapytania.

Często najtrudniejszą częścią przechwytywania jest określenie, kiedy polecenie odpowiada zapytaniu, które należy zmodyfikować. Analizowanie kodu SQL jest jedną z opcji, ale wydaje się być kruche. Inną opcją jest użycie tagów zapytań platformy EF Core do tagowania każdego zapytania, które należy zmodyfikować. Przykład:

var blogs1 = context.Blogs.TagWith("Use hint: robust plan").ToList();

Ten tag można następnie wykryć w przechwytniku, ponieważ zawsze będzie on dołączany jako komentarz w pierwszym wierszu tekstu polecenia. Podczas wykrywania tagu zapytanie SQL jest modyfikowane w celu dodania odpowiedniej wskazówki:

public class TaggedQueryCommandInterceptor : DbCommandInterceptor
{
    public override InterceptionResult<DbDataReader> ReaderExecuting(
        DbCommand command,
        CommandEventData eventData,
        InterceptionResult<DbDataReader> result)
    {
        ManipulateCommand(command);

        return result;
    }

    public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
        DbCommand command,
        CommandEventData eventData,
        InterceptionResult<DbDataReader> result,
        CancellationToken cancellationToken = default)
    {
        ManipulateCommand(command);

        return new ValueTask<InterceptionResult<DbDataReader>>(result);
    }

    private static void ManipulateCommand(DbCommand command)
    {
        if (command.CommandText.StartsWith("-- Use hint: robust plan", StringComparison.Ordinal))
        {
            command.CommandText += " OPTION (ROBUST PLAN)";
        }
    }
}

Uwaga:

  • Przechwytywanie dziedziczy z DbCommandInterceptor , aby uniknąć konieczności implementowania każdej metody w interfejsie przechwytywania.
  • Przechwytywanie implementuje zarówno metody synchronizacji, jak i asynchroniczne. Dzięki temu ta sama wskazówka zapytania jest stosowana do synchronizacji i zapytań asynchronicznych.
  • Przechwytywanie implementuje Executing metody wywoływane przez program EF Core z wygenerowaną bazą danych SQL przed wysłaniem ich do bazy danych. Porównaj to z metodami Executed , które są wywoływane po zwróconym wywołaniu bazy danych.

Uruchomienie kodu w tym przykładzie powoduje wygenerowanie następującego kodu podczas tagu zapytania:

-- Use hint: robust plan

SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b] OPTION (ROBUST PLAN)

Z drugiej strony, gdy zapytanie nie jest oznakowane, jest wysyłane do bazy danych niezmodyfikowane:

SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]

Przykład: przechwytywanie Połączenie na potrzeby uwierzytelniania Usługi SQL Azure przy użyciu usługi AAD

Napiwek

Przykład przechwytywania połączeń można pobrać z usługi GitHub.

Element IDbConnectionInterceptor może służyć do manipulowania elementem DbConnection , zanim zostanie użyty do nawiązania połączenia z bazą danych. Może to służyć do uzyskania tokenu dostępu usługi Azure Active Directory (AAD). Przykład:

public class AadAuthenticationInterceptor : DbConnectionInterceptor
{
    public override InterceptionResult ConnectionOpening(
        DbConnection connection,
        ConnectionEventData eventData,
        InterceptionResult result)
        => throw new InvalidOperationException("Open connections asynchronously when using AAD authentication.");

    public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(
        DbConnection connection,
        ConnectionEventData eventData,
        InterceptionResult result,
        CancellationToken cancellationToken = default)
    {
        var sqlConnection = (SqlConnection)connection;

        var provider = new AzureServiceTokenProvider();
        // Note: in some situations the access token may not be cached automatically the Azure Token Provider.
        // Depending on the kind of token requested, you may need to implement your own caching here.
        sqlConnection.AccessToken = await provider.GetAccessTokenAsync("https://database.windows.net/", null, cancellationToken);

        return result;
    }
}

Napiwek

Microsoft.Data.SqlClient obsługuje teraz uwierzytelnianie usługi AAD za pośrednictwem parametry połączenia. Aby uzyskać więcej informacji, zobacz SqlAuthenticationMethod.

Ostrzeżenie

Zwróć uwagę, że przechwytujący zgłasza błąd, jeśli wykonano wywołanie synchronizacji w celu otwarcia połączenia. Wynika to z faktu, że nie ma metody niesynchronicznej w celu uzyskania tokenu dostępu i nie ma uniwersalnego i prostego sposobu wywoływania metody asynchronicznej z kontekstu niezsynchronicznego bez ryzyka zakleszczenia.

Ostrzeżenie

w niektórych sytuacjach token dostępu może nie być automatycznie buforowany przez dostawcę tokenów platformy Azure. W zależności od rodzaju żądanego tokenu może być konieczne zaimplementowanie własnego buforowania tutaj.

Przykład: zaawansowane przechwytywanie poleceń na potrzeby buforowania

Napiwek

Możesz pobrać zaawansowany przykład przechwytywania poleceń z usługi GitHub.

Przechwytniki ef Core mogą wykonywać następujące czynności:

  • Poinformuj program EF Core, aby pominąć wykonywanie operacji przechwyconej
  • Zmiana wyniku operacji zgłoszonej z powrotem na platformę EF Core

W tym przykładzie pokazano przechwytujący, który używa tych funkcji do zachowania się jak pierwotna pamięć podręczna drugiego poziomu. Wyniki zapytań w pamięci podręcznej są zwracane dla określonego zapytania, unikając powrotu do bazy danych.

Ostrzeżenie

Należy zachować ostrożność podczas zmieniania domyślnego zachowania platformy EF Core w ten sposób. Program EF Core może zachowywać się w nieoczekiwany sposób, jeśli otrzyma nieprawidłowy wynik, którego nie może przetworzyć poprawnie. Ponadto w tym przykładzie przedstawiono koncepcje przechwytywania; nie jest ona przeznaczona jako szablon do niezawodnej implementacji pamięci podręcznej drugiego poziomu.

W tym przykładzie aplikacja często wykonuje zapytanie w celu uzyskania najnowszego "codziennego komunikatu":

async Task<string> GetDailyMessage(DailyMessageContext context)
    => (await context.DailyMessages.TagWith("Get_Daily_Message").OrderBy(e => e.Id).LastAsync()).Message;

To zapytanie jest oznakowane tak, aby można je było łatwo wykryć w przechwytywaniu. Chodzi o to, aby wysyłać zapytania do bazy danych o nowy komunikat raz dziennie. W innym czasie aplikacja będzie używać buforowanego wyniku. (Próbka używa opóźnienia 10 sekund w próbce do symulowania nowego dnia).

Stan przechwytywania

Ten przechwytator jest stanowy: przechowuje identyfikator i tekst wiadomości najnowszego codziennego zapytania, a także czas wykonywania tego zapytania. Ze względu na ten stan potrzebujemy również blokady , ponieważ buforowanie wymaga użycia tego samego przechwytywania przez wiele wystąpień kontekstu.

private readonly object _lock = new object();
private int _id;
private string _message;
private DateTime _queriedAt;

Przed wykonaniem

W metodzie Executing (tj. przed wywołaniem bazy danych) przechwytywanie wykrywa otagowane zapytanie, a następnie sprawdza, czy istnieje buforowany wynik. Jeśli taki wynik zostanie znaleziony, zapytanie zostanie pominięte i zamiast tego zostaną użyte buforowane wyniki.

public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
    DbCommand command,
    CommandEventData eventData,
    InterceptionResult<DbDataReader> result,
    CancellationToken cancellationToken = default)
{
    if (command.CommandText.StartsWith("-- Get_Daily_Message", StringComparison.Ordinal))
    {
        lock (_lock)
        {
            if (_message != null
                && DateTime.UtcNow < _queriedAt + new TimeSpan(0, 0, 10))
            {
                command.CommandText = "-- Get_Daily_Message: Skipping DB call; using cache.";
                result = InterceptionResult<DbDataReader>.SuppressWithResult(new CachedDailyMessageDataReader(_id, _message));
            }
        }
    }

    return new ValueTask<InterceptionResult<DbDataReader>>(result);
}

Zwróć uwagę, że kod wywołuje InterceptionResult<TResult>.SuppressWithResult i przekazuje zamianę DbDataReader zawierającą buforowane dane. Następnie zwracany jest ten parametr InterceptionResult, co powoduje pomijanie wykonywania zapytania. Czytnik zastępczy jest zamiast tego używany przez program EF Core jako wyniki zapytania.

Ten przechwytujący manipuluje również tekstem polecenia. Ta manipulacja nie jest wymagana, ale zwiększa przejrzystość komunikatów dziennika. Tekst polecenia nie musi być prawidłowym plikiem SQL, ponieważ zapytanie nie zostanie wykonane.

Po wykonaniu

Jeśli nie jest dostępny żaden buforowany komunikat lub jeśli wygasł, powyższy kod nie pomija wyniku. W związku z tym program EF Core wykona zapytanie w zwykły sposób. Następnie powróci do metody przechwytywania Executed po wykonaniu. W tym momencie, jeśli wynik nie jest jeszcze czytnikiem w pamięci podręcznej, nowy identyfikator i ciąg komunikatu są wyodrębniane z rzeczywistego czytnika i buforowane gotowe do następnego użycia tego zapytania.

public override async ValueTask<DbDataReader> ReaderExecutedAsync(
    DbCommand command,
    CommandExecutedEventData eventData,
    DbDataReader result,
    CancellationToken cancellationToken = default)
{
    if (command.CommandText.StartsWith("-- Get_Daily_Message", StringComparison.Ordinal)
        && !(result is CachedDailyMessageDataReader))
    {
        try
        {
            await result.ReadAsync(cancellationToken);

            lock (_lock)
            {
                _id = result.GetInt32(0);
                _message = result.GetString(1);
                _queriedAt = DateTime.UtcNow;
                return new CachedDailyMessageDataReader(_id, _message);
            }
        }
        finally
        {
            await result.DisposeAsync();
        }
    }

    return result;
}

Pokaz

Przykład przechwytywania buforowania zawiera prostą aplikację konsolową, która wykonuje zapytania dotyczące codziennych komunikatów w celu przetestowania buforowania:

// 1. Initialize the database with some daily messages.
using (var context = new DailyMessageContext())
{
    await context.Database.EnsureDeletedAsync();
    await context.Database.EnsureCreatedAsync();

    context.AddRange(
        new DailyMessage { Message = "Remember: All builds are GA; no builds are RTM." },
        new DailyMessage { Message = "Keep calm and drink tea" });

    await context.SaveChangesAsync();
}

// 2. Query for the most recent daily message. It will be cached for 10 seconds.
using (var context = new DailyMessageContext())
{
    Console.WriteLine(await GetDailyMessage(context));
}

// 3. Insert a new daily message.
using (var context = new DailyMessageContext())
{
    context.Add(new DailyMessage { Message = "Free beer for unicorns" });

    await context.SaveChangesAsync();
}

// 4. Cached message is used until cache expires.
using (var context = new DailyMessageContext())
{
    Console.WriteLine(await GetDailyMessage(context));
}

// 5. Pretend it's the next day.
Thread.Sleep(10000);

// 6. Cache is expired, so the last message will not be queried again.
using (var context = new DailyMessageContext())
{
    Console.WriteLine(await GetDailyMessage(context));
}

async Task<string> GetDailyMessage(DailyMessageContext context)
    => (await context.DailyMessages.TagWith("Get_Daily_Message").OrderBy(e => e.Id).LastAsync()).Message;

Spowoduje to wykonanie następujących danych wyjściowych:

info: 10/15/2020 12:32:11.801 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      -- Get_Daily_Message

      SELECT "d"."Id", "d"."Message"
      FROM "DailyMessages" AS "d"
      ORDER BY "d"."Id" DESC
      LIMIT 1

Keep calm and drink tea

info: 10/15/2020 12:32:11.821 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='Free beer for unicorns' (Size = 22)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "DailyMessages" ("Message")
      VALUES (@p0);
      SELECT "Id"
      FROM "DailyMessages"
      WHERE changes() = 1 AND "rowid" = last_insert_rowid();

info: 10/15/2020 12:32:11.826 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      -- Get_Daily_Message: Skipping DB call; using cache.

Keep calm and drink tea

info: 10/15/2020 12:32:21.833 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      -- Get_Daily_Message

      SELECT "d"."Id", "d"."Message"
      FROM "DailyMessages" AS "d"
      ORDER BY "d"."Id" DESC
      LIMIT 1

Free beer for unicorns

Zwróć uwagę na dane wyjściowe dziennika, że aplikacja nadal używa buforowanego komunikatu do czasu wygaśnięcia limitu czasu, w którym baza danych jest ponownie odpytywane dla każdego nowego komunikatu.

Przechwytywanie funkcji SaveChanges

Napiwek

Przykład przechwytywania SaveChanges można pobrać z usługi GitHub.

SaveChanges punkty przechwytywania i SaveChangesAsync są definiowane ISaveChangesInterceptor przez interfejs. Jeśli chodzi o inne przechwytniki, SaveChangesInterceptor klasa bazowa bez metod op jest dostarczana jako wygoda.

Napiwek

Przechwytniki są potężne. Jednak w wielu przypadkach może być łatwiej zastąpić metodę SaveChanges lub użyć zdarzeń platformy .NET dla funkcji SaveChanges uwidocznionych w obiekcie DbContext.

Przykład: przechwytywanie saveChanges na potrzeby inspekcji

Funkcja SaveChanges może zostać przechwycona w celu utworzenia niezależnego rekordu inspekcji wprowadzonych zmian.

Uwaga

Nie jest to niezawodne rozwiązanie do inspekcji. Zamiast tego jest to uproszczony przykład używany do zademonstrowania cech przechwytywania.

Kontekst aplikacji

Przykład inspekcji używa prostego elementu DbContext z blogami i wpisami.

public class BlogsContext : DbContext
{
    private readonly AuditingInterceptor _auditingInterceptor = new AuditingInterceptor("DataSource=audit.db");

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .AddInterceptors(_auditingInterceptor)
            .UseSqlite("DataSource=blogs.db");

    public DbSet<Blog> Blogs { get; set; }
}

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }

    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }

    public Blog Blog { get; set; }
}

Zwróć uwagę, że dla każdego wystąpienia obiektu DbContext zarejestrowano nowe wystąpienie przechwytywania. Jest to spowodowane tym, że przechwytywanie inspekcji zawiera stan połączony z bieżącym wystąpieniem kontekstu.

Kontekst inspekcji

Przykład zawiera również drugą bazę danych DbContext i model używany dla bazy danych inspekcji.

public class AuditContext : DbContext
{
    private readonly string _connectionString;

    public AuditContext(string connectionString)
    {
        _connectionString = connectionString;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseSqlite(_connectionString);

    public DbSet<SaveChangesAudit> SaveChangesAudits { get; set; }
}

public class SaveChangesAudit
{
    public int Id { get; set; }
    public Guid AuditId { get; set; }
    public DateTime StartTime { get; set; }
    public DateTime EndTime { get; set; }
    public bool Succeeded { get; set; }
    public string ErrorMessage { get; set; }

    public ICollection<EntityAudit> Entities { get; } = new List<EntityAudit>();
}

public class EntityAudit
{
    public int Id { get; set; }
    public EntityState State { get; set; }
    public string AuditMessage { get; set; }

    public SaveChangesAudit SaveChangesAudit { get; set; }
}

Przechwytywanie

Ogólną ideą inspekcji za pomocą przechwytywania jest:

  • Komunikat inspekcji jest tworzony na początku funkcji SaveChanges i jest zapisywany w bazie danych inspekcji
  • Funkcja SaveChanges może kontynuować
  • Jeśli funkcja SaveChanges powiedzie się, komunikat inspekcji zostanie zaktualizowany, aby wskazać powodzenie
  • Jeśli polecenie SaveChanges zakończy się niepowodzeniem, zostanie zaktualizowany komunikat inspekcji, aby wskazać błąd

Pierwszy etap jest obsługiwany przed wysłaniem wszelkich zmian do bazy danych przy użyciu przesłonięć ISaveChangesInterceptor.SavingChanges elementów i ISaveChangesInterceptor.SavingChangesAsync.

public async ValueTask<InterceptionResult<int>> SavingChangesAsync(
    DbContextEventData eventData,
    InterceptionResult<int> result,
    CancellationToken cancellationToken = default)
{
    _audit = CreateAudit(eventData.Context);

    using var auditContext = new AuditContext(_connectionString);

    auditContext.Add(_audit);
    await auditContext.SaveChangesAsync();

    return result;
}

public InterceptionResult<int> SavingChanges(
    DbContextEventData eventData,
    InterceptionResult<int> result)
{
    _audit = CreateAudit(eventData.Context);

    using var auditContext = new AuditContext(_connectionString);
    auditContext.Add(_audit);
    auditContext.SaveChanges();

    return result;
}

Zastąpienie zarówno metod synchronizacji, jak i asynchronicznych zapewnia, że inspekcja będzie odbywać się niezależnie od tego, czy SaveChangesSaveChangesAsync jest wywoływana. Zauważ również, że samo przeciążenie asynchroniczne jest w stanie wykonać nieblokujące asynchroniczne we/wy do bazy danych inspekcji. Możesz chcieć zgłosić metodę synchronizacji SavingChanges , aby upewnić się, że wszystkie operacje we/wy bazy danych są asynchroniczne. Następnie wymaga to, aby aplikacja zawsze wywołuje metodę SaveChangesAsync i nigdy nie SaveChanges.

Komunikat inspekcji

Każda metoda przechwytywania ma eventData parametr zapewniający kontekstowe informacje o przechwyconym zdarzeniu. W takim przypadku bieżąca aplikacja DbContext jest uwzględniana w danych zdarzenia, które są następnie używane do tworzenia komunikatu inspekcji.

private static SaveChangesAudit CreateAudit(DbContext context)
{
    context.ChangeTracker.DetectChanges();

    var audit = new SaveChangesAudit { AuditId = Guid.NewGuid(), StartTime = DateTime.UtcNow };

    foreach (var entry in context.ChangeTracker.Entries())
    {
        var auditMessage = entry.State switch
        {
            EntityState.Deleted => CreateDeletedMessage(entry),
            EntityState.Modified => CreateModifiedMessage(entry),
            EntityState.Added => CreateAddedMessage(entry),
            _ => null
        };

        if (auditMessage != null)
        {
            audit.Entities.Add(new EntityAudit { State = entry.State, AuditMessage = auditMessage });
        }
    }

    return audit;

    string CreateAddedMessage(EntityEntry entry)
        => entry.Properties.Aggregate(
            $"Inserting {entry.Metadata.DisplayName()} with ",
            (auditString, property) => auditString + $"{property.Metadata.Name}: '{property.CurrentValue}' ");

    string CreateModifiedMessage(EntityEntry entry)
        => entry.Properties.Where(property => property.IsModified || property.Metadata.IsPrimaryKey()).Aggregate(
            $"Updating {entry.Metadata.DisplayName()} with ",
            (auditString, property) => auditString + $"{property.Metadata.Name}: '{property.CurrentValue}' ");

    string CreateDeletedMessage(EntityEntry entry)
        => entry.Properties.Where(property => property.Metadata.IsPrimaryKey()).Aggregate(
            $"Deleting {entry.Metadata.DisplayName()} with ",
            (auditString, property) => auditString + $"{property.Metadata.Name}: '{property.CurrentValue}' ");
}

Wynikiem jest SaveChangesAudit jednostka z kolekcją EntityAudit jednostek, jedną dla każdego wstawiania, aktualizowania lub usuwania. Następnie przechwytujący wstawia te jednostki do bazy danych inspekcji.

Napiwek

Funkcja ToString jest zastępowana w każdej klasie danych zdarzeń programu EF Core w celu wygenerowania równoważnego komunikatu dziennika dla zdarzenia. Na przykład wywołanie ContextInitializedEventData.ToString generuje element "Entity Framework Core 5.0.0 zainicjowane "BlogsContext" przy użyciu dostawcy "Microsoft.EntityFrameworkCore.Sqlite" z opcjami: Brak.

Wykrywanie powodzenia

Jednostka inspekcji jest przechowywana na przechwytywaniu, aby można było uzyskać do niej dostęp ponownie po pomyślnym lub niepowoływanej operacji SaveChanges. W przypadku powodzenia ISaveChangesInterceptor.SavedChanges lub ISaveChangesInterceptor.SavedChangesAsync jest wywoływana.

public int SavedChanges(SaveChangesCompletedEventData eventData, int result)
{
    using var auditContext = new AuditContext(_connectionString);

    auditContext.Attach(_audit);
    _audit.Succeeded = true;
    _audit.EndTime = DateTime.UtcNow;

    auditContext.SaveChanges();

    return result;
}

public async ValueTask<int> SavedChangesAsync(
    SaveChangesCompletedEventData eventData,
    int result,
    CancellationToken cancellationToken = default)
{
    using var auditContext = new AuditContext(_connectionString);

    auditContext.Attach(_audit);
    _audit.Succeeded = true;
    _audit.EndTime = DateTime.UtcNow;

    await auditContext.SaveChangesAsync(cancellationToken);

    return result;
}

Jednostka inspekcji jest dołączona do kontekstu inspekcji, ponieważ już istnieje w bazie danych i musi zostać zaktualizowana. Następnie ustawiamy Succeeded wartości i EndTime, które oznaczają te właściwości jako zmodyfikowane, więc funkcja SaveChanges wyśle aktualizację do bazy danych inspekcji.

Wykrywanie błędu

Niepowodzenie jest obsługiwane w taki sam sposób, jak w przypadku powodzenia, ale w metodzie ISaveChangesInterceptor.SaveChangesFailed or ISaveChangesInterceptor.SaveChangesFailedAsync . Dane zdarzenia zawierają zgłoszony wyjątek.

public void SaveChangesFailed(DbContextErrorEventData eventData)
{
    using var auditContext = new AuditContext(_connectionString);

    auditContext.Attach(_audit);
    _audit.Succeeded = false;
    _audit.EndTime = DateTime.UtcNow;
    _audit.ErrorMessage = eventData.Exception.Message;

    auditContext.SaveChanges();
}

public async Task SaveChangesFailedAsync(
    DbContextErrorEventData eventData,
    CancellationToken cancellationToken = default)
{
    using var auditContext = new AuditContext(_connectionString);

    auditContext.Attach(_audit);
    _audit.Succeeded = false;
    _audit.EndTime = DateTime.UtcNow;
    _audit.ErrorMessage = eventData.Exception.InnerException?.Message;

    await auditContext.SaveChangesAsync(cancellationToken);
}

Pokaz

Przykład inspekcji zawiera prostą aplikację konsolową, która wprowadza zmiany w bazie danych blogów, a następnie wyświetla utworzoną inspekcję.

// Insert, update, and delete some entities

using (var context = new BlogsContext())
{
    context.Add(
        new Blog { Name = "EF Blog", Posts = { new Post { Title = "EF Core 3.1!" }, new Post { Title = "EF Core 5.0!" } } });

    await context.SaveChangesAsync();
}

using (var context = new BlogsContext())
{
    var blog = context.Blogs.Include(e => e.Posts).Single();

    blog.Name = "EF Core Blog";
    context.Remove(blog.Posts.First());
    blog.Posts.Add(new Post { Title = "EF Core 6.0!" });

    context.SaveChanges();
}

// Do an insert that will fail

using (var context = new BlogsContext())
{
    try
    {
        context.Add(new Post { Id = 3, Title = "EF Core 3.1!" });

        await context.SaveChangesAsync();
    }
    catch (DbUpdateException)
    {
    }
}

// Look at the audit trail

using (var context = new AuditContext("DataSource=audit.db"))
{
    foreach (var audit in context.SaveChangesAudits.Include(e => e.Entities).ToList())
    {
        Console.WriteLine(
            $"Audit {audit.AuditId} from {audit.StartTime} to {audit.EndTime} was{(audit.Succeeded ? "" : " not")} successful.");

        foreach (var entity in audit.Entities)
        {
            Console.WriteLine($"  {entity.AuditMessage}");
        }

        if (!audit.Succeeded)
        {
            Console.WriteLine($"  Error: {audit.ErrorMessage}");
        }
    }
}

Wynik przedstawia zawartość bazy danych inspekcji:

Audit 52e94327-1767-4046-a3ca-4c6b1eecbca6 from 10/14/2020 9:10:17 PM to 10/14/2020 9:10:17 PM was successful.
  Inserting Blog with Id: '-2147482647' Name: 'EF Blog'
  Inserting Post with Id: '-2147482647' BlogId: '-2147482647' Title: 'EF Core 3.1!'
  Inserting Post with Id: '-2147482646' BlogId: '-2147482647' Title: 'EF Core 5.0!'
Audit 8450f57a-5030-4211-a534-eb66b8da7040 from 10/14/2020 9:10:17 PM to 10/14/2020 9:10:17 PM was successful.
  Inserting Post with Id: '-2147482645' BlogId: '1' Title: 'EF Core 6.0!'
  Updating Blog with Id: '1' Name: 'EF Core Blog'
  Deleting Post with Id: '1'
Audit 201fef4d-66a7-43ad-b9b6-b57e9d3f37b3 from 10/14/2020 9:10:17 PM to 10/14/2020 9:10:17 PM was not successful.
  Inserting Post with Id: '3' BlogId: '' Title: 'EF Core 3.1!'
  Error: SQLite Error 19: 'UNIQUE constraint failed: Post.Id'.