related que lazy framework c# unit-testing logging entity-framework-core xunit

c# - que - entity framework include related entities



Entity Framework Core: consultas de registro para una sola instancia de contexto db (3)

Puedes usar un contexto delimitado. Usé EF Coed primero para crear dos contextos diferentes

El contexto delimitado por el cliente no registrará ninguna consulta

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

El contexto delimitado por API registrará las consultas

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

esto registrará la consulta para depurar la ventana de salida en VS

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

Utilizando EF Core (o cualquier ORM para ese asunto) quiero hacer un seguimiento del número de consultas que realiza el ORM a la base de datos durante alguna operación en mi software.

He usado SQLAlchemy en Python anteriormente, y en esa pila esto es fácil de configurar. Normalmente tengo pruebas unitarias que afirman sobre el número de consultas realizadas para un escenario, frente a una base de datos SQLite en memoria.

Ahora quiero hacer lo mismo usando EF Core, y he revisado la documentación de Logging .

En mi código de configuración de prueba, hago lo que dice la documentación:

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

Pero me encuentro con problemas que sospecho que son el resultado de lo siguiente (también de los documentos):

Solo necesita registrar el registrador con una única instancia de contexto. Una vez que lo haya registrado, se usará para todas las demás instancias del contexto en el mismo Dominio de aplicación.

Los problemas que veo en mis pruebas indican que mi implementación de registrador se comparte en múltiples contextos (esto está de acuerdo con los documentos a medida que los leo). Y dado que a) mi corredor de prueba ejecuta pruebas en paralelo yb) mi suite de pruebas completa crea cientos de contextos de base de datos; no funciona muy bien.

Pregunta / problemas:

  • Es lo que quiero posible?
  • Es decir, ¿puedo registrar un registrador con un contexto db que solo se usa para esa instancia de contexto db?
  • ¿Hay otras maneras de lograr lo que estoy tratando de hacer?

Lea esto: docs.microsoft.com/en-us/ef/core/miscellaneous/logging

Es muy importante que las aplicaciones no creen una nueva instancia de ILoggerFactory para cada instancia de contexto. Si lo hace, se producirá una pérdida de memoria y un rendimiento deficiente.1

Si desea iniciar sesión en la desidentificación estática (por ejemplo, consola), la respuesta de Ilja funciona, pero si desea iniciar sesión primero en búferes personalizados, cuando cada dbContext recopila mensajes de registro en su propio búfer (y lo que le gustaría hacer en el servicio multiusuario) , luego UPSSS ... Donde EF6 tenía una solución simple para suscribirse a un evento de registro en una línea, ahora para inyectar su registro de esta manera:

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

deberías escribir el monstruo que agrupa.

PD: Alguien le dice a los arquitectos de Ef Core que tienen una comprensión errónea de DI y de esos localizadores de servicios sofisticados que ellos llaman "contenedores" y que usan con fluidez UseXXX de ASP.Core no puede reemplazar al DI "vulgar" del constructor. Al menos la función de registro debe ser normalmente inyectable.

PPS Lea también este https://github.com/aspnet/EntityFrameworkCore/issues/9613 . Esto significa que al agregar LoggerFactory se rompe el acceso al proveedor de datos InMemory. Esta es una fuga de abstracción tal como es. EF Core tiene una arquitectura horrible.

Código de agrupamiento de 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); } } } }


Llame a DbContextOptionsBuilder.UseLoggerFactory(loggerFactory) para registrar todos los resultados SQL de una instancia de contexto particular. Puede inyectar una fábrica de registradores en el constructor del contexto.

Ejemplo de uso:

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

En general, uso esta característica para las pruebas manuales. Para mantener limpia una clase de contexto original, se declara un contexto comprobable derivado con el método OnConfiguring reemplazado:

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

Es suficiente para registrar consultas SQL (adjuntar registradores a loggerFactory antes de pasarlo al contexto).

Parte II: Pase los registros a la salida xUnit y la ventana de salida de prueba ReSharper

Podemos crear un loggerFactory en el constructor de la clase de prueba:

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

La clase de prueba se deriva de BaseTest que permite escribir en xUnit salida:

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

La parte más difícil es implementar un proveedor de registro que acepte IWriter como parámetro:

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() { } } }

Paquetes requeridos:

<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" />