Logs e Consultas LINQ to SQL
Introdução
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
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().
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ê
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
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