¿Debo tomar ILogger, ILogger <T>, ILoggerFactory o ILoggerProvider para una biblioteca?

113

¿Esto puede estar algo relacionado con Pass ILogger o ILoggerFactory a los constructores en AspNet Core? , sin embargo, se trata específicamente de Diseño de bibliotecas , no de cómo la aplicación real que usa esas bibliotecas implementa su registro.

Estoy escribiendo una biblioteca .net Standard 2.0 que se instalará a través de Nuget, y para permitir que las personas que usan esa biblioteca obtengan información de depuración, dependo de Microsoft.Extensions.Logging.Abstractions para permitir que se inyecte un registrador estandarizado.

Sin embargo, veo múltiples interfaces, y el código de muestra en la web a veces usa ILoggerFactoryy crea un registrador en el ctor de la clase. También hay lo ILoggerProviderque parece una versión de solo lectura de Factory, pero las implementaciones pueden o no implementar ambas interfaces, por lo que tendría que elegir. (Factory parece más común que Provider).

Algunos códigos que he visto usan la ILoggerinterfaz 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 registro explícito de todas y cada una de las ILogger<T>variaciones de mi biblioteca usos.

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 Null Logger en su lugar. De esa forma, si no se necesita registro, no se utiliza ninguno. Sin embargo, algunos contenedores DI recogen el ctor más grande y, por lo tanto, fallarían de todos modos.

Tengo curiosidad por saber qué se supone que debo hacer aquí para crear la menor cantidad de dolor de cabeza para los usuarios, y al mismo tiempo permitir el soporte de registro adecuado si lo desea.

Michael Stum
fuente

Respuestas:

113

Definición

Tenemos 3 interfaces: ILogger, ILoggerProvidery ILoggerFactory. Miremos el código fuente para conocer sus responsabilidades:

ILogger : es responsable de escribir un mensaje de registro de un nivel de registro determinado .

ILoggerProvider : es responsable de crear una instancia de ILogger(no se supone que debe usar ILoggerProviderdirectamente para crear un registrador)

ILoggerFactory : puede registrar uno o más ILoggerProviders con la fábrica, que a su vez los usa todos para crear una instancia de ILogger. ILoggerFactorycontiene una colección de ILoggerProviders.

En el siguiente ejemplo, estamos registrando 2 proveedores (consola y archivo) con la fábrica. Cuando creamos un registrador, la fábrica utiliza estos dos proveedores para crear una instancia de registrador:

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

Por tanto, el registrador en sí mismo mantiene una colección de ILoggercorreos electrónicos y escribe el mensaje de registro en todos ellos. Mirando el código fuente de Logger podemos confirmar que Loggertiene una matriz de ILoggers(ie LoggerInformation[]), y al mismo tiempo está implementando una ILoggerinterfaz.


Inyección de dependencia

La documentación de MS proporciona 2 métodos para inyectar un registrador:

1. Inyectando 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. Inyectar un genérico ILogger<T>:

public TodoController(ITodoRepository todoRepository, ILogger<TodoController> logger)
{
    _todoRepository = todoRepository;
    _logger = logger;
}

crea un registrador con Categoría = nombre de tipo completo de TodoController


En mi opinión, lo que hace que la documentación sea confusa es que no menciona nada sobre la inyección de un archivo no genérico ILogger. En el mismo ejemplo anterior, estamos inyectando un no genérico ITodoRepositoryy, sin embargo, no explica por qué no estamos haciendo lo mismo para ILogger.

Según Mark Seemann :

Un constructor de inyección no debería hacer más que recibir las dependencias.

Inyectar una fábrica en el controlador no es un buen enfoque, porque no es responsabilidad del controlador inicializar el registrador (violación de SRP). Al mismo tiempo, inyectar un genérico ILogger<T>agrega ruido innecesario. Consulte el blog de Simple Injector para obtener más detalles: ¿Qué ocurre con la abstracción de ASP.NET Core DI?

Lo que se debe inyectar (al menos de acuerdo con el artículo anterior) no es genérico ILogger, pero eso no es algo que pueda hacer el Contenedor DI integrado de Microsoft, y debe usar una biblioteca DI de terceros. Estos dos documentos explican cómo puede usar bibliotecas de terceros con .NET Core.


Este es otro artículo de Nikola Malovic, en el que explica sus 5 leyes de IoC.

Cuarta ley de IoC de Nikola

Cada constructor de una clase que se está resolviendo no debería tener ninguna implementación más que aceptar un conjunto de sus propias dependencias.

Hooman Bahreini
fuente
3
Su respuesta y el artículo de Steven son más correctos y más deprimentes al mismo tiempo.
bokibeg
30

Esos son todos válidos excepto ILoggerProvider. ILoggery ILogger<T>son lo que se supone que debes usar para iniciar sesión. Para obtener un ILogger, usa un ILoggerFactory. ILogger<T>es un atajo para obtener un registrador para una categoría en particular (atajo para el tipo como la categoría).

Cuando usa ILoggerpara realizar el registro, cada registrado ILoggerProvidertiene la oportunidad de manejar ese mensaje de registro. Realmente no es válido para consumir código para llamar ILoggerProviderdirectamente al.

Davidfowl
fuente
Gracias. Esto me hace pensar que ILoggerFactorysería excelente como una manera fácil de conectar DI para los consumidores con solo tener 1 dependencia ( "Solo dame una fábrica y crearé mis propios registradores" ), pero evitaría el uso de un registrador existente ( a menos que el consumidor utilice algún envoltorio). Tomar un ILogger, genérico o no, le permite al consumidor darme un registrador que prepararon especialmente, pero hace que la configuración de DI sea potencialmente mucho más compleja (a menos que se use un contenedor DI que admita Open Generics). ¿Eso suena correcto? En ese caso, creo que iré con la fábrica.
Michael Stum
3
@MichaelStum: no estoy seguro de seguir tu lógica aquí. ¿Espera que sus consumidores utilicen un sistema DI pero luego desea hacerse cargo y administrar manualmente las dependencias dentro de su biblioteca? ¿Por qué parecería ese el enfoque correcto?
Damien_The_Unbeliever
@Damien_The_Unbeliever Ese es un buen punto. Tomar una fábrica parece extraño. Creo que en lugar de tomar un ILogger<T>, tomaré un ILogger<MyClass>o incluso solo en su ILoggerlugar; de esa manera, el usuario puede conectarlo con un solo registro y sin requerir genéricos abiertos en su contenedor DI. Se inclina por lo no genérico ILogger, pero experimentará mucho durante el fin de semana.
Michael Stum
10

El ILogger<T>fue el real que está hecho para DI. El ILogger vino para ayudar a implementar el patrón de fábrica mucho más fácilmente, en lugar de que usted escriba por su cuenta toda la lógica DI y Factory, esa fue una de las decisiones más inteligentes en el núcleo de asp.net.

Puede elegir entre:

ILogger<T>si necesita usar patrones de fábrica y DI en su código o podría usar ILogger, para implementar un registro simple sin necesidad de DI.

dado eso, The ILoggerProvideres solo un puente para manejar cada uno de los mensajes del registro registrado. No es necesario usarlo, ya que no afecta nada que deba intervenir en el código, escucha el ILoggerProvider registrado y maneja los mensajes. Eso es todo.

Barr J
fuente
1
¿Qué tienen que ver ILogger vs ILogger <T> con DI? Cualquiera se inyectaría, ¿no?
Matthew Hostetler
En realidad, es ILogger <TCategoryName> en MS Docs. Se deriva de ILogger y no agrega ninguna funcionalidad nueva, excepto el nombre de la clase para registrar. Esto también proporciona un tipo único para que la DI inyecte el registrador con el nombre correcto. Las extensiones de Microsoft amplían las no genéricas this ILogger.
Samuel Danielson
8

Siguiendo la pregunta, creo que ILogger<T>es la opción correcta, considerando las desventajas de otras opciones:

  1. Inyectar ILoggerFactoryfuerza a su usuario a ceder el control de la fábrica de registradores global mutable a su biblioteca de clases. Además, al aceptar ILoggerFactorysu clase, ahora puede escribir en el registro con cualquier nombre de categoría arbitrario con el CreateLoggermétodo. Si bien ILoggerFactorygeneralmente está disponible como singleton en el contenedor DI, yo, como usuario, dudaría de por qué cualquier biblioteca necesitaría usarlo.
  2. Si bien el método lo ILoggerProvider.CreateLoggerparece, no está diseñado para inyectarse. Se utiliza ILoggerFactory.AddProviderpara que la fábrica pueda crear agregados ILoggerque escriban en varios ILoggercreados a partir de cada proveedor registrado. Esto queda claro cuando inspecciona la implementación deLoggerFactory.CreateLogger
  3. Aceptar ILoggertambién parece el camino a seguir, pero es imposible con .NET Core DI. En realidad, esto suena como la razón por la que necesitaban proporcionar ILogger<T>en primer lugar.

Entonces, después de todo, no tenemos mejor opción que ILogger<T>si tuviéramos que elegir entre esas clases.

Otro enfoque sería inyectar algo más que no sea genérico ILogger, que en este caso debería ser no genérico. La idea es que al envolverlo con su propia clase, tome el control total de cómo el usuario podría configurarlo.

tia
fuente
6

El enfoque predeterminado está destinado a ser 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, lo obtendrá en las entradas de registro creadas por esta clase. Por ejemplo:

MyLibrary.MyClass: Información: Mi registro de información

Debe usar el ILoggerFactorysi desea especificar su propio contexto. Por ejemplo, que todos los registros de su biblioteca tienen el mismo contexto de registro en lugar de todas las clases. Por ejemplo:

loggerFactory.CreateLogger("MyLibrary");

Y luego el registro se verá así:

MyLibrary: Información: Mi registro de información

Si hace eso 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 desea exponer la estructura de clases interna en los registros.

Respecto a la tala opcional. Creo que siempre debe requerir ILogger o ILoggerFactory en el constructor y dejar que el consumidor de la biblioteca lo desactive o proporcione un Logger que no haga nada en la inyección de dependencia si no quiere 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"
    }
  }
}
AlesD
fuente
5

Para el diseño de bibliotecas, un buen enfoque sería:

1. No obligue a los consumidores a inyectar logger en sus clases. Simplemente cree 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 el número de categorías que utiliza cuando crea registradores para permitir que los consumidores configuren el filtrado de registros fácilmente. this._loggerFactory.CreateLogger(Consts.CategoryName)

Ihar Yakimush
fuente