Sollte ich ILogger, ILogger <T>, ILoggerFactory oder ILoggerProvider für eine Bibliothek nehmen?

113

Dies kann etwas mit Pass ILogger oder ILoggerFactory an Konstruktoren in AspNet Core zusammenhängen? Hier geht es jedoch speziell um das Bibliotheksdesign und nicht darum, wie die eigentliche Anwendung, die diese Bibliotheken verwendet, ihre Protokollierung implementiert.

Ich schreibe eine .net Standard 2.0-Bibliothek, die über Nuget installiert wird. Damit Benutzer dieser Bibliothek Debug-Informationen abrufen können, bin ich auf Microsoft.Extensions.Logging.Abstractions angewiesen, damit ein standardisierter Logger injiziert werden kann.

Ich sehe jedoch mehrere Schnittstellen, und Beispielcode im Web verwendet manchmal ILoggerFactoryeinen Logger im ctor der Klasse und erstellt ihn. Es gibt auch ILoggerProvidereine schreibgeschützte Version der Factory, aber Implementierungen können beide Schnittstellen implementieren oder nicht, also müsste ich mich entscheiden. (Fabrik scheint häufiger als Anbieter).

Einige Codes, die ich gesehen habe, verwenden die nicht generische ILoggerSchnittstelle und teilen möglicherweise sogar eine Instanz desselben Loggers. Einige nehmen eine ILogger<T>in ihren ctor und erwarten, dass der DI-Container offene generische Typen oder die explizite Registrierung jeder einzelnen ILogger<T>Variation meiner Bibliothek unterstützt Verwendet.

Im Moment denke ich, dass dies ILogger<T>der richtige Ansatz ist, und vielleicht ein Ctor, der dieses Argument nicht akzeptiert und stattdessen nur einen Null-Logger übergibt. Auf diese Weise wird keine verwendet, wenn keine Protokollierung erforderlich ist. Einige DI-Container wählen jedoch den größten ctor und würden daher trotzdem ausfallen.

Ich bin gespannt, was ich soll hier zu tun , die geringste Menge an Kopfschmerzen für die Nutzer zu schaffen , während immer noch die richtige Protokollierung Unterstützung ermöglicht , falls gewünscht.

Michael Stum
quelle

Antworten:

113

Definition

Wir haben drei Schnittstellen: ILogger, ILoggerProviderund ILoggerFactory. Schauen wir uns den Quellcode an, um ihre Verantwortlichkeiten herauszufinden:

ILogger : ist dafür verantwortlich, eine Protokollnachricht einer bestimmten Protokollstufe zu schreiben .

ILoggerProvider : ist verantwortlich für das Erstellen einer Instanz von ILogger(Sie sollten nicht ILoggerProviderdirekt zum Erstellen eines Loggers verwenden)

ILoggerFactory : Sie können ein oder mehrere ILoggerProviders bei der Factory registrieren , die wiederum alle zum Erstellen einer Instanz von verwendet ILogger. ILoggerFactoryhält eine Sammlung von ILoggerProviders.

Im folgenden Beispiel registrieren wir 2 Anbieter (Konsole und Datei) bei der Factory. Wenn wir einen Logger erstellen, verwendet die Factory diese beiden Anbieter, um eine Instanz des Loggers zu erstellen:

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

Der Logger selbst verwaltet also eine Sammlung von ILoggers und schreibt die Protokollnachricht an alle. Wenn wir uns den Logger-Quellcode ansehen , können wir bestätigen, dass er Loggerein Array von ILoggers(dh LoggerInformation[]) enthält und gleichzeitig eine ILoggerSchnittstelle implementiert .


Abhängigkeitsspritze

Die MS-Dokumentation bietet zwei Methoden zum Injizieren eines Loggers:

1. Injizieren der Fabrik:

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

erstellt einen Logger mit Category = TodoApi.Controllers.TodoController .

2. Injizieren eines Generikums ILogger<T>:

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

Erstellt einen Logger mit der Kategorie = vollqualifizierter Typname von TodoController


Meiner Meinung nach ist die Dokumentation verwirrend, weil darin nichts über das Injizieren eines nicht generischen Produkts erwähnt wird ILogger. Im gleichen Beispiel oben injizieren wir ein nicht generisches ITodoRepositoryund dennoch erklärt es nicht, warum wir nicht dasselbe für tun ILogger.

Laut Mark Seemann :

Ein Injection Constructor sollte nur die Abhängigkeiten empfangen.

Das Injizieren einer Factory in den Controller ist kein guter Ansatz, da es nicht in der Verantwortung des Controllers liegt, den Logger zu initialisieren (Verstoß gegen SRP). Gleichzeitig ILogger<T>fügt das Injizieren eines Generikums unnötiges Rauschen hinzu. Siehe Einfaches Injector Blog für weitere Informationen: Was ist falsch mit dem ASP.NET - Core DI Abstraktion?

Was injiziert werden sollte (zumindest gemäß dem obigen Artikel), ist nicht generisch ILogger, aber das kann der integrierte DI-Container von Microsoft nicht, und Sie müssen eine DI-Bibliothek eines Drittanbieters verwenden.

Dies ist ein weiterer Artikel von Nikola Malovic, in dem er seine 5 Gesetze des IoC erklärt.

Nikolas 4. IoC-Gesetz

Jeder Konstruktor einer Klasse, die aufgelöst wird, sollte keine andere Implementierung haben, als eine Reihe eigener Abhängigkeiten zu akzeptieren.

Hooman Bahreini
quelle
3
Ihre Antwort und Stevens Artikel sind gleichzeitig am korrektesten und am deprimierendsten.
Bokibeg
30

Diese sind alle gültig bis auf ILoggerProvider. ILoggerund ILogger<T>sind das, was Sie für die Protokollierung verwenden sollen. Um eine zu erhalten ILogger, verwenden Sie eine ILoggerFactory. ILogger<T>ist eine Verknüpfung zum Abrufen eines Loggers für eine bestimmte Kategorie (Verknüpfung für den Typ als Kategorie).

Wenn Sie die ILoggerProtokollierung verwenden, ILoggerProvidererhält jeder Registrierte die Möglichkeit, diese Protokollnachricht zu verarbeiten. Es ist nicht wirklich gültig, Code zu verbrauchen, um ILoggerProviderdirekt in den zu rufen .

Davidfowl
quelle
Vielen Dank. Dies lässt mich denken, dass eine ILoggerFactorygroßartige Möglichkeit wäre, DI für Verbraucher zu verkabeln, indem nur eine Abhängigkeit besteht ( "Gib mir einfach eine Factory und ich erstelle meine eigenen Logger" ), aber die Verwendung eines vorhandenen Loggers verhindern würde ( es sei denn, der Verbraucher verwendet einen Wrapper). Wenn Sie ein ILogger- generisches oder nicht - nehmen, kann der Verbraucher mir einen speziell vorbereiteten Logger geben, aber das DI-Setup ist möglicherweise viel komplexer (es sei denn, es wird ein DI-Container verwendet, der Open Generics unterstützt). Klingt das richtig? In diesem Fall denke ich, dass ich mit der Fabrik gehen werde.
Michael Stum
3
@ MichaelStum - Ich bin mir nicht sicher, ob ich hier deiner Logik folge. Sie erwarten von Ihren Verbrauchern, dass sie ein DI-System verwenden, möchten dann aber Abhängigkeiten in Ihrer Bibliothek übernehmen und manuell verwalten? Warum scheint das der richtige Ansatz zu sein?
Damien_The_Unbeliever
@ Damien_The_Unbeliever Das ist ein guter Punkt. Eine Fabrik zu nehmen scheint seltsam. Ich denke, anstatt eine zu nehmen ILogger<T>, nehme ich eine ILogger<MyClass>oder sogar nur ILogger- auf diese Weise kann der Benutzer sie mit nur einer einzigen Registrierung verkabeln, ohne dass offene Generika in seinem DI-Container erforderlich sind. Ich neige zum Nicht-Generikum ILogger, werde aber am Wochenende viel experimentieren.
Michael Stum
10

Das ILogger<T>war das eigentliche, das für DI gemacht ist. Der ILogger wurde entwickelt, um das Factory-Muster viel einfacher zu implementieren, anstatt die gesamte DI- und Factory-Logik selbst zu schreiben. Dies war eine der klügsten Entscheidungen im asp.net-Kern.

Sie können wählen zwischen:

ILogger<T>Wenn Sie Factory- und DI-Muster in Ihrem Code verwenden müssen oder das verwenden könnten ILogger, um eine einfache Protokollierung zu implementieren, ohne dass DI erforderlich ist.

Angesichts dessen ist The ILoggerProvidernur eine Brücke, um die Nachrichten des registrierten Protokolls zu verarbeiten. Es ist nicht erforderlich, es zu verwenden, da es nichts bewirkt, was Sie in den Code eingreifen sollten. Es hört auf den registrierten ILoggerProvider und verarbeitet die Nachrichten. Das ist alles.

Barr J.
quelle
1
Was hat ILogger vs ILogger <T> mit DI zu tun? Entweder würde injiziert werden, nein?
Matthew Hostetler
Es ist eigentlich ILogger <TCategoryName> in MS Docs. Es leitet sich von ILogger ab und fügt keine neuen Funktionen außer dem zu protokollierenden Klassennamen hinzu. Dies bietet auch einen eindeutigen Typ, sodass der DI den korrekt benannten Logger injiziert. Microsoft-Erweiterungen erweitern das nicht generische this ILogger.
Samuel Danielson
8

Ich ILogger<T>halte mich an die Frage und halte sie für die richtige Option, wenn man die Nachteile anderer Optionen berücksichtigt:

  1. Durch das Injizieren ILoggerFactorywird Ihr Benutzer gezwungen, die Kontrolle über die veränderbare globale Logger-Factory an Ihre Klassenbibliothek weiterzugeben. Wenn Sie ILoggerFactoryIhre Klasse jetzt akzeptieren, können Sie außerdem mit der CreateLoggerMethode in ein beliebiges Kategorienamen schreiben . Während ILoggerFactoryes normalerweise als Singleton im DI-Container verfügbar ist, würde ich als Benutzer bezweifeln, warum eine Bibliothek es verwenden müsste.
  2. Während die Methode so ILoggerProvider.CreateLoggeraussieht, ist sie nicht für die Injektion vorgesehen. Es wird mit verwendet, ILoggerFactory.AddProviderdamit die Factory aggregierte ILoggerSchreibvorgänge erstellen kann, die auf mehrere ILoggervon jedem registrierten Anbieter erstellte Schreibvorgänge geschrieben werden. Dies wird deutlich, wenn Sie die Implementierung von überprüfenLoggerFactory.CreateLogger
  3. Das Akzeptieren scheint ILoggerebenfalls der richtige Weg zu sein, ist jedoch mit .NET Core DI nicht möglich. Dies klingt tatsächlich nach dem Grund, warum sie überhaupt zur Verfügung ILogger<T>stellen mussten.

Wir haben also keine bessere Wahl, als ILogger<T>wenn wir aus diesen Klassen wählen würden.

Ein anderer Ansatz wäre, etwas anderes zu injizieren, das nicht generisch umschließt ILogger, was in diesem Fall nicht generisch sein sollte. Die Idee ist, dass Sie durch das Umschließen mit Ihrer eigenen Klasse die volle Kontrolle darüber übernehmen, wie der Benutzer sie konfigurieren kann.

tia
quelle
6

Der Standardansatz soll sein ILogger<T>. Dies bedeutet, dass im Protokoll die Protokolle der jeweiligen Klasse deutlich sichtbar sind, da sie den vollständigen Klassennamen als Kontext enthalten. Wenn der vollständige Name Ihrer Klasse beispielsweise lautet MyLibrary.MyClass, wird dies in den von dieser Klasse erstellten Protokolleinträgen angezeigt. Beispielsweise:

MyLibrary.MyClass: Information: Mein Informationsprotokoll

Sie sollten das verwenden, ILoggerFactorywenn Sie Ihren eigenen Kontext angeben möchten. Zum Beispiel, dass alle Protokolle aus Ihrer Bibliothek anstelle jeder Klasse denselben Protokollkontext haben. Beispielsweise:

loggerFactory.CreateLogger("MyLibrary");

Und dann sieht das Protokoll folgendermaßen aus:

MyLibrary: Information: Mein Informationsprotokoll

Wenn Sie dies in allen Klassen tun, ist der Kontext nur MyLibrary für alle Klassen. Ich kann mir vorstellen, dass Sie dies für eine Bibliothek tun möchten, wenn Sie die innere Klassenstruktur in den Protokollen nicht verfügbar machen möchten.

In Bezug auf die optionale Protokollierung. Ich denke, Sie sollten immer den ILogger oder die ILoggerFactory im Konstruktor benötigen und es dem Konsumenten der Bibliothek überlassen, sie zu deaktivieren oder einen Logger bereitzustellen, der nichts in der Abhängigkeitsinjektion tut, wenn er keine Protokollierung wünscht. Es ist sehr einfach, die Protokollierung für einen bestimmten Kontext in der Konfiguration zu deaktivieren. Beispielsweise:

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

Für das Bibliotheksdesign wäre ein guter Ansatz:

1. Erzwingen Sie keine Verbraucher, Logger in Ihre Klassen einzufügen. Erstellen Sie einfach einen anderen Ctor, der NullLoggerFactory übergibt.

class MyClass
{
    private readonly ILoggerFactory _loggerFactory;

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

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

2. Begrenzen Sie die Anzahl der Kategorien, die Sie beim Erstellen von Protokollierern verwenden, damit Verbraucher die Filterung von Protokollen einfach konfigurieren können. this._loggerFactory.CreateLogger(Consts.CategoryName)

Ihar Yakimush
quelle