在 EF Core 中使用诊断侦听器

小窍门

可以从 GitHub 下载本文的示例

诊断侦听器允许侦听当前 .NET 进程中发生的任何 EF Core 事件。 该 DiagnosticListener 类是 .NET 中通用机制 的一部分,用于从正在运行的应用程序获取诊断信息。

诊断侦听器不适合从单个 DbContext 实例获取事件。 EF Core 拦截器按每个上下文进行注册,提供对相同事件的访问权限。

诊断侦听器不用于日志记录。 请考虑使用 简单的日志记录Microsoft.Extensions.Logging 进行日志记录

示例:观察诊断事件

解决 EF Core 事件是一个需要两个步骤的过程。 首先,必须创建一个观察者用于DiagnosticListener本身:

public class DiagnosticObserver : IObserver<DiagnosticListener>
{
    public void OnCompleted()
        => throw new NotImplementedException();

    public void OnError(Exception error)
        => throw new NotImplementedException();

    public void OnNext(DiagnosticListener value)
    {
        if (value.Name == DbLoggerCategory.Name) // "Microsoft.EntityFrameworkCore"
        {
            value.Subscribe(new KeyValueObserver());
        }
    }
}

该方法 OnNext 查找来自 EF Core 的 DiagnosticListener。 此侦听器的名称为“Microsoft.EntityFrameworkCore”,可以如下所示从 DbLoggerCategory 类中获取。

然后必须将此观察者全局注册,例如,在应用程序的 Main 方法中:

DiagnosticListener.AllListeners.Subscribe(new DiagnosticObserver());

其次,找到 EF Core DiagnosticListener 后,会创建一个新的键值观察程序,以便订阅具体的 EF Core 事件。 例如:

public class KeyValueObserver : IObserver<KeyValuePair<string, object>>
{
    public void OnCompleted()
        => throw new NotImplementedException();

    public void OnError(Exception error)
        => throw new NotImplementedException();

    public void OnNext(KeyValuePair<string, object> value)
    {
        if (value.Key == CoreEventId.ContextInitialized.Name)
        {
            var payload = (ContextInitializedEventData)value.Value;
            Console.WriteLine($"EF is initializing {payload.Context.GetType().Name} ");
        }

        if (value.Key == RelationalEventId.ConnectionOpening.Name)
        {
            var payload = (ConnectionEventData)value.Value;
            Console.WriteLine($"EF is opening a connection to {payload.Connection.ConnectionString} ");
        }
    }
}

每个 EF Core 事件都会通过键/值对来调用OnNext方法。 密钥是事件的名称,可以从以下项之一获取:

  • CoreEventId 用于所有 EF Core 数据库提供程序通用的事件
  • RelationalEventId 用于所有关系数据库提供程序通用的事件
  • 与当前数据库提供程序相关的事件的类似类。 例如,SqlServerEventId 是 SQL Server 提供程序的示例。

键值对中的值是与事件相关的负载类型。 可以预期的有效负载类型记录在这些事件类定义的每个事件中。

例如,上述代码处理 ContextInitialized 事件和 ConnectionOpening 事件。 对于其中第一个,有效负载为 ContextInitializedEventData。 第二个是 ConnectionEventData

小窍门

ToString 方法在每个 EF Core 事件数据类中被重定义,以生成事件的相应日志消息。 例如,调用 ContextInitializedEventData.ToString 会生成“Entity Framework Core 5.0.0 使用提供程序‘Microsoft.EntityFrameworkCore.Sqlite’和选项‘无’初始化‘BlogsContext’”。

该示例包含一个简单的控制台应用程序,该应用程序对博客数据库进行更改并输出遇到的诊断事件。

public static async Task Main()
{
    DiagnosticListener.AllListeners.Subscribe(new DiagnosticObserver());

    using (var context = new BlogsContext())
    {
        await context.Database.EnsureDeletedAsync();
        await context.Database.EnsureCreatedAsync();

        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();
    }

此代码的输出显示检测到的事件:

EF is initializing BlogsContext
EF is opening a connection to Data Source=blogs.db;Mode=ReadOnly
EF is opening a connection to DataSource=blogs.db
EF is opening a connection to Data Source=blogs.db;Mode=ReadOnly
EF is opening a connection to DataSource=blogs.db
EF is opening a connection to DataSource=blogs.db
EF is opening a connection to DataSource=blogs.db
EF is initializing BlogsContext
EF is opening a connection to DataSource=blogs.db
EF is opening a connection to DataSource=blogs.db