Logs e Consultas LINQ to SQL

27 minuto(s) de leitura - July 01, 2018

01

Fala pessoal, tudo bem?! 💚

Introdução

Bom para começarmos nosso pequeno artigo, vamos falar um pouco sobre o "EU TER", é fundamental que todo desenvolvedor, ou integrador de software tenha o controle de tudo ou quase tudo que acontece no banco. é imprescindível que não tenhamos esse controle. É uma forma de saber se as consultas estão sendo geradas corretamente, mas em outra oportunidade iremos falar mais sobre isso, enfim ..., como o objetivo maior de nosso artigo é mostrar como visualizar/capturar os comandos enviados para o banco, nada mais justo que utilizarmos o EntityFramework Core para isso 😊.

O EF Core fornece já um conjunto de opções para que possamos verificar as saídas SQL, vale a pena ressaltar que para o SQL Server temos o magnífico SQL Server Profiler, monitor de instruções SQL em tempo real, ótimo para saber quais querys por exemplo consumiram mais tempo.

Iremos apresentar aqui 2 opções de Logs (1-Log no console do aplicativo, 2-Log em uma variável) e criaremos uma extensão para projetar o SQL de uma consulta LINQ (Queryable).

Estrutura de nosso projeto

Class Blog

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime Date { get; set; } 
}

Nosso DbContext

public class SampleContext : DbContext
{ 
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var sqlConnectionStringBuilder = "Server=(localdb)\\mssqllocaldb;Database=ExemploArtigo;Integrated Security=True;"; 
        optionsBuilder.UseSqlServer(sqlConnectionStringBuilder); 
        base.OnConfiguring(optionsBuilder);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>();
    }
}

Até aqui tudo bem, temos já o principal para continuar com nosso artigo.

Registro de Logs

Considerações:
Para usar alguma das opções abaixo, tem que instalar, são pacotes separados, então requer uma instalação.

Alguns dos principais registros de Logs são:

Microsoft.Extensions.Logging.Console
Um agente de log de console simples.

Microsoft.Extensions.Logging.AzureAppServices:
Serviços de aplicativo do Azure oferece suporte a 'Logs de diagnóstico' e recursos de fluxo de Log.

Microsoft.Extensions.Logging.Debug
Logs de um monitor de depuração usando System.Diagnostics.Debug.WriteLine().

Microsoft.Extensions.Logging.EventLog
Registros de log de eventos do Windows.

Microsoft.Extensions.Logging.EventSource
Dá suporte a EventSource/EventListener.

Microsoft.Extensions.Logging.TraceSource
Logs para um ouvinte de rastreamento usando System.Diagnostics.TraceSource.TraceEvent().

Você pode ver mais informações sobre as opções apresentadas aqui:
https://docs.microsoft.com/pt-br/ef/core/miscellaneous/logging
que por sinal é uma excelente documentação.

Mão na massa

Vamos agora ver como utilizar alguns deles.
Primeiramente o Console
O que o AddConsole faz é jogar todas instruções SQL no console do aplicativo, é bem simples, após a instalação do pacote basta apenas referenciar.

O pacote Microsoft.Extensions.Logging.Console disponibiliza um método de extensão AddConsole para o LoggerFactory
Veja um exemplo simples de fazer isso!

var logConsole = new LoggerFactory().AddConsole();
optionsBuilder.UseLoggerFactory(logConsole);

Completo:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    var strConexao = "..."; 
    optionsBuilder.UseSqlServer(strConexao); 
    optionsBuilder.UseLoggerFactory(new LoggerFactory().AddConsole());
}


Output SQL de meu aplicativo console:

info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 2.1.1-rtm-30846 initialized 'SampleContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (146ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE') SELECT 1 ELSE SELECT 0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (62ms) [Parameters=[@p0='?' (DbType = DateTime2), @p1='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Date], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Blogs]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (15ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [p].[Id], [p].[Date], [p].[Name]
      FROM [Blogs] AS [p]
      WHERE [p].[Id] > 0

Muito simples não é?!
Certo, mas aqui temos apenas as querys sendo projetadas no console, eu gostaria de ter algo mais customizado isso é possível?
Resposta: SIM

E iremos ver um exemplo básico de como podemos construir um log manipulável, é um exemplo básico, mas você terá uma ideia de como construir algo mais complexo para sua aplicação!

Log Customizado

Agora a coisa começa a ficar melhor… :), vamos criar uma classe com a seguinte estrutura:
Classe responsável por fazer a manipulação do log.

private class CustomLoggerProvider : ILoggerProvider
{
    public ILogger CreateLogger(string categoryName) => new SampleLogger(); 

    private class SampleLogger : ILogger
    {
        public void Log<TState>(
            LogLevel logLevel, 
            EventId eventId, 
            TState state, 
            Exception exception,
            Func<TState, Exception, string> formatter)
        {
            if (eventId.Id == RelationalEventId.CommandExecuting.Id)
            {
                var log = formatter(state, exception);
                Logs.Add(log); 
            }
        }

        public bool IsEnabled(LogLevel logLevel) => true;

        public IDisposable BeginScope<TState>(TState state) => null;
    }

    public void Dispose() { }
}

Observações:
existe uma variável Logs em minha classe acima, e minha classe também está como privada, fiz isso para não expor ela, apenas quero utilizar de forma que apenas meu DbContext tenha acesso a ela, veja nosso exemplo completo como ficou.

Nosso contexto completo ficou assim:

public class SampleContext : DbContext
{
    public SampleContext()
    {
        if (Logs == null)
        {
            this.GetService<ILoggerFactory>().AddProvider(new CustomLoggerProvider());
            Logs = new List<string>();
        }
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var sqlConnectionStringBuilder = "Server=(localdb)\\mssqllocaldb;Database=ExemploExtensao;Integrated Security=True;";
        optionsBuilder.UseSqlServer(sqlConnectionStringBuilder); 
        base.OnConfiguring(optionsBuilder);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>();
    }

    public static IList<string> Logs = null;

    private class CustomLoggerProvider : ILoggerProvider
    {
        public ILogger CreateLogger(string categoryName) => new SampleLogger();

        private class SampleLogger : ILogger
        {
            public void Log<TState>(
                LogLevel logLevel,
                EventId eventId,
                TState state,
                Exception exception,
                Func<TState, Exception, string> formatter)
            {
                if (eventId.Id == RelationalEventId.CommandExecuting.Id)
                {
                    var log = formatter(state, exception);
                    Logs.Add(log);
                }
            }

            public bool IsEnabled(LogLevel logLevel) => true;

            public IDisposable BeginScope<TState>(TState state) => null;
        }

        public void Dispose() { }
    }
}

Essa classe é tudo que precisamos para criar uma instância de ILogger, onde é feito todo rastreamento das query’s, mas claro falando de forma genérica, já que podemos fazer “N” coisas!
Feito isso vamos agora injetar/adicionar ele como um provider customizado, a forma mais simples é recuperar o serviço que já foi injetado por (DI - injeção de dependência) através da interface ILoggerFactory, da seguinte maneira.

this.GetService<ILoggerFactory>().AddProvider(new CustomLoggerProvider());

Como foi mostrado no exemplo completo acima!
Nosso exemplo de uso

static void Main(string[] args)
{
    using(var db = new SampleContext())
    {
        db.Database.EnsureCreated();
        db.Set<Blog>().Add(new Blog
        {
            Name = "Rafael Almeida",
            Date = DateTime.Now
        });
        db.SaveChanges();

        db.Set<Blog>().Where(p=>p.Id > 0).ToList();
    }

	// Recuperar o log dos comandos executados
    foreach (var log in SampleContext.Logs)
    {
        Console.WriteLine(log);
    }
    Console.ReadKey();
}

Vamos criar nossa Extensão

Sabemos que podemos monitorar os comandos SQL como mostrado acima, mas em alguns casos podemos querer ver uma query especifica de um comando LINQ especifico.

O EF Core nos fornece uma possibilidade de obter todo DDL de nosso banco de dados com o método de extensão GenerateCreateScript disponibilizado pelo próprio EF Core, veja o exemplo abaixo:

var scriptBanco = db.Database.GenerateCreateScript();

Agora iremos construir nosso próprio conversor LINQ to SQL com base em uma consulta LINQ tipo Queryable.

Como sempre falo, System.Reflection I LOVE, SEMPRE, SEMPRE!!!

Com algumas magias usando Reflection podemos fazer a recuperação de algumas informações serializadas que estão em memória.

Algumas informações para você

EntityQueryProvider (IQueryCompiler)
Essa API oferece suporte à infraestrutura do Entity Framework Core e não se destina a ser usada diretamente em seu código.

DatabaseDependencies
Classe de parâmetro de dependências de serviço para o banco de dados. Esse tipo é normalmente usado por provedores de banco de dados (e outras extensões). Geralmente não é usado no código do aplicativo.
Não construa instâncias dessa classe diretamente do provedor ou do código do aplicativo, pois a assinatura do construtor pode mudar à medida que novas dependências são adicionadas. Em vez disso, use esse tipo em seu construtor para que uma instância seja criada e injetada automaticamente pelo contêiner de injeção de dependência.

Veja nossa classe completa

public static class RalmsExtensionSql
{
    private static readonly TypeInfo _queryCompilerTypeInfo = typeof(QueryCompiler).GetTypeInfo();

    private static readonly FieldInfo _queryCompiler
        = typeof(EntityQueryProvider)
            .GetTypeInfo()
            .DeclaredFields
            .Single(x => x.Name == "_queryCompiler"); 

    private static readonly FieldInfo _queryModelGenerator
        = _queryCompilerTypeInfo
            .DeclaredFields
            .Single(x => x.Name == "_queryModelGenerator");

    private static readonly FieldInfo _database = _queryCompilerTypeInfo
        .DeclaredFields
        .Single(x => x.Name == "_database");

    private static readonly PropertyInfo _dependencies
        = typeof(Database)
            .GetTypeInfo()
            .DeclaredProperties
            .Single(x => x.Name == "Dependencies");

    public static string ToSql<T>(this IQueryable<T> queryable)
        where T : class
    {
        var queryCompiler = _queryCompiler.GetValue(queryable.Provider) as IQueryCompiler;
        var queryModelGen = _queryModelGenerator.GetValue(queryCompiler) as IQueryModelGenerator;
        var queryCompilationContextFactory
            = ((DatabaseDependencies)_dependencies.GetValue(_database.GetValue(queryCompiler)))
                .QueryCompilationContextFactory;

        var queryCompilationContext = queryCompilationContextFactory.Create(false);
        var modelVisitor = (RelationalQueryModelVisitor)queryCompilationContext.CreateQueryModelVisitor();

        modelVisitor.CreateQueryExecutor<T>(queryModelGen.ParseQuery(queryable.Expression));

        return modelVisitor
            .Queries
            .FirstOrDefault()
            .ToString();
    }
}


Exemplo de uso

static void Main(string[] args)
{
    using (var db = new SampleContext())
    { 
        db.Database.EnsureCreated();
        db.Set<Blog>().Add(new Blog
        {
            Name = "Rafael Almeida",
            Date = DateTime.Now
        });
        db.SaveChanges(); 

        // Gerar/Projetar o SQL 
        var strSQL = db.Set<Blog>().Where(p => p.Id > 0).ToSql();
    } 

    Console.ReadKey();
}


Veja o exemplo 01

Referências

EF Core
QueryCompiler
DatabaseDependencies
RelationalQueryModelVisitor


Click aqui para acessar os fontes no github!

Pessoal, fico por aqui #efcore #mvp #mvpbr #mvpbuzz

Deixe um comentário