Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Перехватчики Entity Framework Core (EF Core) позволяют перехватывать, изменять и/или подавлять операции EF Core. Это включает низкоуровневые операции базы данных, такие как выполнение команды, а также операции более высокого уровня, такие как вызовы SaveChanges.
Перехватчики отличаются от ведения журнала и диагностики тем, что они позволяют изменять либо подавлять перехватываемую операцию. Простое ведение журнала или Microsoft.Extensions.Logging лучше всего подходит для ведения журнала.
Перехватчики регистрируются для каждого экземпляра DbContext при настройке контекста. Используйте прослушиватель диагностики для получения той же информации, но для всех экземпляров DbContext, участвующих в процессе.
Регистрация перехватчиков
Перехватчики регистрируются с помощью AddInterceptors при настройке экземпляра DbContext. Обычно это делается при переопределении DbContext.OnConfiguring. Рассмотрим пример.
public class ExampleContext : BlogsContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(new TaggedQueryCommandInterceptor());
}
Кроме того, AddInterceptors
можно вызывать как часть AddDbContext или при создании экземпляра DbContextOptions для передачи в конструктор DbContext.
Подсказка
Метод OnConfiguring по-прежнему вызывается, когда используется AddDbContext или экземпляр DbContextOptions передается в конструктор DbContext. Это делает его идеальным местом для применения конфигурации контекста независимо от того, как создается DbContext.
Перехватчики часто являются бессерверными, что означает, что для всех экземпляров DbContext можно использовать один экземпляр перехватчика. Рассмотрим пример.
public class TaggedQueryCommandInterceptorContext : BlogsContext
{
private static readonly TaggedQueryCommandInterceptor _interceptor
= new TaggedQueryCommandInterceptor();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(_interceptor);
}
Каждый экземпляр перехватчика должен реализовывать один или несколько интерфейсов, производных от IInterceptor. Каждый экземпляр должен быть зарегистрирован только один раз, даже если он реализует несколько интерфейсов перехвата; EF Core перенаправит события для каждого интерфейса соответствующим образом.
Перехват базы данных
Замечание
Перехват базы данных доступен только для поставщиков реляционных баз данных.
Перехват базы данных низкого уровня разделен на три интерфейса, показанные в следующей таблице.
Перехватчик | Операции с базой данных перехвачены |
---|---|
IDbCommandInterceptor | Создание команд Выполнение команд Сбои команд Удаление DbDataReader команды |
IDbConnectionInterceptor | Сбои при открытии и закрытии подключений |
IDbTransactionInterceptor | Создание транзакций Использование существующих транзакций Фиксация транзакций Откат транзакций Создание и использование контрольных точекСбои транзакций |
Базовые классы DbCommandInterceptor, DbConnectionInterceptor, и DbTransactionInterceptor содержат no-op реализации для каждого метода в соответствующем интерфейсе. Используйте базовые классы, чтобы избежать необходимости реализации неиспользуемых методов перехвата.
Методы каждого типа перехватчика организованы в пары, где первый метод вызывается перед началом операции с базой данных, а второй вызывается после ее завершения. Например, DbCommandInterceptor.ReaderExecuting вызывается перед выполнением запроса и DbCommandInterceptor.ReaderExecuted вызывается после отправки запроса в базу данных.
Каждая пара методов имеет синхронные и асинхронные вариации. Это позволяет асинхронным операциям ввода-вывода, таким как запрос маркера доступа, происходить в рамках перехвата асинхронной операции базы данных.
Пример: Командный перехват для добавления подсказок к запросу
Подсказка
Вы можете загрузить пример перехватчика команд с GitHub.
Можно IDbCommandInterceptor использовать для изменения SQL перед отправкой в базу данных. В этом примере показано, как изменить SQL для включения указания запроса.
Часто самая сложная часть перехвата определяет, когда команда соответствует запросу, который необходимо изменить. Анализ SQL является одним из вариантов, но, как правило, хрупким. Другим вариантом является использование тегов запросов EF Core для маркировки каждого запроса, который следует изменить. Рассмотрим пример.
var blogs1 = await context.Blogs.TagWith("Use hint: robust plan").ToListAsync();
Затем этот тег можно обнаружить в перехватчике, так как он всегда будет включен в качестве комментария в первой строке текста команды. При обнаружении тега sql-запрос изменяется, чтобы добавить соответствующее указание:
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)";
}
}
}
Примечание.
- Перехватчик наследует от DbCommandInterceptor, чтобы не нужно было реализовывать каждый метод в интерфейсе перехватчика.
- Перехватчик реализует как синхронные, так и асинхронные методы. Это гарантирует, что для синхронизации и асинхронных запросов применяется та же подсказка запроса.
- Перехватчик реализует методы
Executing
, которые вызываются EF Core с созданным SQL-кодом перед отправкой его в базу данных. Сравните это сExecuted
методами, которые вызываются после возврата вызова базы данных.
Выполнение кода в этом примере создает следующее при теге запроса:
-- Use hint: robust plan
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b] OPTION (ROBUST PLAN)
С другой стороны, если запрос не помечен, он отправляется в базу данных без изменений:
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
Пример. Перехват подключений для проверки подлинности SQL Azure с помощью AAD
Подсказка
Вы можете скачать пример перехватчика подключений из GitHub.
IDbConnectionInterceptor можно использовать для управления DbConnection перед его использованием для подключения к базе данных. Это можно использовать для получения токена доступа Azure Active Directory (AAD). Рассмотрим пример.
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;
}
}
Подсказка
Microsoft.Data.SqlClient теперь поддерживает проверку подлинности AAD через строку подключения. См. SqlAuthenticationMethod для получения дополнительной информации.
Предупреждение
Обратите внимание, что перехватчик выдает ошибку, если при открытии подключения выполняется синхронный вызов. Это связано с тем, что нет асинхронного метода для получения маркера доступа и нет универсального и простого способа вызова асинхронного метода из несинхронного контекста без риска взаимоблокировки.
Предупреждение
В некоторых ситуациях маркер доступа может не кэшироваться автоматически поставщиком маркеров Azure. В зависимости от типа запрошенного маркера, здесь может потребоваться реализовать собственное кэширование.
Пример. Расширенный перехват команд для кэширования
Подсказка
Вы можете скачать пример расширенного перехватчика команд из GitHub.
Перехватчики EF Core могут:
- Сообщите EF Core, чтобы отключить выполнение операции, перехватываемой
- Измените результат операции, сообщаемый обратно в EF Core
В этом примере показан перехватчик, использующий эти функции для поведения как примитивный кэш второго уровня. Кэшированные результаты запроса возвращаются для определенного запроса, избегая обхода базы данных.
Предупреждение
Будьте осторожны при изменении поведения по умолчанию в EF Core таким образом. EF Core может вести себя неожиданно, если он получает ненормальный результат, который он не может правильно обрабатывать. Кроме того, в этом примере демонстрируются концепции перехватчиков; он не предназначен в качестве шаблона для надежной реализации кэша второго уровня.
В этом примере приложение часто выполняет запрос, чтобы получить последнее "ежедневное сообщение":
async Task<string> GetDailyMessage(DailyMessageContext context)
=> (await context.DailyMessages.TagWith("Get_Daily_Message").OrderBy(e => e.Id).LastAsync()).Message;
Этот запрос помечен так, чтобы его можно было легко обнаружить в перехватчике. Идея заключается в том, чтобы запрашивать базу данных для нового сообщения только один раз в день. В других случаях приложение будет использовать кэшированный результат. (В примере используется задержка в течение 10 секунд, чтобы имитировать новый день.)
Состояние перехватчика
Этот перехватчик является состоянием: он сохраняет идентификатор и текст последнего ежедневного сообщения, по которому был выполнен запрос, а также время выполнения этого запроса. Из-за этого состояния нам также нужен замок, поскольку кэширование требует, чтобы один и тот же перехватчик использовался в нескольких экземплярах контекста.
private readonly object _lock = new object();
private int _id;
private string _message;
private DateTime _queriedAt;
Перед выполнением
В методе Executing
(т. е. перед вызовом базы данных) перехватчик обнаруживает помеченный запрос, а затем проверяет наличие кэшированного результата. Если такой результат найден, запрос подавляется и кэшируются результаты вместо него.
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);
}
Обратите внимание, как код вызывает InterceptionResult<TResult>.SuppressWithResult и передает замену DbDataReader , содержащую кэшированные данные. Этот результат перехвата затем возвращается, что вызывает подавление выполнения запроса. Заменяющий ридер вместо этого используется EF Core для предоставления результатов запроса.
Этот перехватчик также изменяет текст команды. Эта манипуляция не требуется, но улучшает ясность в сообщениях журнала. Текст команды не должен быть допустимым SQL, так как запрос теперь не будет выполнен.
После выполнения
Если кэшированное сообщение недоступно или истекло, приведенный выше код не подавляет результат. Следовательно, EF Core будет выполнять запрос как обычный. Затем, после выполнения, он вернется к методу перехватчика Executed
. На этом этапе, если результат еще не является кэшируемым средством чтения, новый идентификатор сообщения и строка извлекаются из реального средства чтения и кэшируются для следующего использования этого запроса.
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;
}
Демонстрация
Пример использования перехватчика кэширования включает простое консольное приложение, которое отправляет запросы на получение ежедневных сообщений для тестирования действия кэширования:
// 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;
Результат должен быть таким:
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
Обратите внимание на выходные данные журнала, что приложение продолжает использовать кэшированное сообщение до истечения срока ожидания, после чего база данных запрашивается повторно для любого нового сообщения.
Перехват функции SaveChanges
Подсказка
Вы можете скачать пример перехватчика SaveChanges из GitHub.
SaveChanges и SaveChangesAsync точки перехвата определяются интерфейсом ISaveChangesInterceptor . Что касается других перехватчиков, базовый класс SaveChangesInterceptor с методами no-op предоставляется для удобства.
Подсказка
Перехватчики мощны. Однако во многих случаях может быть проще переопределить метод SaveChanges или использовать события .NET для SaveChanges , предоставляемые в DbContext.
Пример: перехватчик SaveChanges для проверки
SaveChanges можно интерцептировать для создания независимой аудиторской записи о внесенных изменениях.
Замечание
Это не предназначено для надежного решения аудита. Скорее это простой пример, используемый для демонстрации функций перехвата.
Контекст приложения
Образец для аудита использует простой DbContext с блогами и публикациями.
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; }
}
Обратите внимание, что для каждого экземпляра DbContext создается и регистрируется новый объект перехватчика. Это связано с тем, что перехватчик аудита содержит состояние, связанное с текущим экземпляром контекста.
Контекст аудита
Пример также содержит вторую базу данных DbContext и модель, используемую для базы данных аудита.
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; }
}
Перехватчик
Идея использования перехватчика для аудита заключается в следующем:
- Сообщение аудита создается в начале SaveChanges и записывается в базу данных аудита.
- Разрешено продолжить выполнение SaveChanges.
- Если SaveChanges успешно выполнена, сообщение аудита обновляется, чтобы указать успешность
- Если выполнение SaveChanges завершается ошибкой, сообщение аудита обновляется, чтобы указать на провал.
Первый этап обрабатывается перед отправкой изменений в базу данных с использованием переопределений ISaveChangesInterceptor.SavingChanges и 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;
}
Переопределение как синхронных, так и асинхронных методов гарантирует, что аудит будет выполняться независимо от того, вызывается ли SaveChanges
или SaveChangesAsync
. Обратите внимание, что асинхронная перегрузка сама по себе может выполнять неблокирующие асинхронные операции ввода-вывода в базу данных аудита. Возможно, вы хотите выдать исключение из синхронного метода SavingChanges
, чтобы убедиться, что все операции ввода-вывода базы данных асинхронны. Затем это требует, чтобы приложение всегда вызывало SaveChangesAsync
и никогда не вызывало SaveChanges
.
Сообщение аудита
Каждый метод перехватчика имеет eventData
параметр, предоставляющий контекстную информацию о перехватываемом событии. В этом случае текущее приложение DbContext включается в данные события, которые затем используются для создания сообщения аудита.
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}' ");
}
Результатом является сущность SaveChangesAudit
с коллекцией сущностей EntityAudit
, каждая из которых соответствует вставке, обновлению или удалению. Затем перехватчик вставляет эти сущности в базу данных аудита.
Подсказка
ToString переопределяется в каждом классе данных событий EF Core, чтобы создать эквивалентное сообщение журнала для события. Например, вызов ContextInitializedEventData.ToString
генерирует сообщение "Entity Framework Core 5.0.0 инициализировал 'BlogsContext' с помощью поставщика 'Microsoft.EntityFrameworkCore.Sqlite' с параметрами: Никакие".
Обнаружение успеха
Сущность аудита хранится на перехватчике, чтобы к ней можно было снова получить доступ после завершения SaveChanges, будь то успешное завершение или сбой. Для достижения успеха вызывается ISaveChangesInterceptor.SavedChanges или ISaveChangesInterceptor.SavedChangesAsync.
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;
}
Сущность аудита присоединена к контексту аудита, так как она уже существует в базе данных и должна быть обновлена. Затем мы задали Succeeded
и EndTime
, пометили эти свойства как измененные, таким образом SaveChanges отправит обновление в базу данных аудита.
Обнаружение сбоя
Сбой обрабатывается почти так же, как и успех, но в методах ISaveChangesInterceptor.SaveChangesFailed или ISaveChangesInterceptor.SaveChangesFailedAsync. Данные события содержат исключение, которое было вызвано.
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);
}
Демонстрация
Пример аудита содержит простое консольное приложение, которое вносит изменения в базу данных блогов, а затем отображает созданный аудит.
// 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}");
}
}
}
Результат показывает содержимое базы данных аудита:
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'.