Back-End

5 jul, 2018

Logs e consultas LINQ to SQL

Publicidade

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 tenhamos esse controle. É uma forma de saber se as consultas estão sendo geradas corretamente, mas em outra oportunidade falaremos mais sobre isso.

Como o objetivo maior de nosso artigo é mostrar como visualizar/capturar os comandos enviados para o banco, nada mais justo que utilizarmos o Entity Framework Core para isso.

O EF Core fornece um conjunto de opções para verificarmos 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 queries, por exemplo, consumiram mais tempo.

Apresentaremos aqui duas 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, já temos 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 requerem 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. Por sinal, é uma excelente documentação.

Mãos 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 como 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 queries sendo projetadas no console.

Eu gostaria de ter algo mais customizado, isso é possível?

Sim! E veremos 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 sobre 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 queries, mas, claro, falando de forma genérica, já que podemos fazer N coisas!

Feito isso, agora vamos 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 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 vamos 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 pra 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 (IQueryCompiler): 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 em 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:

Referências

Clique aqui para acessar os fontes no GitHub.

Pessoal, fico por aqui.