Uso do DiagnosticListener no EF Core

Dica

É possível baixar a amostra deste artigo no GitHub.

Os ouvintes de diagnóstico permitem ouvir qualquer evento do EF Core que ocorra no processo .NET atual. A classe DiagnosticListener faz parte de um mecanismo comum em todo o .NET para obter informações de diagnóstico de aplicativos em execução.

Os ouvintes de diagnóstico não são adequados para obter eventos de uma única instância do DbContext. Os interceptadores do EF Core fornecem acesso aos mesmos eventos com registro por contexto.

Os ouvintes de diagnóstico não foram projetados para registro em log. Considere usar registro em log simples ou Microsoft.Extensions.Logging para registro em log.

Exemplo: observação de eventos de diagnóstico

A resolução de eventos do EF Core é um processo de duas etapas. Primeiro, um observador para o próprio DiagnosticListener deve ser criado:

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

O método OnNext procura o DiagnosticListener proveniente do EF Core. Esse ouvinte tem o nome "Microsoft.EntityFrameworkCore", que pode ser obtido da classe DbLoggerCategory, conforme mostrado.

Esse observador deve então ser registrado globalmente, por exemplo, no método Main do aplicativo:

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

Em segundo lugar, depois que o DiagnosticListener do EF Core é encontrado, um novo observador de valor-chave é criado para assinar os eventos reais do EF Core. Por exemplo:

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

O método OnNext é chamado desta vez com um par chave/valor para cada evento EF Core. A chave é o nome do evento, que pode ser obtido de um dos:

  • CoreEventId para eventos comuns a todos os provedores de banco de dados EF Core
  • RelationalEventId para eventos comuns a todos os provedores de banco de dados relacional
  • Uma classe semelhante para eventos específicos do provedor de banco de dados atual. Por exemplo, SqlServerEventId para o provedor do SQL Server.

O valor do par chave/valor é um tipo de conteúdo específico para o evento. O tipo de conteúdo a ser esperado está documentado em cada evento definido nessas classes de evento.

Por exemplo, o código acima manipula o ContextInitialized e os eventos de ConnectionOpening. Para o primeiro deles, o conteúdo é ContextInitializedEventData. Para o segundo, é ConnectionEventData.

Dica

ToString é substituído em cada classe de dados de evento do EF Core a fim de gerar a mensagem de log equivalente para o evento. Por exemplo, chamar ContextInitializedEventData.ToString gera "O Entity Framework Core 5.0.0 iniciou 'BlogsContext' usando o provedor 'Microsoft.EntityFrameworkCore.Sqlite' com opções: Nenhum".

A amostra contém um aplicativo de console simples que faz alterações no banco de dados de blog e imprime os eventos de diagnóstico encontrados.

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

    using (var context = new BlogsContext())
    {
        context.Database.EnsureDeleted();
        context.Database.EnsureCreated();

        context.Add(
            new Blog { Name = "EF Blog", Posts = { new Post { Title = "EF Core 3.1!" }, new Post { Title = "EF Core 5.0!" } } });

        context.SaveChanges();
    }

    using (var context = new BlogsContext())
    {
        var blog = context.Blogs.Include(e => e.Posts).Single();

        blog.Name = "EF Core Blog";
        context.Remove(blog.Posts.First());
        blog.Posts.Add(new Post { Title = "EF Core 6.0!" });

        context.SaveChanges();
    }

A saída desse código mostra os eventos detectados:

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