Simple EF6-style Logging for EF Core

It took me a while to figure out logging in EF Core, and I missed the simple way you could add logging for EF6. Remember in EF6 it's as easy as

 using (var db = new BloggingContext())
{
  db.Database.Log = s => Console.WriteLine(s);
  //. . .
}

This is exacerbated that EF Core doesn't support returning the SQL for a query with ToString() like EF6 did. So you have to get the SQL Text through Logging (or IntelliTrace, or Profiler).

In EF Core it's more code, and more complicated, as documented here: /en-us/ef/core/miscellaneous/logging

 using (var db = new BloggingContext())
{
  var serviceProvider = db.GetInfrastructure();
  var loggerFactory = serviceProvider.GetService();
  loggerFactory.AddProvider(new MyLoggerProvider());
  //. . .
}

And what's worse, the log message handling is buried inside the guts of MyLoggerProvider. So I took that sample and reworked so you can add a logger for your EF Core 2 DbContext like this:

 using (var db = new BloggingContext())
{
  db.ConfigureLogging( s => Console.WriteLine(s) );
  //. . .
}

You can also filter the logger by LogLevel and/or category with an optional second parameter. Here the logger will capture all Errors and all Queries like this:

 using (var db = new BloggingContext())
{
  db.ConfigureLogging( s => Console.WriteLine(s) , (c,l) => l == LogLevel.Error || c == DbLoggerCategory.Query.Name);
  //. . .
}

And there's a shortcut to log the Query, Command, and Update categories to capture the all the generated SQL and execution statistics.

 using (var db = new BloggingContext())
{
  db.ConfigureLogging(s => Console.WriteLine(s), LoggingCategories.SQL);
  //. . .
}

Once you configure logging on a DbContext instance it will be enabled on all instances of that DbContext type. Repeated calls to ConfigureLogging() will change the logging for the DbContext type. Behind the scenes there is a single LogProvider for each DbContext type.

Note in a multi-threaded environment changing the logging will affect all new instances of your DbContext type.

It's an extension method so you need to import the namespace with:

 using Microsoft.Samples.EFLogging;

And here's the code:

 namespace Microsoft.Samples.EFLogging
{
    using System;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.Infrastructure;
    using Microsoft.Extensions.Logging;
    using System.Collections.Concurrent;
    using System.Collections.Generic;


    public enum LoggingCategories
    {
        All = 0,
        SQL = 1
    }

    public static class DbContextLoggingExtensions
    {
        public static void ConfigureLogging(this DbContext db, Action<String> logger, Func<string, LogLevel, bool> filter )
        {
            var serviceProvider = db.GetInfrastructure<IServiceProvider>();
            var loggerFactory = (ILoggerFactory)serviceProvider.GetService(typeof(ILoggerFactory));

            LogProvider.CreateOrModifyLoggerForDbContext(db.GetType(), loggerFactory, logger, filter);
        }
        
        public static void ConfigureLogging(this DbContext db, Action<String> logger, 
                                           LoggingCategories categories = LoggingCategories.All)
        {
            var serviceProvider = db.GetInfrastructure<IServiceProvider>();
            var loggerFactory = (LoggerFactory)serviceProvider.GetService(typeof(ILoggerFactory));

            if (categories == LoggingCategories.SQL)
            {
                var SqlCategories = new List<string> { DbLoggerCategory.Database.Command.Name,
                                                       DbLoggerCategory.Query.Name,
                                                       DbLoggerCategory.Update.Name };
                LogProvider.CreateOrModifyLoggerForDbContext(db.GetType(), 
                                                             loggerFactory, 
                                                             logger, 
                                                             (c, l) => SqlCategories.Contains(c));
            }
            else if (categories == LoggingCategories.All)
            {
                LogProvider.CreateOrModifyLoggerForDbContext(db.GetType(), 
                                                             loggerFactory, logger, 
                                                             (c, l) => true);
            }
        }
    }
    class LogProvider : ILoggerProvider
    {

        //volatile to allow the configuration to be switched without locking
        public volatile LoggingConfiguration Configuration;
        static bool DefaultFilter(string CategoryName, LogLevel level) => true;

        static ConcurrentDictionary<Type, LogProvider> providers = new ConcurrentDictionary<Type, LogProvider>();

        public static void CreateOrModifyLoggerForDbContext(Type DbContextType, 
                                                            ILoggerFactory loggerFactory, 
                                                            Action<string> logger, 
                                                            Func<string, LogLevel, bool> filter = null)
        {
            bool isNew = false;
            var provider = providers.GetOrAdd(DbContextType, t =>
              {
                  var p = new LogProvider(logger, filter ?? DefaultFilter);
                  loggerFactory.AddProvider(p);
                  isNew = true;
                  return p;
               }
              );
            if (!isNew)
            {
                provider.Configuration = new LoggingConfiguration(logger, filter ?? DefaultFilter);
            }
            
        }

        public class LoggingConfiguration
        {
            public LoggingConfiguration(Action<string> logger, Func<string, LogLevel, bool> filter)
            {
                this.logger = logger;
                this.filter = filter;
            }
            public readonly Action<string> logger;
            public readonly Func<string, LogLevel, bool> filter;
        }


        private LogProvider(Action<string> logger, Func<string, LogLevel, bool> filter)
        {
            this.Configuration = new LoggingConfiguration(logger, filter);
        }
   
        public ILogger CreateLogger(string categoryName)
        {
            return new Logger(categoryName, this);
        }

        public void Dispose()
        { }

        private class Logger : ILogger
        {
            
            readonly string categoryName;
            readonly LogProvider provider;
            public Logger(string categoryName, LogProvider provider)
            {
                this.provider = provider;
                this.categoryName = categoryName;
            }
            public bool IsEnabled(LogLevel logLevel)
            {
                return true; 
            }

            public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, 
                                    Exception exception, Func<TState, Exception, string> formatter)
            {
                //grab a reference to the current logger settings for consistency, 
                //and to eliminate the need to block a thread reconfiguring the logger
                var config = provider.Configuration;
                if (config.filter(categoryName, logLevel))
                {
                    config.logger(formatter(state, exception));
                }
            }

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