Entity Framework Core 中的日志记录与拦截器
Posted JimCarter
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Entity Framework Core 中的日志记录与拦截器相关的知识,希望对你有一定的参考价值。
https://docs.microsoft.com/zh-cn/ef/core/logging-events-diagnostics/
文章目录
1.机制
efcore包含一些用于生成日志、响应时间和或者诊断结果的机制。具体见下表:
机制名称 | 支持异步 | 范围 | 进行配置的位置 | 用途 |
---|---|---|---|---|
简单的日志记录 | 否 | 上下文 | 数据库上下文 | 开发时日志记录 |
Microsoft.Extensions.Logging | 否 | 上下文 | DI或数据库上下文 | 生产日志记录 |
事件 | 否 | 上下文 | 任何位置 | 对 EF 事件做出反应 |
拦截器 | 是 | 上下文 | 数据库上下文 | 进行 EF 操作 |
诊断侦听器 | 否 | 进程 | 全局 | 应用程序诊断 |
2. 简单的日志记录
直接在OnConfiguring
里配置LogTo
即可:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.LogTo(Console.WriteLine);
//或
optionsBuilder.LogTo(msg=>Console.WriteLine(msg));
}
2.1 日志的详细信息
默认efcore不会将数据的值包含在异常消息中,因为这类数据可能是敏感的。但是在调试时如果能够看到这些值将是非常非常有用的,通过EnableSensitiveDataLogging()
启用此功能:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.LogTo(Console.WriteLine)
.EnableSensitiveDataLogging();
出于性能方面的考虑,efcore不会将每一次对数据库的调用都用try-catch包裹起来。但这样有时候遇到问题时会很难诊断,特别是当数据库返回null值而模型却不允许null的时候。可以使用EnableDetailedErrors
方法,让efcore返回具体的错误信息:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.LogTo(Console.WriteLine)
.EnableDetailedErrors();
2.2 日志过滤
- 通过LogTo的第二个参数控制日志级别:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);
- 将EventId(可从
CoreEventId
类或RelationalEventId
类获取)传入LogTo的第二个参数,以输出指定类型的日志:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.LogTo(Console.WriteLine, new[] { CoreEventId.ContextDisposed, CoreEventId.ContextInitialized });
- 也可以根据消息的类别进行过滤
每条日志消息都分配到一个命名的分层记录器类别。 类别为:
类别 | 说明 |
---|---|
Microsoft.EntityFrameworkCore | 所有 EF Core 消息 |
Microsoft.EntityFrameworkCore.Database | 所有数据库交互 |
Microsoft.EntityFrameworkCore.Database.Connection | 使用数据库连接 |
Microsoft.EntityFrameworkCore.Database.Command | 使用数据库命令 |
Microsoft.EntityFrameworkCore.Database.Transaction | 使用数据库事务 |
Microsoft.EntityFrameworkCore.Update | 保存实体,排除数据库交互 |
Microsoft.EntityFrameworkCore.Model | 所有模型和元数据交互 |
Microsoft.EntityFrameworkCore.Model.Validation | 模型验证 |
Microsoft.EntityFrameworkCore.Query | 查询,排除数据库交互 |
Microsoft.EntityFrameworkCore.Infrastructure | 常规事件,例如上下文创建 |
Microsoft.EntityFrameworkCore.Scaffolding | 数据库反向工程 |
Microsoft.EntityFrameworkCore.Migrations | 迁移 |
Microsoft.EntityFrameworkCore.ChangeTracking | 更改跟踪交互 |
比如记录数据库交互的日志:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.LogTo(Console.WriteLine, new[] { DbLoggerCategory.Database.Name });
- 自定义过滤器
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.LogTo(
Console.WriteLine,
(eventId, logLevel) => logLevel >= LogLevel.Information
|| eventId == RelationalEventId.ConnectionOpened
|| eventId == RelationalEventId.ConnectionClosed);
2.3 设置日志的内容和格式
默认日志内容是多行显示,第一行包括Loglevel、本地时间、EventId、消息类别,第二行是具体内容:
info: 10/6/2020 10:52:45.581 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE "Blogs" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Blogs" PRIMARY KEY AUTOINCREMENT,
"Name" INTEGER NOT NULL
);
dbug: 10/6/2020 10:52:45.582 RelationalEventId.TransactionCommitting[20210] (Microsoft.EntityFrameworkCore.Database.Transaction)
Committing transaction.
dbug: 10/6/2020 10:52:45.585 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
Committed transaction.
设置DbContextLoggerOptions.DefaultWithUtcTime
使用UTC时间和单行显示
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.LogTo(
Console.WriteLine,
LogLevel.Debug,
DbContextLoggerOptions.UtcTime | DbContextLoggerOptions.SingleLine);
3. 使用Microsoft.Extensions.Logging
默认已支持
4. 事件
efcore包含有以下几个事件
事件 | 引入的efcore版本 | 触发时间 |
---|---|---|
DbContext.SavingChanges | 5.0 | 在调用SaveChanges 或SaveChangesAsync 前 |
DbContext.SavedChanges | 5.0 | 成功调用SaveChanges 或 SaveChangesAsync 之后 |
DbContext.SaveChangesFailed | 5.0 | 调用SaveChanges 或 SaveChangesAsync 失败时 |
ChangeTracker.Tracked | 2.1 | context首次跟踪实体后 |
ChangeTracker.StateChanged | 2.1 | 当被跟踪的实体更改其状态时(首次进行跟踪时不会触发) |
示例:
public interface IHasTimestamps
{
DateTime? Added { get; set; }
DateTime? Deleted { get; set; }
DateTime? Modified { get; set; }
}
public BlogsContext()
{
ChangeTracker.StateChanged += UpdateTimestamps;
ChangeTracker.Tracked += UpdateTimestamps;
}
private static void UpdateTimestamps(object sender, EntityEntryEventArgs e)
{
if (e.Entry.Entity is IHasTimestamps entityWithTimestamps)
{
switch (e.Entry.State)
{
case EntityState.Deleted:
entityWithTimestamps.Deleted = DateTime.UtcNow;
Console.WriteLine($"Stamped for delete: {e.Entry.Entity}");
break;
case EntityState.Modified:
entityWithTimestamps.Modified = DateTime.UtcNow;
Console.WriteLine($"Stamped for update: {e.Entry.Entity}");
break;
case EntityState.Added:
entityWithTimestamps.Added = DateTime.UtcNow;
Console.WriteLine($"Stamped for insert: {e.Entry.Entity}");
break;
}
}
}
5. 拦截器
可以拦截、修改、禁止efcore的操作。包括低级的数据库操作(如执行命令)和高级的数据库操作(如对SaveChanges的调用)。
5.1 注册拦截器
在OnConfiguring
里进行配置
public class ExampleContext : BlogsContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(new TaggedQueryCommandInterceptor());
}
5.2 实现数据库拦截器
低级别的数据库拦截涉及到三个接口:
接口 | efcore自带实现基类 | 已截获数据库操作 |
---|---|---|
IDbCommandInterceptor | DbCommandInterceptor | 创建命令 执行命令 执行失败 |
IDbConnectionInterceptor | DbConnectionInterceptor | 打开/关闭连接 连接失败 |
IDbTransactionInterceptor | DbTransactionInterceptor | 创建事务 提交事务 回退事务 创建和使用保存点 事务失败 |
然后使用TagWith
方法对查询进行标记:
var blogs1 = context.Blogs.TagWith("Use hint: robust plan").ToList();
创建上面的TaggedQueryCommandInterceptor
拦截器:
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)";
}
}
}
拦截器同时实现了同步和异步的方法,可应用到同步或异步查询中。此时生成的sql如下:
-- Use hint: robust plan
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b] OPTION (ROBUST PLAN)
5.2.1 示例
需要实现的功能:当查询前先查下缓存,如有有缓存则直接返回。否则将从数据库查询出来的数据存放到缓存中。
以下示例用来获取最新的每天消息:
- 首先给查询打tag:
async Task<string> GetDailyMessage(DailyMessageContext context)
=> (await context.DailyMessages.TagWith("Get_Daily_Message").OrderBy(e => e.Id).LastAsync()).Message;
- 设置拦截器需要的一些参数
public class CachingCommandInterceptor : DbCommandInterceptor
{
private readonly object _lock = new object();
private int _id;
private string _message;
private DateTime _queriedAt;
public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command,CommandEventData eventData,InterceptionResult<DbDataReader> result,CancellationToken cancellationToken = default)
{
//...
}
public override async ValueTask<DbDataReader> ReaderExecutedAsync(DbCommand command,CommandExecutedEventData eventData,DbDataReader result,CancellationToken cancellationToken = default)
{
//...
}
}
因为拦截器是全局的而且可以应用到多个dbcotext上,所以需要定义一个锁。
3. 在数据库查询执行之前,拦截器根据之前打的tag来定位具体的查询,然后检查是否有缓存
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))//查询10s以内的数据
{
//因为sql不会到数据库执行,所以可以加一些自定义的注释
command.CommandText = "-- Get_Daily_Message: Skipping DB call; using cache.";
result = InterceptionResult<DbDataReader>.SuppressWithResult(new CachedDailyMessageDataReader(_id, _message));
}
}
}
return new ValueTask<InterceptionResult<DbDataReader>>(result);
}
- 如果没有发现缓存则会执行到
ReaderExecutedAsync
方法
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;
}
5.3 对OnSaveChanges
的拦截
应用场景:记录某些人对数据做了哪些更改时。
实现方法:有两个数据库上下文,用户对BlogsContext
进行更改操作,并配置拦截。AuditContext
存储用户的具体操作。
- 首先配置
BlogsContext
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; }
}
主要是配置拦截器
2. AuditContext
无须额外设置
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; }
}
AuditingInterceptor
的实现逻辑
在SaveChanges
时就记录数据,并写入数据库。如果保存失败,则记录操作失败。如果保存成功,则记录操作成功。
实现ISaveChangesInterceptor
接口,实现SavingChangesAsync
、SavedChangesAsync
、SaveChangesFailedAsync
三个异步方法和对应的同步方法。
//保存时
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 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;
}
//保存失败时
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 auditContextEntity Framework Core 2 中的 ConnectionString 生成器
根据 ASP.NET Core 和 Entity Framework Core 中的条件禁用 [必需] 属性
Entity Framework Core 中的动态数据模型
Entity Framework Core 中的动态查询执行