entity framework核心:记录单个数据库上下文实例的查询

使用EF Core(或任何ORM)我想跟踪ORM在我的软件中进行某些操作期间对数据库的查询次数。

我之前在Python下使用过SQLAlchemy,在这个堆栈上,这很容易设置。 我通常有一个unit testing,它针对一个场景的查询数量,针对内存中的SQLite数据库进行断言。

现在我想使用EF Core做同样的事情,并查看了Logging文档 。

在我的测试设置代码中,我按照文档说的那样做:

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

但我遇到的问题我怀疑是以下结果(也来自文档):

您只需要使用单个上下文实例注册记录器。 注册后,它将用于同一AppDomain中上下文的所有其他实例。

我在测试中看到的问题表明我的记录器实现是在多个上下文中共享的(这与我阅读它们时的文档一致)。 并且因为a)我的测试运行器在并行运行测试而b)我的整个测试套件创建了数百个db上下文 – 它不能很好地工作。

问题/问题:

  • 我想要的是什么?
  • 即我可以使用仅用于该db上下文实例的db上下文注册记录器吗?
  • 还有其他方法可以完成我想要做的事情吗?

调用DbContextOptionsBuilder.UseLoggerFactory(loggerFactory)来记录特定上下文实例的所有SQL输出。 您可以在上下文的构造函数中注入记录器工厂。

用法示例:

 //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(); } 

通常我使用此function进行手动测试。 为了保持原始上下文类的清洁,使用重写的OnConfiguring方法声明派生的可测试上下文:

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

记录SQL查询就足够了(在将logger传递给上下文之前将记录器附加到loggerFactory )。

第二部分:将日志传递给xUnit输出和ReSharper测试输出窗口

我们可以在测试类构造函数中创建一个loggerFactory

 public class TestContext_SmokeTests : BaseTest { public TestContext_SmokeTests(ITestOutputHelper output) : base(output) { var serviceProvider = new ServiceCollection().AddLogging().BuildServiceProvider(); _loggerFactory = serviceProvider.GetService(); _loggerFactory.AddProvider(new XUnitLoggerProvider(this)); } private readonly ILoggerFactory _loggerFactory; } 

Test类派生自BaseTest ,允许写入xUnit输出:

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

最棘手的部分是实现一个接受IWriter作为参数的日志记录提供程序:

 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(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func 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 state) { return new XUnitScope(); } } public class XUnitScope : IDisposable { public void Dispose() { } } } 

所需包裹:

      

阅读: docs.microsoft.com/en-us/ef/core/miscellaneous/logging

应用程序不为每个上下文实例创建新的ILoggerFactory实例非常重要。 这样做会导致内存泄漏和性能下降

如果你想登录静态destionation(例如控制台),Ilja的答案是有效的,但是如果你想首先登录到自定义缓冲区,当每个dbContext将日志消息收集到它自己的缓冲区时(以及你想在多用户服务中做什么) ,然后UPSSS – 内存泄漏(每个几乎空模型的内存泄漏大约20 MB)…

当EF6有一个简单的解决方案来订阅一行中的Log事件时,现在以这种方式注入你的日志:

  var messages = new List(); Action verbose = (text) => { messages.Add(text); }; // add logging message to buffer using (var dbContext = new MyDbContext(BuildOptionsBuilder(connectionString, inMemory), verbose)) { //.. }; 

你应该写出合群的怪物。

PS有人告诉Ef Core架构师他们对DI的错误理解以及他们称之为“容器”的那些花哨的服务定位器以及他们从ASP.Core借来的流畅的UseXXX不能取代构造函数中的“粗俗”DI。 至少logfunction通常应该是可注射的。

* PPS请阅读此https://github.com/aspnet/EntityFrameworkCore/issues/10420 。 这意味着添加LoggerFactory可以访问InMemory数据提供程序。 这是一个抽象泄漏。 EF Core存在架构问题。

ILoggerFactory汇集代码:

 public class StatefullLoggerFactoryPool { public static readonly StatefullLoggerFactoryPool Instance = new StatefullLoggerFactoryPool(()=> new StatefullLoggerFactory()); private readonly Func construct; private readonly ConcurrentBag bag = new ConcurrentBag(); private StatefullLoggerFactoryPool(Func construct) => this.construct = construct; public StatefullLoggerFactory Get(Action verbose, LoggerProviderConfiguration loggerProviderConfiguration) { if (!bag.TryTake(out StatefullLoggerFactory statefullLoggerFactory)) statefullLoggerFactory = construct(); statefullLoggerFactory.LoggerProvider.Set(verbose, loggerProviderConfiguration); return statefullLoggerFactory; } public void Return(StatefullLoggerFactory statefullLoggerFactory) { statefullLoggerFactory.LoggerProvider.Set(null, null); bag.Add(statefullLoggerFactory); } } public class StatefullLoggerFactory : LoggerFactory { public readonly StatefullLoggerProvider LoggerProvider; internal StatefullLoggerFactory() : this(new StatefullLoggerProvider()){} private StatefullLoggerFactory(StatefullLoggerProvider loggerProvider) : base(new[] { loggerProvider }) => LoggerProvider = loggerProvider; } public class StatefullLoggerProvider : ILoggerProvider { internal LoggerProviderConfiguration loggerProviderConfiguration; internal Action verbose; internal StatefullLoggerProvider() {} internal void Set(Action verbose, LoggerProviderConfiguration loggerProviderConfiguration) { this.verbose = verbose; this.loggerProviderConfiguration = loggerProviderConfiguration; } public ILogger CreateLogger(string categoryName) => new Logger(categoryName, this); void IDisposable.Dispose(){} } public class MyDbContext : DbContext { readonly Action buildOptionsBuilder; readonly Action verbose; public MyDbContext(Action buildOptionsBuilder, Action verbose=null): base() { this.buildOptionsBuilder = buildOptionsBuilder; this.verbose = verbose; } private Action returnLoggerFactory; protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (verbose != null) { var loggerFactory = StatefullLoggerFactoryPool.Instance.Get(verbose, new LoggerProviderConfiguration { Enabled = true, CommandBuilderOnly = false }); returnLoggerFactory = () => StatefullLoggerFactoryPool.Instance.Return(loggerFactory); optionsBuilder.UseLoggerFactory(loggerFactory); } buildOptionsBuilder(optionsBuilder); } // NOTE: not threadsafe way of disposing public override void Dispose() { returnLoggerFactory?.Invoke(); returnLoggerFactory = null; base.Dispose(); } } private static Action BuildOptionsBuilder(string connectionString, bool inMemory) { return (optionsBuilder) => { if (inMemory) optionsBuilder.UseInMemoryDatabase( "EfCore_NETFramework_Sandbox" ); else //Assembly.GetAssembly(typeof(Program)) optionsBuilder.UseSqlServer( connectionString, sqlServerDbContextOptionsBuilder => sqlServerDbContextOptionsBuilder.MigrationsAssembly("EfCore.NETFramework.Sandbox") ); }; } class Logger : ILogger { readonly string categoryName; readonly StatefullLoggerProvider statefullLoggerProvider; public Logger(string categoryName, StatefullLoggerProvider statefullLoggerProvider) { this.categoryName = categoryName; this.statefullLoggerProvider = statefullLoggerProvider; } public IDisposable BeginScope(TState state) => null; public bool IsEnabled(LogLevel logLevel) => statefullLoggerProvider?.verbose != null; static readonly List events = new List { "Microsoft.EntityFrameworkCore.Database.Connection.ConnectionClosing", "Microsoft.EntityFrameworkCore.Database.Connection.ConnectionClosed", "Microsoft.EntityFrameworkCore.Database.Command.DataReaderDisposing", "Microsoft.EntityFrameworkCore.Database.Connection.ConnectionOpened", "Microsoft.EntityFrameworkCore.Database.Connection.ConnectionOpening", "Microsoft.EntityFrameworkCore.Infrastructure.ServiceProviderCreated", "Microsoft.EntityFrameworkCore.Infrastructure.ContextInitialized" }; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { if (statefullLoggerProvider?.verbose != null) { if (!statefullLoggerProvider.loggerProviderConfiguration.CommandBuilderOnly || (statefullLoggerProvider.loggerProviderConfiguration.CommandBuilderOnly && events.Contains(eventId.Name) )) { var text = formatter(state, exception); statefullLoggerProvider.verbose($"MESSAGE; categoryName={categoryName} eventId={eventId} logLevel={logLevel}" + Environment.NewLine + text); } } } } 

您可以使用有界上下文。 我首先使用EF Coed来创建两个不同的上下文

客户有界上下文不会记录任何查询

 public class CustomerModelDataContext : DbContext { public DbSet Customers { get; set; } public DbSet PostalCodes { get; set; } public CustomerModelDataContext() : base("ConnectionName") { Configuration.LazyLoadingEnabled = true; Configuration.ProxyCreationEnabled = true; Database.SetInitializer(new Initializer()); //Database.Log = message => DBLog.WriteLine(message); } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); } } 

API有界上下文将记录查询

 public class ApiModelDataContext : DbContext { public DbSet ApiTokens { get; set; } public DbSet ApiClients { get; set; } public DbSet ApiApplications { get; set; } public ApiModelDataContext() : base("ConnectionName") { Configuration.LazyLoadingEnabled = true; Configuration.ProxyCreationEnabled = true; Database.SetInitializer(new Initializer()); Database.Log = message => DBLog.WriteLine(message); } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); } } 

这会将查询记录到VS中的调试输出窗口

 public static class DBLog { public static void WriteLine(string message) { Debug.WriteLine(message); } }