Dois-je prendre ILogger, ILogger <T>, ILoggerFactory ou ILoggerProvider pour une bibliothèque?

113

Cela peut être quelque peu lié à Passer ILogger ou ILoggerFactory aux constructeurs dans AspNet Core? , mais il s'agit spécifiquement de la conception de bibliothèques , pas de la manière dont l'application réelle qui utilise ces bibliothèques implémente sa journalisation.

J'écris une bibliothèque .net Standard 2.0 qui sera installée via Nuget, et pour permettre aux personnes utilisant cette bibliothèque d'obtenir des informations de débogage, je dépends de Microsoft.Extensions.Logging.Abstractions pour permettre l'injection d'un enregistreur standardisé.

Cependant, je vois plusieurs interfaces et des exemples de code sur le Web utilisent ILoggerFactoryet créent parfois un enregistreur dans le ctor de la classe. Il y a aussi ILoggerProviderqui ressemble à une version en lecture seule de l'usine, mais les implémentations peuvent implémenter ou non les deux interfaces, donc je devrais choisir. (L'usine semble plus courante que le fournisseur).

Certains codes que j'ai vu utilisent l' ILoggerinterface non générique et peuvent même partager une instance du même enregistreur, et certains prennent un ILogger<T>dans leur ctor et s'attendent à ce que le conteneur DI prenne en charge les types génériques ouverts ou l'enregistrement explicite de chaque ILogger<T>variante de ma bibliothèque les usages.

À l'heure actuelle, je pense que ILogger<T>c'est la bonne approche, et peut-être un facteur qui ne prend pas cet argument et passe simplement un Null Logger à la place. De cette façon, si aucune journalisation n'est nécessaire, aucune n'est utilisée. Cependant, certains conteneurs DI choisissent le plus gros facteur et échoueraient donc de toute façon.

Je suis curieux de savoir ce que je suis censé faire ici pour créer le moins de maux de tête pour les utilisateurs, tout en permettant une prise en charge appropriée de la journalisation si vous le souhaitez.

Michael Stum
la source

Réponses:

113

Définition

Nous avons 3 interfaces: ILogger, ILoggerProvideret ILoggerFactory. Regardons le code source pour découvrir leurs responsabilités:

ILogger : est chargé d'écrire un message de journal d'un niveau de journal donné .

ILoggerProvider : est responsable de créer une instance de ILogger(vous n'êtes pas censé utiliser ILoggerProviderdirectement pour créer un enregistreur)

ILoggerFactory : vous pouvez enregistrer un ou plusieurs ILoggerProviders auprès de la fabrique, qui à son tour les utilise tous pour créer une instance de ILogger. ILoggerFactorydétient une collection de ILoggerProviders.

Dans l'exemple ci-dessous, nous enregistrons 2 fournisseurs (console et fichier) auprès de l'usine. Lorsque nous créons un enregistreur, l'usine utilise ces deux fournisseurs pour créer une instance d'enregistreur:

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

Ainsi, l'enregistreur lui-même maintient une collection de ILoggers et il écrit le message de journal à chacun d'eux. En regardant le code source de Logger, nous pouvons confirmer qu'il Loggera un tableau de ILoggers(c'est-à-dire LoggerInformation[]), et en même temps il implémente l' ILoggerinterface.


Injection de dépendance

La documentation MS fournit 2 méthodes pour injecter un enregistreur:

1. Injection de l'usine:

public TodoController(ITodoRepository todoRepository, ILoggerFactory logger)
{
    _todoRepository = todoRepository;
    _logger = logger.CreateLogger("TodoApi.Controllers.TodoController");
}

crée un enregistreur avec Category = TodoApi.Controllers.TodoController .

2. Injection d'un générique ILogger<T>:

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

crée un enregistreur avec Category = nom de type complet de TodoController


À mon avis, ce qui rend la documentation est source de confusion qu'il ne mentionne rien au sujet de l' injection d' un non-générique, ILogger. Dans le même exemple ci-dessus, nous injectons un non générique ITodoRepositoryet pourtant, cela n'explique pas pourquoi nous ne faisons pas la même chose pour ILogger.

Selon Mark Seemann :

Un constructeur d'injection ne doit pas faire plus que recevoir les dépendances.

L'injection d'une usine dans le contrôleur n'est pas une bonne approche, car il n'est pas de la responsabilité du contrôleur d'initialiser l'enregistreur (violation de SRP). En même temps, l'injection d'un générique ILogger<T>ajoute du bruit inutile. Voir le blog de Simple Injector pour plus de détails: Quel est le problème avec l'abstraction ASP.NET Core DI?

Ce qui devrait être injecté (au moins selon l'article ci-dessus) est non générique ILogger, mais ce n'est pas quelque chose que le conteneur DI intégré de Microsoft peut faire, et vous devez utiliser une bibliothèque DI tierce. Ces deux documents expliquent comment vous pouvez utiliser des bibliothèques tierces avec .NET Core.


C'est un autre article de Nikola Malovic, dans lequel il explique ses 10 lois d'IoC.

La 4e loi d'IoC de Nikola

Chaque constructeur d'une classe en cours de résolution ne doit pas avoir d'implémentation autre que l'acceptation d'un ensemble de ses propres dépendances.

Hooman Bahreini
la source
3
Votre réponse et l'article de Steven sont les plus corrects et les plus déprimants à la fois.
bokibeg le
30

Ceux-ci sont tous valables sauf pour ILoggerProvider. ILoggeret ILogger<T>sont ce que vous êtes censé utiliser pour la journalisation. Pour obtenir un ILogger, vous utilisez un ILoggerFactory. ILogger<T>est un raccourci pour obtenir un enregistreur pour une catégorie particulière (raccourci pour le type comme catégorie).

Lorsque vous utilisez ILoggerpour effectuer la journalisation, chaque inscrit ILoggerProvidera la possibilité de gérer ce message de journal. Il n'est pas vraiment valable pour la consommation de code d'appeler ILoggerProviderdirectement.

Davidfowl
la source
Merci. Cela me fait penser qu'un ILoggerFactoryserait formidable comme moyen facile de câbler DI pour les consommateurs en n'ayant qu'une seule dépendance ( "Donnez-moi simplement une usine et je créerai mes propres enregistreurs" ), mais cela empêcherait d'utiliser un enregistreur existant ( à moins que le consommateur n'utilise un wrapper). Prendre un ILogger- générique ou non - permet au consommateur de me donner un enregistreur qu'il a spécialement préparé, mais rend la configuration DI potentiellement beaucoup plus complexe (à moins qu'un conteneur DI prenant en charge Open Generics soit utilisé). Cela vous semble-t-il correct? Dans ce cas, je pense que j'irai avec l'usine.
Michael Stum
3
@MichaelStum - Je ne suis pas sûr de suivre votre logique ici. Vous vous attendez à ce que vos consommateurs utilisent un système DI, mais vous souhaitez ensuite prendre le relais et gérer manuellement les dépendances au sein de votre bibliothèque? Pourquoi cela semble-t-il la bonne approche?
Damien_The_Unbeliever
@Damien_The_Unbeliever C'est un bon point. Prendre une usine semble étrange. Je pense qu'au lieu de prendre un ILogger<T>, je prendrai un ILogger<MyClass>ou même juste à la ILoggerplace - de cette façon, l'utilisateur peut le câbler avec un seul enregistrement, et sans avoir besoin de génériques ouverts dans leur conteneur DI. Penchée vers le non générique ILogger, mais expérimentera beaucoup ce week-end.
Michael Stum
10

Le ILogger<T>était une réelle qui est faite pour DI. L'ILogger est venu pour aider à implémenter le modèle d'usine beaucoup plus facilement, au lieu d'écrire vous-même toute la logique DI et Factory, c'était l'une des décisions les plus intelligentes du noyau asp.net.

Vous pouvez choisir entre:

ILogger<T>si vous avez besoin d'utiliser des modèles d'usine et DI dans votre code ou si vous pouvez utiliser le ILogger, pour implémenter une journalisation simple sans DI nécessaire.

étant donné que, The ILoggerProviderest juste un pont pour gérer chacun des messages du journal enregistré. Il n'est pas nécessaire de l'utiliser, car il n'effectue rien que vous devez intervenir dans le code, il écoute le ILoggerProvider enregistré et gère les messages. C'est à peu près ça.

Barr J
la source
1
Quel est le rapport entre ILogger et ILogger <T> avec DI? L'un ou l'autre serait injecté, non?
Matthew Hostetler
Il s'agit en fait d'ILogger <TCategoryName> dans MS Docs. Il dérive d'ILogger et n'ajoute aucune nouvelle fonctionnalité à l'exception du nom de classe à consigner. Cela fournit également un type unique pour que la DI injecte le logger correctement nommé. Les extensions Microsoft étendent le non générique this ILogger.
Samuel Danielson le
8

S'en tenir à la question, je pense que ILogger<T>c'est la bonne option, compte tenu des inconvénients des autres options:

  1. L'injection ILoggerFactoryforce votre utilisateur à céder le contrôle de la fabrique d'enregistreurs globaux mutables à votre bibliothèque de classes. De plus, en acceptant ILoggerFactoryvotre classe, vous pouvez maintenant écrire dans le journal avec n'importe quel nom de catégorie arbitraire avec CreateLoggerméthode. Bien qu'il ILoggerFactorysoit généralement disponible en tant que singleton dans un conteneur DI, en tant qu'utilisateur, je doute pourquoi une bibliothèque aurait besoin de l'utiliser.
  2. Bien que la méthode lui ILoggerProvider.CreateLoggerressemble, elle n'est pas destinée à l'injection. Il est utilisé avec ILoggerFactory.AddProviderpour que l'usine puisse créer des agrégés ILoggerqui écrivent sur plusieurs ILoggercréés à partir de chaque fournisseur enregistré. Cela est clair lorsque vous inspectez la mise en œuvre deLoggerFactory.CreateLogger
  3. Accepter ILoggerressemble également à la voie à suivre, mais c'est impossible avec .NET Core DI. Cela ressemble en fait à la raison pour laquelle ils devaient fournir ILogger<T>en premier lieu.

Donc, après tout, nous n'avons pas de meilleur choix que ILogger<T>de choisir parmi ces classes.

Une autre approche consisterait à injecter quelque chose d'autre qui enveloppe non générique ILogger, qui dans ce cas devrait être non générique. L'idée est qu'en l'enveloppant avec votre propre classe, vous prenez le contrôle total de la façon dont l'utilisateur peut le configurer.

tia
la source
6

L'approche par défaut est censée être ILogger<T>. Cela signifie que dans le journal, les journaux de la classe spécifique seront clairement visibles car ils incluront le nom complet de la classe comme contexte. Par exemple, si le nom complet de votre classe est, MyLibrary.MyClassvous l'obtiendrez dans les entrées de journal créées par cette classe. Par exemple:

MyLibrary.MyClass: Information: Mon journal d'informations

Vous devez utiliser le ILoggerFactorysi vous souhaitez spécifier votre propre contexte. Par exemple, tous les journaux de votre bibliothèque ont le même contexte de journal au lieu de chaque classe. Par exemple:

loggerFactory.CreateLogger("MyLibrary");

Et puis le journal ressemblera à ceci:

MyLibrary: Information: Mon journal d'informations

Si vous faites cela dans toutes les classes, le contexte sera juste MyLibrary pour toutes les classes. J'imagine que vous voudriez faire cela pour une bibliothèque si vous ne voulez pas exposer la structure de classe interne dans les journaux.

Concernant la journalisation optionnelle. Je pense que vous devriez toujours exiger ILogger ou ILoggerFactory dans le constructeur et laisser au consommateur de la bibliothèque le soin de l'éteindre ou de fournir un Logger qui ne fait rien dans l'injection de dépendance s'il ne veut pas de journalisation. Il est très facile de désactiver la journalisation pour un contexte spécifique dans la configuration. Par exemple:

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning",
      "MyLibrary": "None"
    }
  }
}
AlesD
la source
5

Pour la conception d'une bibliothèque, une bonne approche serait:

Ne forcez pas les consommateurs à injecter un enregistreur dans vos classes. Créez simplement un autre ctor passant NullLoggerFactory.

class MyClass
{
    private readonly ILoggerFactory _loggerFactory;

    public MyClass():this(NullLoggerFactory.Instance)
    {

    }
    public MyClass(ILoggerFactory loggerFactory)
    {
      this._loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
    }
}

2. Limitez le nombre de catégories que vous utilisez lorsque vous créez des enregistreurs pour permettre aux consommateurs de configurer facilement le filtrage des journaux. this._loggerFactory.CreateLogger(Consts.CategoryName)

Ihar Yakimush
la source