c# - net - ¿Debo tomar ILogger, ILogger<T>, ILoggerFactory o ILoggerProvider para una biblioteca?
que es asp net core (6)
¿Esto puede estar relacionado de alguna manera con Pass ILogger o ILoggerFactory a los constructores en AspNet Core? Sin embargo, esto se refiere específicamente al diseño de bibliotecas , no a cómo la aplicación real que utiliza esas bibliotecas implementa su registro.
Estoy escribiendo una .net Standard 2.0 Library que se instalará a través de Nuget, y para permitir que las personas que la usan obtengan información de depuración, dependo de Microsoft.Extensions.Logging.Abstractions para permitir la inyección de un registrador estandarizado.
Sin embargo, estoy viendo varias interfaces, y el código de muestra en la web a veces usa
ILoggerFactory
y crea un registrador en el ctor de la clase.
También hay
ILoggerProvider
que parece una versión de solo lectura de Factory, pero las implementaciones pueden o no implementar ambas interfaces, por lo que tendré que elegir.
(La fábrica parece más común que el proveedor).
Algunos códigos que he visto utilizan la interfaz
ILogger
no genérica e incluso pueden compartir una instancia del mismo registrador, y algunos toman un
ILogger<T>
en su ctor y esperan que el contenedor DI admita tipos genéricos abiertos o el registro explícito de cada uno y cada
ILogger<T>
usa mi biblioteca.
En este momento, creo que
ILogger<T>
es el enfoque correcto, y tal vez un ctor que no toma ese argumento y simplemente pasa un Logger Nulo en su lugar.
De esa manera, si no se necesita registro, no se usa ninguno.
Sin embargo, algunos contenedores DI seleccionan el ctor más grande y, por lo tanto, fallarían de todos modos.
Tengo curiosidad de lo que se supone que debo hacer aquí para crear la menor cantidad de dolor de cabeza para los usuarios, al mismo tiempo que todavía admito el soporte de registro adecuado si lo desea.
Definición
Tenemos 3 interfaces:
ILogger
,
ILoggerProvider
e
ILoggerFactory
.
Veamos el
código fuente
para averiguar sus responsabilidades:
ILogger
: la responsabilidad principal es escribir un mensaje de registro de un
nivel de registro
dado.
ILoggerProvider
: tiene una sola responsabilidad y es crear un
ILogger
.
ILoggerFactory
: tiene 2 responsabilidades, crear un
ILogger
y agregar un
ILoggerProvider
.
Tenga en cuenta que podemos registrar uno o más proveedores (consola, archivo, etc.) en la fábrica. Cuando creamos un registrador utilizando la fábrica, utiliza a todos los proveedores registrados para crear su registrador correspondiente.
ILoggerFactory factory = new LoggerFactory().AddConsole(); // add console provider
factory.AddProvider(new LoggerFileProvider("c://log.txt")); // add file provider
Logger logger = factory.CreateLogger(); // <-- creates a console logger and a file logger
Así que Logger mantiene una lista de registradores, y escribe el mensaje de registro a todos ellos.
Mirando el código fuente
del
ILoggers
, podemos confirmar que el registrador tiene una matriz de
ILoggers
(es decir, LoggerInformation []), y al mismo tiempo está implementando la interfaz de
ILogger
.
Inyección de dependencia
La documentación de MS proporciona 2 métodos para inyectar un registrador:
1. Inyectar la fábrica:
public TodoController(ITodoRepository todoRepository, ILoggerFactory logger) { _todoRepository = todoRepository; _logger = logger.CreateLogger("TodoApi.Controllers.TodoController"); }
crea un registrador con Category = TodoApi.Controllers.TodoController .
2. Inyectando un
ILogger<T>
genéricoILogger<T>
:
public TodoController(ITodoRepository todoRepository, ILogger<TodoController> logger) { _todoRepository = todoRepository; _logger = logger; }
crea un registrador con Categoría = nombre de tipo totalmente calificado de T
En mi opinión, lo que hace que la documentación sea confusa es que no menciona nada sobre la inyección de un
ILogger
no genérico.
En el mismo ejemplo anterior, estamos inyectando un
ITodoRepository
no genérico, y aún así no explica por qué no estamos haciendo lo mismo para
ILogger
.
Según Mark Seemann :
Un Constructor de inyección no debe hacer más que recibir las dependencias.
Inyectar una fábrica en el controlador no es un buen método, ya que no es responsabilidad del controlador inicializar el registrador (violación de SRP).
Al mismo tiempo, inyectar un
ILogger<T>
genérico
ILogger<T>
agrega ruido innecesario.
Ver la publicación de Steven en el
blog Simple Injector
Lo que se debe inyectar (al menos de acuerdo con el artículo anterior) es un
ILogger
no genérico, pero eso no es algo que el Contenedor de DI incorporado de Microsoft pueda hacer, y necesita usar una Biblioteca de DI de terceros.
Este es otro artículo de Nikola Malovic, en el que explica sus 5 leyes de IoC.
4ta ley de Nikola de la IoC
Cada constructor de una clase que se esté resolviendo no debería tener otra implementación que no sea aceptar un conjunto de sus propias dependencias.
El
ILogger<T>
fue el real que está hecho para DI.
El ILogger vino para ayudar a implementar el patrón de fábrica con mucha más facilidad, en lugar de que escribiera por su cuenta toda la lógica de DI y de fábrica, esa fue una de las decisiones más inteligentes en el núcleo de asp.net.
Puedes elegir entre:
ILogger<T>
si necesita utilizar patrones de fábrica y DI en su código
o
puede usar el
ILogger
, para implementar un registro simple sin necesidad de DI.
dado que, el
ILoggerProvider
es solo un puente para manejar cada uno de los mensajes del registro registrado.
No hay necesidad de usarlo, ya que no afecta a nada de lo que debe intervenir en el código. Escucha el ILoggerProvider registrado y maneja los mensajes.
Eso es todo.
El enfoque predeterminado es
ILogger<T>
.
Esto significa que en el registro los registros de la clase específica serán claramente visibles porque incluirán el nombre completo de la clase como contexto.
Por ejemplo, si el nombre completo de su clase es
MyLibrary.MyClass
, obtendrá esto en las entradas de registro creadas por esta clase.
Por ejemplo:
MyLibrary.MyClass: Información: Mi registro de información
Debería usar
ILoggerFactory
si desea especificar su propio contexto.
Por ejemplo, todos los registros de su biblioteca tienen el mismo contexto de registro en lugar de cada clase.
Por ejemplo:
loggerFactory.CreateLogger("MyLibrary");
Y luego el registro se verá así:
MyLibrary: Información: Mi registro de información
Si lo hace en todas las clases, el contexto será solo MyLibrary para todas las clases. Me imagino que querría hacer eso para una biblioteca si no quiere exponer la estructura interna de la clase en los registros.
Respecto al registro opcional. Creo que siempre debe exigir ILogger o ILoggerFactory en el constructor y dejar que el consumidor de la biblioteca lo apague o proporcione un registrador que no haga nada en la inyección de dependencia si no quieren el registro. Es muy fácil desactivar el registro para un contexto específico en la configuración. Por ejemplo:
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"MyLibrary": "None"
}
}
}
Para el diseño de bibliotecas un buen enfoque sería:
1.No obligue a los consumidores a inyectar logger en sus clases. Simplemente crea otro ctor pasando NullLoggerFactory.
class MyClass
{
private readonly ILoggerFactory _loggerFactory;
public MyClass():this(NullLoggerFactory.Instance)
{
}
public MyClass(ILoggerFactory loggerFactory)
{
this._loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
}
}
2. Limite la cantidad de categorías que utiliza cuando crea los registradores para permitir que los consumidores configuren fácilmente el filtrado de registros.
this._loggerFactory.CreateLogger(Consts.CategoryName)
Siguiendo con la pregunta, creo que
ILogger<T>
es la opción correcta, considerando el inconveniente de otras opciones:
-
La inyección de
ILoggerFactory
obliga a su usuario a ceder el control de la fábrica de registradores globales mutables a su biblioteca de clases. Además, al aceptarILoggerFactory
su clase ahora puede escribir en el registro con cualquier nombre de categoría arbitrario con el métodoCreateLogger
. Si bienILoggerFactory
generalmente está disponible como un singleton en el contenedor DI, yo como usuario dudaría de por qué cualquier biblioteca necesitaría usarlo. -
Si bien el método
ILoggerProvider.CreateLogger
ve así, no está diseñado para ser inyectado. Se utiliza conILoggerFactory.AddProvider
para que la fábrica pueda crearILogger
agregado que escribe en múltiplesILogger
creados a partir de cada proveedor registrado. Esto queda claro cuando inspecciona la implementación deLoggerFactory.CreateLogger
-
Aceptar
ILogger
también parece ser el camino a seguir, pero es imposible con .NET Core DI. Esto realmente suena como la razón por la que necesitaban proporcionarILogger<T>
en primer lugar.
Así que, después de todo, no tenemos una mejor opción que
ILogger<T>
, si tuviéramos que elegir entre esas clases.
Otro enfoque sería inyectar otra cosa que envuelva
ILogger
no genérico, que en este caso no debería ser genérico.
La idea es que al envolverlo con su propia clase, tome el control total de cómo el usuario podría configurarlo.
Todos esos son válidos, excepto para el proveedor de ILogger. ILogger e ILogger <T> son lo que se supone que debes usar para el registro. Para obtener un ILogger, utiliza un ILoggerFactory. El ILogger <T> es un acceso directo para obtener un registrador para una categoría en particular (acceso directo para el tipo como categoría).
Cuando usa el ILogger para realizar el registro, cada uno de los proveedores registrados de ILogger tiene la oportunidad de manejar ese mensaje de registro. No es realmente válido para consumir código para llamar directamente al ILoggerProvider.