[英]Entity Framework Core: Log queries for a single db context instance
使用 EF Core(或任何与此相关的 ORM)我想跟踪 ORM 在我的软件中的某些操作期间对数据库进行的查询数量。
我之前在 Python 下使用过 SQLAlchemy,并且在该堆栈上设置起来并不容易。 我通常会针对内存中的 SQLite 数据库进行单元测试,以断言为场景所做的查询数量。
现在我想使用 EF Core 做同样的事情,并查看了Logging 文档。
在我的测试设置代码中,我按照文档说:
using (var db = new BloggingContext())
{
var serviceProvider = db.GetInfrastructure<IServiceProvider>();
var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
loggerFactory.AddProvider(new MyLoggerProvider());
}
但是我遇到了我怀疑是以下结果(也来自文档)的问题:
您只需要使用单个上下文实例注册记录器。 注册后,它将用于同一 AppDomain 中上下文的所有其他实例。
我在测试中看到的问题表明我的记录器实现是跨多个上下文共享的(这与我阅读的文档一致)。 并且因为 a) 我的测试运行器并行运行测试并且 b) 我的整个测试套件创建了数百个 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();
}
通常,我使用此功能进行手动测试。 为了保持原始上下文类的清洁,使用重写的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 查询就足够了。 在将它传递给上下文之前,不要忘记将合适的记录器(如控制台)附加到loggerFactory
。
我们可以在测试类构造函数中创建一个loggerFactory
:
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;
}
测试类派生自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<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()
{
}
}
}
我们已经完成了! 所有 SQL 日志都将显示在 Rider/Resharper 测试输出窗口中。
很简单,安装此 Nuget 包 => Microsoft.Extensions.Logging.Console(右键单击您的项目 => 管理 Nuget 包 => 然后查找它)(或在此链接https://www.nuget.org/packages /Microsoft.Extensions.Logging.Console/ ) 然后重建项目 // 然后你的数据库上下文必须看起来像这样 =>
public class Db : DbContext
{
public readonly ILoggerFactory MyLoggerFactory;
public Db()
{
MyLoggerFactory = LoggerFactory.Create(builder => { builder.AddConsole(); });
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseLoggerFactory(MyLoggerFactory);
}
}
为 EF Core 5.0 引入了简单日志记录(多好的名字!)
在配置 DbContext 实例时,可以通过使用 LogTo 从任何类型的应用程序访问 EF Core 日志。 此配置通常在 DbContext.OnConfiguring 的覆盖中完成。 例如:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.LogTo(Console.WriteLine);
或者,LogTo 可以作为 AddDbContext 的一部分或在创建 DbContextOptions 实例以传递给 DbContext 构造函数时调用。
写入文件。 但我宁愿将某种记录器注入 db 上下文并使用它,而不是在上下文中编写日志记录逻辑。
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();
}
阅读: docs.microsoft.com/en-us/ef/core/miscellaneous/logging
应用程序不要为每个上下文实例创建新的 ILoggerFactory 实例,这一点非常重要。 这样做会导致内存泄漏和性能下降。 1
如果您想登录到静态目的地(例如控制台)Ilja 的答案有效,但如果您想首先登录到自定义缓冲区,则当每个 dbContext 将日志消息收集到自己的缓冲区时(以及您想在多用户服务中执行的操作) ,然后是 UPSSS - 内存泄漏(每个几乎空模型的内存泄漏约为 20 mb)...
当 EF6 有简单的解决方案在一行中订阅 Log 事件时,现在以这种方式注入日志:
var messages = new List<string>();
Action<string> 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”! 至少日志函数应该通常可以通过构造函数注入。
*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<StatefullLoggerFactory> construct;
private readonly ConcurrentBag<StatefullLoggerFactory> bag = new ConcurrentBag<StatefullLoggerFactory>();
private StatefullLoggerFactoryPool(Func<StatefullLoggerFactory> construct) =>
this.construct = construct;
public StatefullLoggerFactory Get(Action<string> 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<string> verbose;
internal StatefullLoggerProvider() {}
internal void Set(Action<string> 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<DbContextOptionsBuilder> buildOptionsBuilder;
readonly Action<string> verbose;
public MyDbContext(Action<DbContextOptionsBuilder> buildOptionsBuilder, Action<string> 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<DbContextOptionsBuilder> 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>(TState state) =>
null;
public bool IsEnabled(LogLevel logLevel) =>
statefullLoggerProvider?.verbose != null;
static readonly List<string> events = new List<string> {
"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<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> 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<Customer> Customers { get; set; }
public DbSet<PostalCode> PostalCodes { get; set; }
public CustomerModelDataContext()
: base("ConnectionName")
{
Configuration.LazyLoadingEnabled = true;
Configuration.ProxyCreationEnabled = true;
Database.SetInitializer<CustomerModelDataContext>(new Initializer<CustomerModelDataContext>());
//Database.Log = message => DBLog.WriteLine(message);
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
API 有界上下文将记录查询
public class ApiModelDataContext : DbContext
{
public DbSet<ApiToken> ApiTokens { get; set; }
public DbSet<ApiClient> ApiClients { get; set; }
public DbSet<ApiApplication> ApiApplications { get; set; }
public ApiModelDataContext()
: base("ConnectionName")
{
Configuration.LazyLoadingEnabled = true;
Configuration.ProxyCreationEnabled = true;
Database.SetInitializer<ApiModelDataContext>(new Initializer<ApiModelDataContext>());
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);
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.