CHAPTER 5
Entity Framework Core makes use of the logging infrastructure of .NET Core. This can come in handy because it offers a glimpse (or even more) into what Entity Framework is doing, like the SQL it is generating, how long the operations take, etc.
The logging framework of .NET Core—used by Entity Framework Core—consists of the following:
A logger is returned for a concrete logger category. Logger categories come from infrastructure class names, the ones that provide the logging information. Because Entity Framework Core is built using a modular, layered approach, some of its operations will come from the datastore-agnostic core (DbContext, InternalQueryCompiler). Others will come from the relational layers (RelationalCommandBuilderFactory, RelationalModelValidator, SqlServerQueryCompilationContextFactory, SqlServerCompositeMethodCallTranslator), and finally, others will come from database-specific classes (SqlServerConnection).
Each logging entry actually consists of:
The event ID is specific to the infrastructure. Some common values are as follows:
Table 6
ID | Meaning | Sender | State |
|---|---|---|---|
1 | Execute SQL | RelationalCommandBuilderFactory | DbCommandLogData |
2 | Create database | SqlServerConnection | Database and server names as strings |
3 | Open connection | SqlServerConnection | Database and server names as strings |
4 | Close connection | SqlServerConnection | Database and server names as strings |
5 | Begin transaction | SqlServerConnection | IsolationLevel |
6 | Commit transaction | SqlServerConnection | IsolationLevel |
7 | Rollback transaction | SqlServerConnection | IsolationLevel |
> 7 | Warnings |
In the case of relational data sources, these values are specified in the RelationalEventId enumeration. There is also CoreEventId for EF Core generic events (context initialized, etc.) and SqlServerRelationalId (for SQL Server-specific events, of course). Other databases will feature similar constants.
When we add logging to an Entity Framework context through the OnConfiguring method, we start to get things in the logging target of our choice—in this example, it is the console. For example, we issue a query such as this:
Code Listing 128
var projects = ctx .Projects .Where(x => x.Start == DateTime.Today) .Select(x => new { x.ProjectId, x.Start }) .ToList(); |
We're likely to get output like the following in the console:
info:
Microsoft.EntityFrameworkCore.Storage.Internal.RelationalCommandBuilderFactory[1]
Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [x].[ProjectId], [x].[Start]
FROM [Project] AS [x]
WHERE [x].[Start] = @__Today_0
Notice that the SQL does not include the actual filtering value (DateTime.Today); instead, it references the parameter name that was used. Besides the actual SQL, we can see that the execution took two milliseconds.
How do we interpret this? Well, first, we can see the log level, info in this case. Then we have the provider class that logged this (the category name), RelationalCommandBuilderFactory, and then the event inside [], which in this example is 1. Finally, we have the actual message, which is specific to the log event parameters (Executed DbCommand).
Microsoft makes some logging providers for .NET Core available:
Table 7
Provider | Purpose |
|---|---|
Microsoft.Extensions.Logging.Console | Logs all messages with log level equal to or greater than information to the console of the current application. |
Microsoft.Extensions.Logging.Debug | Logs to the debug window of the current attached debugger (like Visual Studio while debugging) with log level equal to or greater than information. |
Microsoft.Extensions.Logging.EventLog | Writes to the Windows Event Log. |
Microsoft.Extensions.Logging.EventSource | Logs to registered EventListeners. |
Microsoft.Extensions.Logging.TraceSource | Writes to all registered trace listeners. |
You can also write your own logging provider. You need to create a logging provider factory, register it with an Entity Framework Core context, and it will be used automatically. Here is an example for writing a (simple) log provider that calls a supplied delegate:
Code Listing 129
public static class LoggerFactoryExtensions { public static ILoggerFactory AddAction(this ILoggerFactory loggerFactory, Action<LogLevel, EventId, Exception, string> action, Func<string, LogLevel, bool> filter) { if (action == null) { throw new ArgumentNullException(nameof(action)); } loggerFactory.AddProvider(new ActionLoggerProvider(action, filter)); return loggerFactory; } public static ILoggerFactory AddAction(this ILoggerFactory loggerFactory, Action<LogLevel, EventId, Exception, string> action) { return AddAction(loggerFactory, action, null); } } public sealed class ActionLoggerProvider : ILoggerProvider { private readonly Action<LogLevel, EventId, Exception, string> _action; private readonly Func<string, LogLevel, bool> _filter; public ActionLoggerProvider(Action<LogLevel, EventId, Exception, string> action, Func<string, LogLevel, bool> filter) { this._action = action; this._filter = filter ?? delegate (string cn, LogLevel ll) { return true; }; } public ILogger CreateLogger(string categoryName) { return new ActionLogger(categoryName, this._action, this._filter); } public void Dispose() { } } public sealed class ActionLogger : ILogger { private readonly string _categoryName; private Action<LogLevel, EventId, Exception, string> _action; private Func<string, LogLevel, bool> _filter; public ActionLogger(string categoryName, Action<LogLevel, EventId, Exception, string> action, Func<string, LogLevel, bool> filter) { this._categoryName = categoryName; this._action = action; this._filter = filter; } public IDisposable BeginScope<TState>(TState state) { return NullDisposable.Instance; } public bool IsEnabled(LogLevel logLevel)s { return this._filter(this._categoryName, logLevel); } public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { this._action(logLevel, eventId, exception, formatter(state, exception)); } } internal sealed class NullDisposable : IDisposable { public static readonly IDisposable Instance = new NullDisposable(); private NullDisposable() { } public void Dispose() { } } |
So, to add this provider, you need to do this:
Code Listing 130
loggerFactory.AddAction((logLevel, eventId, exception, message) => { //do something }); |
You can either pass a filter to AddAction or not. If you don’t, anything will be logged.
Entity Framework Core will use the logger factory provided in the “default” dependency injection configuration, if you are using ASP.NET Core, but you can also provide your own, with your specific settings. For example, in .NET Core 2.x, you can filter out which events to log, based on the log category (the class name bound to the logger), the log level or the provider itself. For example, if you wish to log to the console all the SQL being produced by EF Core, you can do something like this:
Code Listing 131
var loggerFactory = new LoggerFactory() .AddConsole((categoryName, logLevel) => (logLevel == LogLevel.Information) && (categoryName == DbLoggerCategory.Database.Command.Name)); var optionsBuilder = new DbContextOptionsBuilder<ProjectsContext>() .UseLoggerFactory(loggerFactory) .UseSqlServer("<connection string>"); var ctx = new ProjectsContext(optionsBuilder.Options); |
So, the UseLoggerFactory method allows you to supply your own logger provider to a DbContext. You can both set it the way I’ve shown (using a DbContextOptionsBuilder<T>) or through the OnConfiguring method. The DbLoggerCategory class and its subclasses contain names for logger category names, and they are:
Table 8
Class | Purpose |
|---|---|
DbLoggerCategory.Database | Events related to database interactions |
DbLoggerCategory.Database.Command | SQL sent to the database |
DbLoggerCategory.Database.Connection | Connection-related events |
DbLoggerCategory.Database.Transaction | Transaction operations |
DbLoggerCategory.Infrastructure | Miscelaneous messages |
DbLoggerCategory.Migrations | Migration events |
DbLoggerCategory.Model | Model building and metadata events |
DbLoggerCategory.Model.Validation | Model validation events |
DbLoggerCategory.Query | Messages related to queries, excluding the SQL generated |
DbLoggerCategory.Scaffolding | Scaffolding/reverse engineer events |
DbLoggerCategory.Update | Database updates |
The Name property contains the category name which is also output by the logger.
Finally, you can change the behavior of certain events–ignore, log, throw an exception–by making a call to ConfigureWarnings inside the OnConfiguring method:
Code Listing 132
optionsBuilder.ConfigureWarnings( warnings => { warnings.Ignore(RelationalEventId.OpeningConnection, RelationalEventId.ClosingConnection); warnings.Throw(RelationalEventId.RollingbackTransaction); }); |
Setting a default is as simple as this:
Code Listing 133
optionsBuilder.ConfigureWarnings( warnings => { warnings.Default(WarningBehavior.Ignore); warnings.Log(RelationalEventId.CommitTransaction); }); |
In this case, the default log behavior is to ignore everything and only log transaction commits.
EF Core also leverages the diagnostics framework. In order to understand it, you must know that all .NET Core components produce diagnostic information (events), which can be consumed by listeners registered to the default DiagnosticListener (instantiated automatically and registered to the dependency injection framework). For example, EF Core produces events for the "Microsoft.EntityFrameworkCore.Database.Command.CommandExecuted" and "Microsoft.EntityFrameworkCore.Database.Command.CommandExecuting" occurrences. So, we can hook a listener to the DiagnosticListener, a reference to which we can obtain from the DI framework:
Code Listing 134
DiagnosticListener listener = …; listener.SubscribeWithAdapter(new CommandListener()); |
And a sample listener could look like this:
Code Listing 135
public class CommandListener { [DiagnosticName( "Microsoft.EntityFrameworkCore.Database.Command.CommandExecuting")] public void OnCommandExecuting(DbCommand command, DbCommandMethod executeMethod, Guid commandId, Guid connectionId, bool async, DateTimeOffset startTime) { }
[DiagnosticName( "Microsoft.EntityFrameworkCore.Database.Command.CommandExecuted")] public void OnCommandExecuted(object result, bool async) { } } |
These two methods can be named whatever you want, but they must be decorated with [DiagnosticName] attributes with these names, and take the same-name parameters. When EF Core executes commands (SQL), they will be called, and the developer is even given a chance to modify the generated SQL or the parameters to the DbCommand.
There are many events that EF Core produces, and they match the names of RelationalEventId and CoreEventId (and SqlServerEventId, for SQL Server):
Table 9
Name |
|---|
RelationalEventId.AmbientTransactionWarning |
RelationalEventId.BoolWithDefaultWarning |
RelationalEventId.CommandError |
RelationalEventId.CommandExecuted |
RelationalEventId.CommandExecuting |
RelationalEventId.ConnectionClosed |
RelationalEventId.ConnectionClosing |
RelationalEventId.ConnectionError |
RelationalEventId.ConnectionOpened |
RelationalEventId.ConnectionOpening |
RelationalEventId.DataReaderDisposing |
RelationalEventId.MigrateUsingConnection |
RelationalEventId.MigrationApplying |
RelationalEventId.MigrationGeneratingDownScript |
RelationalEventId.MigrationGeneratingUpScript |
RelationalEventId.MigrationReverting |
RelationalEventId.MigrationsNotApplied |
RelationalEventId.MigrationsNotFound |
RelationalEventId.ModelValidationKeyDefaultValueWarning |
RelationalEventId.QueryClientEvaluationWarning |
RelationalEventId.QueryPossibleExceptionWithAggregateOperator |
RelationalEventId.QueryPossibleUnintendedUseOfEqualsWarning |
RelationalEventId.TransactionCommitted |
RelationalEventId.TransactionDisposed |
RelationalEventId.TransactionError |
RelationalEventId.TransactionRolledBack |
RelationalEventId.TransactionStarted |
RelationalEventId.TransactionUsed |
These names should be meaningful enough for you to understand, but a problem with them is that they all take different parameters (the ones that need to go in the listener method marked with [DiagnosticName]). You will need to consult the documentation on each of these properties to find out which they are.