Using EF Core (or any ORM for that matter) I want to keep track of the number of queries the ORM makes to the database during some operation in my software.
I've used SQLAlchemy under Python earlier, and on that stack this is faily easy to set up. I typically have unit tests that assert on the number of queries made for a scenario, against an in-memory SQLite database.
Now I want to do the same thing using EF Core, and have looked at the Logging documentation.
In my test setup code I do as the documentation says:
using (var db = new BloggingContext()) { var serviceProvider = db.GetInfrastructure<IServiceProvider>(); var loggerFactory = serviceProvider.GetService<ILoggerFactory>(); loggerFactory.AddProvider(new MyLoggerProvider()); }
But I run into problems that I suspect are the results of the following (also from the docs):
You only need to register the logger with a single context instance. Once you have registered it, it will be used for all other instances of the context in the same AppDomain.
The problems I see in my tests indicates that my logger implementation is shared across multiple contexts (this is in accordance with the docs as I read them). And since a) my test runner runs tests in parallell and b) my entire test suite creates hundreds of db contexts - it does not work very well.
Question/issues:
Multiple DbContext was first introduced in Entity Framework 6.0. Multiple context classes may belong to a single database or two different databases.
EF and EF Core DbContext types implement IDisposable . As such, best practice programming suggests that you should wrap them in a using() block (or new C# 8 using statement). Unfortunately, doing this, at least in web apps, is generally a bad idea.
Using Caching in Entity Framework Core. NCache provides integration for caching in Entity Framework Core through Extension Methods. You can cache the result sets of LINQ queries whether they are for transactional data or reference data.
Call DbContextOptionsBuilder.UseLoggerFactory(loggerFactory)
method to log all SQL output of a particular context instance. You could inject a logger factory in the context's constructor.
Here is a usage example:
//this context writes SQL to any logs and to ReSharper test output window using (var context = new TestContext(_loggerFactory)) { var customers = context.Customer.ToList(); } //this context doesn't using (var context = new TestContext()) { var products = context.Product.ToList(); }
Generally, I use this feature for manual testing. To keep the original context class clean, a derived testable context is declared with overridden OnConfiguring
method:
public class TestContext : FooContext { private readonly ILoggerFactory _loggerFactory; public TestContext() { } public TestContext(ILoggerFactory loggerFactory) { _loggerFactory = loggerFactory; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); optionsBuilder.UseLoggerFactory(_loggerFactory); } }
It's enough to log SQL queries. Don't forget to attach a suitable logger (like Console) to loggerFactory
before you pass it to context.
We can create a loggerFactory
in a test class constructor:
public class TestContext_SmokeTests : BaseTest { public TestContext_SmokeTests(ITestOutputHelper output) : base(output) { var serviceProvider = new ServiceCollection().AddLogging().BuildServiceProvider(); _loggerFactory = serviceProvider.GetService<ILoggerFactory>(); _loggerFactory.AddProvider(new XUnitLoggerProvider(this)); } private readonly ILoggerFactory _loggerFactory; }
The test class is derived from BaseTest
which supports the writing to xUnit
output:
public interface IWriter { void WriteLine(string str); } public class BaseTest : IWriter { public ITestOutputHelper Output { get; } public BaseTest(ITestOutputHelper output) { Output = output; } public void WriteLine(string str) { Output.WriteLine(str ?? Environment.NewLine); } }
The most tricky part is to implement a logging provider accepting IWriter
as a parameter:
public class XUnitLoggerProvider : ILoggerProvider { public IWriter Writer { get; private set; } public XUnitLoggerProvider(IWriter writer) { Writer = writer; } public void Dispose() { } public ILogger CreateLogger(string categoryName) { return new XUnitLogger(Writer); } public class XUnitLogger : ILogger { public IWriter Writer { get; } public XUnitLogger(IWriter writer) { Writer = writer; Name = nameof(XUnitLogger); } public string Name { get; set; } public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { if (!this.IsEnabled(logLevel)) return; if (formatter == null) throw new ArgumentNullException(nameof(formatter)); string message = formatter(state, exception); if (string.IsNullOrEmpty(message) && exception == null) return; string line = $"{logLevel}: {this.Name}: {message}"; Writer.WriteLine(line); if (exception != null) Writer.WriteLine(exception.ToString()); } public bool IsEnabled(LogLevel logLevel) { return true; } public IDisposable BeginScope<TState>(TState state) { return new XUnitScope(); } } public class XUnitScope : IDisposable { public void Dispose() { } } }
We've done here! All the SQL logs will be shown in Rider/Resharper test output window.
For EF Core 5.0 Simple Logging (what a name!) was introduced
EF Core logs can be accessed from any type of application through the use of LogTo when configuring a DbContext instance. This configuration is commonly done in an override of DbContext.OnConfiguring. For example:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.LogTo(Console.WriteLine);
Alternately, LogTo can be called as part of AddDbContext or when creating a DbContextOptions instance to pass to the DbContext constructor.
Writing to a file. But I'd rather inject some kind of logger into db context and use it instead of writing logging logic inside of context.
private readonly StreamWriter _logStream = new StreamWriter("mylog.txt", append: true); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.LogTo(_logStream.WriteLine); public override void Dispose() { base.Dispose(); _logStream.Dispose(); } public override async ValueTask DisposeAsync() { await base.DisposeAsync(); await _logStream.DisposeAsync(); }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With