Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować się zalogować lub zmienić katalog.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
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 funkcji SaveChanges.
Przechwytywacze różnią się od rejestrowania i diagnostyki, ponieważ umożliwiają modyfikację lub pominięcie przechwyconej operacji. Proste rejestrowanie lub Microsoft.Extensions.Logging to lepsze opcje rejestrowania.
Przechwytywacze są rejestrowane dla każdego wystąpienia DbContext podczas konfigurowania kontekstu. Użyj odbiornika diagnostycznego, aby uzyskać te same informacje, ale dla wszystkich wystąpień DbContext w procesie.
Rejestrowanie interektorów
Przechwytywacze są rejestrowani przy użyciu AddInterceptors podczas konfigurowania wystąpienia DbContext. Jest to często wykonywane w zastąpieniu DbContext.OnConfiguring. Przykład:
public class ExampleContext : BlogsContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(new TaggedQueryCommandInterceptor());
}
Alternatywnie, AddInterceptors może być wywoływane jako część AddDbContext, lub podczas tworzenia wystąpienia DbContextOptions w celu przekazania do konstruktora DbContext.
Wskazówka
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żda instancja interceptora musi implementować jeden lub więcej interfejsów pochodzących z klasy IInterceptor. Każda instancja powinna być zarejestrowana tylko raz, nawet jeśli implementuje wiele interfejsów przechwytywania; EF Core będzie kierować zdarzeniami dla każdego interfejsu odpowiednio.
Przechwytywanie bazy danych
Uwaga / Notatka
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.
| Przechwytywacz | Przechwycone operacje bazy danych |
|---|---|
| IDbCommandInterceptor | Tworzenie poleceń Wykonywanie poleceń Niepowodzenia poleceń Usuwanie obiektu DbDataReader polecenia |
| IDbConnectionInterceptor | Błędy otwierania i zamykania połączeń |
| IDbTransactionInterceptor | Tworzenie transakcji Używanie istniejących transakcji Zatwierdzanie transakcji Wycofywanie transakcji Tworzenie i używanie punktów zapisywania Awarie transakcji |
Klasy DbCommandInterceptorpodstawowe , DbConnectionInterceptori DbTransactionInterceptor zawierają implementacje no-op 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 interceptora występują parami, gdzie pierwszy jest wywoływany przed rozpoczęciem operacji bazy danych, a drugi po jej zakończeniu. 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ń
Wskazówka
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 = await context.Blogs.TagWith("Use hint: robust plan").ToListAsync();
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:
- Przechwytywacz dziedziczy z DbCommandInterceptor, aby uniknąć konieczności implementowania każdej metody w interfejsie przechwytywacza.
- Przechwytywacz implementuje zarówno metody synchronizacyjne, jak i asynchroniczne. Dzięki temu ta sama wskazówka zapytania jest stosowana do synchronizacji i zapytań asynchronicznych.
- Przechwytywacz implementuje
Executingmetody, które są wywoływane przez EF Core z wygenerowanym SQL przed jego wysłaniem do bazy danych. Porównaj to z metodamiExecuted, które są wywoływane po powrocie wywołania z bazy danych.
Uruchomienie kodu w tym przykładzie powoduje wygenerowanie następującego wyniku, gdy zapytanie jest oznaczone tagiem:
-- 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łączeń na potrzeby uwierzytelniania usługi SQL Azure przy użyciu usługi AAD
Wskazówka
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;
}
}
Wskazówka
Microsoft.Data.SqlClient obsługuje teraz uwierzytelnianie usługi AAD za pośrednictwem parametrów 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
Wskazówka
Możesz pobrać zaawansowany przykład przechwytywania poleceń z usługi GitHub.
Przechwytniki ef Core mogą wykonywać następujące czynności:
- Powiedz EF Core, aby powstrzymało wykonywanie przechwytywanej operacji
- Zmiana wyniku operacji zgłoszonej z powrotem na platformę EF Core
W tym przykładzie pokazano przechwytywacz, który używa tych funkcji do działania jak prymitywna 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ć przez interceptor. 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 przechwytywacza
Ten przechwytator jest zachowujący stan: przechowuje identyfikator i tekst najnowszej wiadomości dziennej, którą zapytano, a także czas, kiedy to zapytanie zostało wykonane. Ze względu na ten stan potrzebujemy również blokady, ponieważ buforowanie wymaga użycia tego samego interceptora 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 zamiennik DbDataReader zawierający dane z pamięci podręcznej. 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 interceptor również manipuluje 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 interceptora Executed po wykonaniu. W tym momencie, jeśli wynik nie jest jeszcze czytnikiem w pamięci podręcznej, nowy identyfikator wiadomości i ciąg znaków są wyodrębniane z rzeczywistego czytnika i przechowywane w pamięci podręcznej do późniejszego 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 przechwytywacza 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 aż do wygaśnięcia limitu czasu, w którym to momencie baza danych jest ponownie odpytywana w celu uzyskania nowego komunikatu.
Przechwytywanie funkcji SaveChanges
Wskazówka
Przykład przechwytywania SaveChanges można pobrać na GitHubie.
Punkty przechwytywania SaveChanges i SaveChangesAsync są definiowane przez interfejs ISaveChangesInterceptor. Podobnie jak w przypadku innych interceptorów, klasa bazowa SaveChangesInterceptor z metodami no-op jest udostępniana dla wygody.
Wskazówka
Przechwytniki są potężne. Jednak w wielu przypadkach może być łatwiej zastąpić metodę SaveChanges lub użyć zdarzeń .NET dla SaveChanges dostępnych w DbContext.
Przykład: przechwytywanie SaveChanges do celów audytu
Funkcja SaveChanges może zostać przechwycona w celu utworzenia niezależnego rekordu inspekcji wprowadzonych zmian.
Uwaga / Notatka
Nie jest to niezawodne rozwiązanie do inspekcji. Zamiast tego jest to uproszczony przykład używany do zademonstrowania cech przechwytywania.
Kontekst aplikacji
Próbka do audytu 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żdej instancji DbContext zarejestrowano nowe wystąpienie interceptora. Jest to spowodowane tym, że interceptor auditowy zawiera stan związany 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; }
}
Przechwytywacz
Ogólną ideą audytu przy użyciu przechwytującego jest:
- Komunikat inspekcji jest tworzony na początku funkcji SaveChanges i jest zapisywany w bazie danych inspekcji
- 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 nadpisania ISaveChangesInterceptor.SavingChanges 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;
}
Nadpisanie zarówno metod synchronicznych, jak i asynchronicznych zapewnia, że inspekcja będzie odbywać się niezależnie od tego, czy SaveChanges lub SaveChangesAsync są wywoływane. Warto zauważyć, że samo przeciążenie asynchroniczne jest w stanie wykonać nieblokujące asynchroniczne operacje wejścia/wyjścia do bazy danych audytu. Możesz chcieć rzucić wyjątek z metody synchronicznej SavingChanges, aby upewnić się, że wszystkie operacje we/wy bazy danych są asynchroniczne. Następnie wymaga to, aby aplikacja zawsze wywoływała metodę SaveChangesAsync i nigdy 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 obiekt SaveChangesAudit z kolekcją obiektów EntityAudit, obejmującą jeden dla każdego dodawania, aktualizacji lub usunięcia. Następnie przechwytywacz wstawia te jednostki do bazy danych audytu.
Wskazówka
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 "Entity Framework Core 5.0.0 zainicjował 'BlogsContext' przy użyciu dostawcy 'Microsoft.EntityFrameworkCore.Sqlite' z opcjami: Brak".
Wykrywanie powodzenia
Jednostka audytu jest przechowywana w interceptorze, aby można było uzyskać do niej ponownie dostęp, gdy operacja SaveChanges powiedzie się lub nie. W przypadku sukcesu ISaveChangesInterceptor.SavedChanges lub ISaveChangesInterceptor.SavedChangesAsync jest wywoływane.
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 i EndTime, co oznacza, że te właściwości są oznaczone jako zmodyfikowane, dzięki czemu funkcja SaveChanges wyśle aktualizację do bazy danych audytu.
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ą wyjątek, który został zgłoszony.
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 audytu zawiera prostą aplikację konsolową, która dokonuje zmian w bazie danych blogów, a następnie przedstawia wyniki przeprowadzonego audytu.
// 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 = await context.Blogs.Include(e => e.Posts).SingleAsync();
blog.Name = "EF Core Blog";
context.Remove(blog.Posts.First());
blog.Posts.Add(new Post { Title = "EF Core 6.0!" });
await context.SaveChangesAsync();
}
// 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 await context.SaveChangesAudits.Include(e => e.Entities).ToListAsync())
{
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'.