Ядро Entity Framework: запросы журнала для одного экземпляра контекста db
Используя EF Core (или любой ORM, если на то пошло), я хочу отслеживать количество запросов, которые ORM делает для базы данных во время некоторой операции в моем программном обеспечении.
Я раньше использовал SQLAlchemy под Python, и в этом стеке это легко настроить. Обычно у меня есть модульные тесты, которые утверждают число запросов, сделанных для сценария, в базе данных SQLite в памяти.
Теперь я хочу сделать то же самое, используя EF Core, и посмотрел Документацию по протоколированию.
В моем тестовом установочном коде я делаю, как говорится в документации:
using (var db = new BloggingContext())
{
var serviceProvider = db.GetInfrastructure<IServiceProvider>();
var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
loggerFactory.AddProvider(new MyLoggerProvider());
}
Но я сталкиваюсь с проблемами, которые, как я подозреваю, являются результатом следующих (также из документов):
Вам нужно только зарегистрировать регистратор с помощью одного экземпляра контекста. После того, как вы зарегистрировали его, он будет использоваться для всех других экземпляров контекста в том же AppDomain.
Проблемы, которые я вижу в своих тестах, указывают на то, что моя реализация logger делится на несколько контекстов (это соответствует документам, когда я их читал). И так как a) мой тестовый бегун выполняет тесты в параллельном режиме и b) весь тестовый набор создает сотни контекстов db - он не работает очень хорошо.
Вопрос/вопросы:
- Я хочу, чтобы это было возможно?
- т.е. могу ли я зарегистрировать регистратор с контекстом db, который используется только для этого экземпляра контекста db?
- Есть ли другие способы сделать то, что я пытаюсь сделать?
Ответы
Ответ 1
Вызовите DbContextOptionsBuilder.UseLoggerFactory(loggerFactory)
, чтобы зарегистрировать весь вывод SQL конкретного экземпляра контекста. Вы можете ввести logger factory в конструктор контекста.
Пример использования:
//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
, прежде чем передавать его в контекст).
Часть II: Пропустить журналы на выход xUnit и окно вывода теста ReSharper
Мы можем создать 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()
{
}
}
}
Необходимые пакеты:
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="1.1.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="1.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="1.1.1" />
<PackageReference Include="xunit" Version="2.2.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
Ответ 2
Прочитайте это: docs.microsoft.com/en-us/ef/core/miscellaneous/logging
Очень важно, чтобы приложения не создавали новый экземпляр ILoggerFactory для каждого экземпляра контекста. Это приведет к утечке памяти и снижению производительности.
Если вы хотите войти в статическое описание (например, консоль), ответ Ilja работает, но если вы хотите сначала войти в пользовательские буферы, когда каждый dbContext собирает сообщения журнала в свой собственный буфер (и то, что вы хотели бы сделать в многопользовательском сервисе) то UPSSS - утечки памяти (и утечка памяти составляет около 20 мб на почти пустую модель)...
Когда у 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 и те причудливые локаторы служб, которые они называют "контейнерами", и свободно используют UseXXX, которые они заимствуют у ASP.Core не может заменить ими "вульгарный 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);
}
}
}
}
Ответ 3
Вы можете использовать ограниченный контекст. Сначала я использовал 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);
}
}