Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Os interceptadores do EF Core (Entity Framework Core) permitem interceptação, modificação e/ou supressão de operações do EF Core. Isso inclui operações de banco de dados de baixo nível, como a execução de um comando, bem como operações de nível superior, como chamadas para SaveChanges.
Interceptadores são diferentes do registro e do diagnóstico, pois permitem modificar ou suprimir a operação interceptada. Registro em log simples ou Microsoft.Extensions.Logging são melhores opções para registro em log.
Interceptores são registrados por instância DbContext quando o contexto é configurado. Use um ouvinte de diagnóstico para obter as mesmas informações, mas para todas as instâncias de DbContext no processo.
Registrando interceptadores
Interceptores são registrados usando AddInterceptors, ao configurar uma instância do DbContext. Isso geralmente é feito em uma sobrescrição de DbContext.OnConfiguring. Por exemplo:
public class ExampleContext : BlogsContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(new TaggedQueryCommandInterceptor());
}
Como alternativa, AddInterceptors
pode ser chamado como parte de AddDbContext ou ao criar uma DbContextOptions instância para passar para o construtor DbContext.
Dica
OnConfigurando ainda é chamado quando AddDbContext é usado ou uma instância DbContextOptions é passada para o construtor DbContext. Isso torna o local ideal para aplicar a configuração de contexto, independentemente de como o DbContext é construído.
Os interceptadores geralmente são sem estado, o que significa que uma única instância de interceptador pode ser usada para todas as instâncias de DbContext. Por exemplo:
public class TaggedQueryCommandInterceptorContext : BlogsContext
{
private static readonly TaggedQueryCommandInterceptor _interceptor
= new TaggedQueryCommandInterceptor();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(_interceptor);
}
Cada instância do interceptor deve implementar uma ou mais interfaces derivadas de IInterceptor. Cada instância só deve ser registrada uma vez, mesmo que implemente várias interfaces de interceptação; O EF Core roteará eventos para cada interface conforme apropriado.
Interceptação de banco de dados
Observação
A interceptação de banco de dados só está disponível para provedores de banco de dados relacionais.
A interceptação de banco de dados de baixo nível é dividida nas três interfaces mostradas na tabela a seguir.
Interceptador | Operações de banco de dados interceptadas |
---|---|
IDbCommandInterceptor | Criando comandos Executando comandos Falhas de comando Descartando o DbDataReader do comando |
IDbConnectionInterceptor | Abertura e fechamento de conexões Falhas de conexão |
IDbTransactionInterceptor | Criando transações Usando transações existentes Confirmando transações Revertendo transações Criando e usando pontos de salvamento Falhas de transação |
As classes DbCommandInterceptor, DbConnectionInterceptor e DbTransactionInterceptor contêm implementações de no-op para cada método na interface correspondente. Use as classes base para evitar a necessidade de implementar métodos de interceptação não utilizados.
Os métodos em cada tipo de interceptador vêm em pares, com o primeiro sendo chamado antes da operação de banco de dados ser iniciada e o segundo após a conclusão da operação. Por exemplo, DbCommandInterceptor.ReaderExecuting é chamado antes de uma consulta ser executada e DbCommandInterceptor.ReaderExecuted é chamado depois que a consulta é enviada para o banco de dados.
Cada par de métodos tem variações assíncronas e de sincronização. Isso permite que operações de E/S assíncronas, como solicitar um token de acesso, aconteçam como parte da interceptação de uma operação assíncrona de banco de dados.
Exemplo: interceptação de comando para adicionar sugestões de consulta
Dica
Você pode baixar o exemplo do interceptador de comandos no GitHub.
Um IDbCommandInterceptor pode ser usado para modificar o SQL antes de ser enviado para o banco de dados. Este exemplo mostra como modificar o SQL para incluir uma dica de consulta.
Geralmente, a parte mais complicada da interceptação é determinar quando o comando corresponde à consulta que precisa ser modificada. Analisar o SQL é uma opção, mas tende a ser frágil. Outra opção é usar marcas de consulta EF Core para marcar cada consulta que deve ser modificada. Por exemplo:
var blogs1 = await context.Blogs.TagWith("Use hint: robust plan").ToListAsync();
Essa tag pode ser detectada no interceptor, já que sempre será incluída como um comentário na primeira linha do texto de comando. Ao detectar a tag, o SQL de consulta é modificado para adicionar a pista apropriada.
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)";
}
}
}
Aviso:
- O interceptor herda de DbCommandInterceptor para evitar a necessidade de implementar todos os métodos na interface do interceptor.
- O interceptor implementa métodos síncronos e assíncronos. Isso garante que a mesma dica de consulta seja aplicada a consultas assíncronas e sincronizadas.
- O interceptor implementa os
Executing
métodos que são chamados pelo EF Core com o SQL gerado antes de ser enviado para o banco de dados. Contraste isso com osExecuted
métodos, que são chamados após o retorno da chamada de banco de dados.
A execução do código neste exemplo gera o seguinte quando uma consulta é marcada:
-- Use hint: robust plan
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b] OPTION (ROBUST PLAN)
Por outro lado, quando uma consulta não é marcada, ela é enviada para o banco de dados sem modificação:
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
Exemplo: interceptação de conexão para autenticação do SQL Azure usando o AAD
Dica
Você pode baixar o exemplo do interceptador de conexão no GitHub.
Um IDbConnectionInterceptor pode ser usado para manipular o DbConnection antes de ser usado para se conectar ao banco de dados. Isso pode ser usado para obter um token de acesso do AAD (Azure Active Directory). Por exemplo:
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;
}
}
Dica
O Microsoft.Data.SqlClient agora dá suporte à autenticação do AAD por meio da cadeia de conexão. Consulte SqlAuthenticationMethod para obter mais informações.
Aviso
Observe que o interceptor é gerado se uma chamada de sincronização for feita para abrir a conexão. Isso ocorre porque não há nenhum método não assíncrono para obter o token de acesso e não há uma maneira universal e simples de chamar um método assíncrono do contexto não assíncrono sem correr o risco de deadlock.
Aviso
em algumas situações, o token de acesso pode não ser armazenado em cache automaticamente no Provedor de Token do Azure. Dependendo do tipo de token solicitado, talvez seja necessário implementar seu próprio cache aqui.
Exemplo: interceptação de comando avançada para armazenamento em cache
Dica
Você pode baixar o exemplo de interceptador de comando avançado do GitHub.
Os interceptadores do EF Core podem:
- Instrua o EF Core a suprimir a execução da operação interceptada.
- Alterar o resultado da operação informado de volta ao EF Core
Este exemplo mostra um interceptor que usa esses recursos para se comportar como um cache primitivo de segundo nível. Os resultados da consulta armazenada em cache são retornados para uma consulta específica, evitando uma viagem de ida e volta do banco de dados.
Aviso
Tome cuidado ao alterar o comportamento padrão do EF Core dessa maneira. O EF Core poderá se comportar de maneiras inesperadas se receber um resultado anormal que não possa ser processado corretamente. Além disso, este exemplo demonstra conceitos de interceptador; ele não se destina como um modelo para uma implementação robusta de cache de segundo nível.
Neste exemplo, o aplicativo executa frequentemente uma consulta para obter a "mensagem diária" mais recente:
async Task<string> GetDailyMessage(DailyMessageContext context)
=> (await context.DailyMessages.TagWith("Get_Daily_Message").OrderBy(e => e.Id).LastAsync()).Message;
Essa consulta é marcada para que possa ser facilmente detectada no interceptor. A ideia é consultar apenas o banco de dados para uma nova mensagem uma vez por dia. Em outras ocasiões, o aplicativo usará um resultado armazenado em cache. O exemplo usa um atraso de 10 segundos para simular um novo dia.
Estado do interceptor
Esse interceptador tem estado: armazena o identificador e o texto da mensagem diária mais recente pesquisada, além da hora em que essa consulta foi executada. Devido a esse estado, também precisamos de um bloqueio , pois o cache requer que o mesmo interceptor seja usado por várias instâncias de contexto.
private readonly object _lock = new object();
private int _id;
private string _message;
private DateTime _queriedAt;
Antes da execução
Executing
No método (ou seja, antes de fazer uma chamada de banco de dados), o interceptador detecta a consulta marcada e verifica se há um resultado armazenado em cache. Se esse resultado for encontrado, a consulta será suprimida e os resultados armazenados em cache serão usados.
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);
}
Observe como o código chama InterceptionResult<TResult>.SuppressWithResult e passa uma substituição DbDataReader que contém os dados armazenados em cache. Ao retornar esse InterceptionResult, ocorre a supressão da execução da consulta. Em vez disso, o leitor de substituição é usado pelo EF Core como os resultados da consulta.
Esse interceptor também manipula o texto do comando. Essa manipulação não é necessária, mas melhora a clareza nas mensagens de log. O texto do comando não precisa ser SQL válido, pois a consulta agora não será executada.
Após a execução
Se nenhuma mensagem armazenada em cache estiver disponível ou se tiver expirado, o código acima não suprimirá o resultado. O EF Core, portanto, executará a consulta normalmente. Em seguida, ele retornará ao método do Executed
interceptor após a execução. Neste ponto, se o resultado ainda não for um leitor armazenado em cache, a nova ID da mensagem e a cadeia de caracteres serão extraídas do leitor real e armazenadas em cache prontas para o próximo uso dessa consulta.
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;
}
Demonstração
O exemplo do interceptador de cache contém um aplicativo de console simples que consulta mensagens diárias para testar o cache:
// 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;
Isso resulta na seguinte saída:
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
Observe na saída de log que o aplicativo continua a usar a mensagem armazenada em cache até que o tempo limite expire, momento em que o banco de dados é consultado novamente para qualquer nova mensagem.
Interceptação do SaveChanges
Dica
Você pode baixar o exemplo do interceptador SaveChanges no GitHub.
Os pontos de interceptação SaveChanges e SaveChangesAsync são definidos pela interface ISaveChangesInterceptor. Quanto a outros interceptores, a SaveChangesInterceptor classe base com métodos no-op é fornecida como uma conveniência.
Dica
Interceptores são poderosos. No entanto, em muitos casos, pode ser mais fácil sobrescrever o método SaveChanges ou usar os eventos .NET para SaveChanges que são expostos no DbContext.
Exemplo: interceptação do SaveChanges para auditoria
SaveChanges pode ser interceptado para criar um registro de auditoria independente das alterações feitas.
Observação
Essa não se destina a ser uma solução de auditoria robusta. Em vez disso, é um exemplo simplista usado para demonstrar os recursos de interceptação.
O contexto do aplicativo
O exemplo de auditoria usa um DbContext simples com blogs e postagens.
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; }
}
Observe que uma nova instância do interceptador está registrada para cada uma das instâncias de DbContext. Isso se deve ao fato de que o interceptador de auditoria contém um estado vinculado à instância de contexto atual.
O contexto de auditoria
O exemplo também contém um segundo DbContext e um modelo usado para o banco de dados de auditoria.
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; }
}
O interceptador
A ideia geral para auditoria com o interceptor é:
- Uma mensagem de auditoria é criada no início do SaveChanges e é gravada no banco de dados de auditoria
- SaveChanges tem permissão para continuar
- Caso o SaveChanges seja bem-sucedido, a mensagem de auditoria será atualizada para indicar sucesso
- Se SaveChanges falhar, a mensagem de auditoria será atualizada para indicar a falha
O primeiro estágio é realizado antes de qualquer alteração ser enviada ao banco de dados, utilizando as substituições de ISaveChangesInterceptor.SavingChanges e 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;
}
Anular os métodos síncronos e assíncronos garante que a auditoria ocorra independentemente de SaveChanges
ou SaveChangesAsync
serem chamadas. Observe também que a sobrecarga de função assíncrona é, por si só, capaz de realizar operações de E/S assíncrona sem bloqueio no banco de dados de auditoria. Talvez você deseje lançar a partir do método SavingChanges
para assegurar que todas as E/S do banco de dados sejam assíncronas. Em seguida, isso requer que o aplicativo sempre chame SaveChangesAsync
e nunca SaveChanges
.
A mensagem de auditoria
Cada método interceptor tem um eventData
parâmetro que fornece informações contextuais sobre o evento que está sendo interceptado. Nesse caso, o DbContext do aplicativo atual é incluído nas informações do evento, sendo então utilizado para criar uma mensagem de auditoria.
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}' ");
}
O resultado é uma SaveChangesAudit
entidade com uma coleção de EntityAudit
entidades, uma para cada inserção, atualização ou exclusão. Em seguida, o interceptador insere essas entidades no banco de dados de auditoria.
Dica
ToString é substituído em cada classe de dados de evento EF Core para gerar a mensagem de log equivalente para o evento. Por exemplo, a chamada ContextInitializedEventData.ToString
gera "Entity Framework Core 5.0.0 inicializado 'BlogsContext' usando o provedor 'Microsoft.EntityFrameworkCore.Sqlite' com opções: Nenhuma".
Detectando êxito
A entidade de auditoria é armazenada no interceptor para que possa ser acessada novamente quando SaveChanges tiver êxito ou falhar. Para o sucesso, ISaveChangesInterceptor.SavedChanges ou ISaveChangesInterceptor.SavedChangesAsync é necessário.
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;
}
A entidade de auditoria é anexada ao contexto de auditoria, pois já existe no banco de dados e precisa ser atualizada. Em seguida, definimos Succeeded
e EndTime
, o que marca essas propriedades como modificadas para que SaveChanges envie uma atualização para o banco de dados de auditoria.
Identificação de falhas
A falha é tratada da mesma maneira que o sucesso, mas no método ISaveChangesInterceptor.SaveChangesFailed ou ISaveChangesInterceptor.SaveChangesFailedAsync. Os dados do evento contêm a exceção que foi gerada.
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);
}
Demonstração
O exemplo de auditoria contém um aplicativo de console simples que faz alterações no banco de dados de blogs e depois mostra a auditoria criada.
// 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}");
}
}
}
O resultado mostra o conteúdo do banco de dados de auditoria:
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'.