Logging and Diagnostics in Entity Framework

Logging is crucial for every application. It helps with debugging, troubleshooting, performance optimization, monitoring, alerting, security auditing, etc.

Entity Framework provides several mechanisms for generating logs and obtaining diagnostics.

Let’s dive deep into how we can use those mechanisms.

Note: All examples use EF 8.  

ToQueryString

The simplest way for us to see the query EF generates is through the ToQueryString method.

var query =  context.People
    .Where(p => p.Id == 1)
    .Select(p => new { p.FirstName, p.LastName })
    .ToQueryString();

Console.WriteLine(query);

// DECLARE @__p_0 bigint = CAST(1 AS bigint);

// SELECT[p].[FirstName], [p].[LastName]
// FROM[People] AS[p]
// WHERE[p].[Id] = @__p_0

The method returns a string representation of the fetching queries. Easy, but limited.

It doesn’t work for direct executions like creating, modifying, or deletion. It would be best if you used it only in debugging.

Simple Logging

If you want to get logs for all EF operations, including transaction management and migrations, you can configure simple logging when configuring a DbContext.

    protected override void OnConfiguring(
        DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder
                .LogTo(Console.WriteLine)
                .EnableSensitiveDataLogging()
                .EnableDetailedErrors();

The LogTo method accepts a delegate that accepts a string. EF calls this delegate for each log message generated. It’s often used to write each log to the console using Console.WriteLine.

But you can also write the logs to the debug window using Debug.WriteLine or directly to the file using StreamWriter.

EF doesn’t include any data values in the exception messages by default. You can enable it with the EnableSensitiveDataLogging method.

EnableDetailedErrors adds try-catch blocks for each call to the database provider. This is helpful, as some exceptions can be hard to diagnose without it.

Simple logging is only for debugging during application development. You don’t want to expose sensitive production data and degrade performance because of try-catch blocks.

You can filter the logs you want to see if you’re looking for a specific issue.

    protected override void OnConfiguring(
        DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder
                .LogTo(Console.WriteLine, LogLevel.Warning)
                .LogTo(Console.WriteLine, new[] { CoreEventId.SaveChangesCompleted });

Advanced Logging

For advanced logging, you can use the UserLoggerFactory method to use Microsoft.Extensions.Logging for logging. You can use Microsoft Logger or any other supported logger, like Serilog

var logger = new LoggerConfiguration()
    .WriteTo.Console()
    .CreateLogger();

builder.Logging.ClearProviders();
builder.Logging.AddProvider(
    new SerilogLoggerProvider(logger));
internal class MyContext : DbContext
{
    public DbSet<Person> People { get; set; }

    private readonly ILoggerFactory _loggerFactory;

    public MyContext(DbContextOptions<MyContext> options,
        ILoggerFactory loggerFactory) : base(options)
    {
        _loggerFactory = loggerFactory;
    }

    protected override void OnConfiguring(
        DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder
                .UseLoggerFactory(_loggerFactory);
}

This approach is suitable for all environments, including production. Remember about logging sensitive data. Turn it off or configure it so as not to expose sensitive data.

Interceptors

Interceptors allow you to intercept and potentially modify database operations before or after they are sent to the database. They can also be used for logging and diagnostics.

There are several interceptor interfaces to implement.

IDbCommandInterceptor intercepts creating and executing commands, command failures, and disposing of the commands’s DbDataReader.

IDbConnectionInterceptor intercepts opening and closing connections and connection failures.

IDbTransactionInterceptor intercepts everything regarding transactions and savepoints.

To register the interceptor, you need to use the AddInterceptors method while using the DbContext configuration. 

class CommandQueryInterceptor : DbCommandInterceptor
{

    public override InterceptionResult<DbDataReader> ReaderExecuting(
               DbCommand command,
                      CommandEventData eventData,
                             InterceptionResult<DbDataReader> result)
    {
        Console.WriteLine(command.CommandText);

        return base.ReaderExecuting(command, eventData, result);
    }
}
    protected override void OnConfiguring(
        DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder
                .AddInterceptors(new CommandQueryInterceptor());

You can register many interceptors. Their limitation is that they are registered per DbContext.  

Diagnostic Listeners

The diagnostic listeners can provide the same information as interceptors. However, they do so for many DbContexts instances in the current .NET process.

The diagnostic listeners are a standard mechanism across .NET. But they are not designed for logging.

As the name suggests, they are suitable for diagnostics. Some configuration is required to resolve EF events. First, you need to create an observer and register it globally. 

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

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

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

In the OnNext method, we look for DiagnosticListener from EF. 

Now, we have to create a key-value observer for specific EF events. 

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

In the OnNext method, we look for an EF event responsible for context initialization.

Summary

Logging and diagnostics are vital in Entity Framework for debugging, optimizing performance, ensuring security compliance, real-time monitoring, and educating developers.

EF offers several approaches to diagnosing or logging issues. Each has pros and cons. Understanding and learning how each can help you solve your needs is essential. 

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top