Abhängigkeitsinjektion und benannte Logger

74

Ich bin daran interessiert, mehr darüber zu erfahren, wie Benutzer die Protokollierung mit Abhängigkeitsinjektionsplattformen injizieren. Obwohl sich die folgenden Links und meine Beispiele auf log4net und Unity beziehen, werde ich keines davon unbedingt verwenden. Für die Abhängigkeitsinjektion / IOC werde ich wahrscheinlich MEF verwenden, da dies der Standard ist, auf den sich der Rest des Projekts (groß) einlässt.

Ich bin sehr neu in Dependency Injection / ioc und ziemlich neu in C # und .NET (habe nach den letzten 10 Jahren von VC6 und VB6 sehr wenig Produktionscode in C # /. NET geschrieben). Ich habe viele Untersuchungen zu den verschiedenen Protokollierungslösungen durchgeführt, die es gibt, daher denke ich, dass ich ihre Funktionen gut im Griff habe. Ich bin einfach nicht vertraut genug mit der tatsächlichen Mechanik, eine Abhängigkeit zu injizieren (oder, vielleicht "korrekter", eine abstrahierte Version einer Abhängigkeit zu injizieren).

Ich habe andere Beiträge im Zusammenhang mit Protokollierung und / oder Abhängigkeitsinjektion gesehen, wie: Abhängigkeitsinjektion und Protokollierungsschnittstellen

Best Practices für die Protokollierung

Wie würde eine Log4Net Wrapper-Klasse aussehen?

Nochmals zu log4net und Unity IOC config

Meine Frage hat nicht speziell mit "Wie injiziere ich die Protokollierungsplattform xxx mit dem ioc-Tool JJJ?" Zu tun. Ich bin vielmehr daran interessiert, wie die Leute mit dem Verpacken der Protokollierungsplattform (wie oft, aber nicht immer empfohlen) und der Konfiguration (dh app.config) umgegangen sind. Am Beispiel von log4net könnte ich beispielsweise (in app.config) eine Reihe von Loggern konfigurieren und diese Logger (ohne Abhängigkeitsinjektion) auf die Standardmethode für die Verwendung von Code wie folgt abrufen:

private static readonly ILog logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

Wenn mein Logger nicht nach einer Klasse, sondern nach einem Funktionsbereich benannt ist, kann ich alternativ Folgendes tun:

private static readonly ILog logger = LogManager.GetLogger("Login");
private static readonly ILog logger = LogManager.GetLogger("Query");
private static readonly ILog logger = LogManager.GetLogger("Report");

Ich denke also, dass meine "Anforderungen" ungefähr so ​​aussehen würden:

  1. Ich möchte die Quelle meines Produkts von einer direkten Abhängigkeit von einer Protokollierungsplattform isolieren.

  2. Ich möchte in der Lage sein, eine bestimmte benannte Logger-Instanz (die wahrscheinlich dieselbe Instanz unter allen Anforderern derselben benannten Instanz gemeinsam nutzt) entweder direkt oder indirekt durch eine Art Abhängigkeitsinjektion, wahrscheinlich MEF, aufzulösen.

  3. Ich weiß nicht, ob ich dies als harte Anforderung bezeichnen würde, aber ich möchte die Möglichkeit, bei Bedarf einen benannten Logger (anders als den Klassenlogger) zu erhalten. Zum Beispiel könnte ich einen Logger für meine Klasse basierend auf dem Klassennamen erstellen, aber eine Methode erfordert eine besonders umfangreiche Diagnose, die ich separat steuern möchte. Mit anderen Worten, ich möchte möglicherweise, dass eine einzelne Klasse von zwei separaten Logger-Instanzen "abhängt".

Beginnen wir mit Nummer 1. Ich habe eine Reihe von Artikeln gelesen, hauptsächlich hier über Stackoverflow, in denen es darum geht, ob es eine gute Idee ist, sie zu verpacken oder nicht. Sehen Sie sich den Link "Best Practices" oben an und lesen Sie den Kommentar von Jeffrey Hantin , um zu sehen, warum es schlecht ist, log4net zu verpacken. Wenn Sie wickeln würden (und wenn Sie effektiv wickeln könnten), würden Sie ausschließlich zum Zweck der Injektion / Entfernung der direkten Abhängigkeit wickeln? Oder würden Sie auch versuchen, einige oder alle Informationen der log4net app.config zu abstrahieren?

Angenommen, ich möchte System.Diagnostics verwenden. Ich möchte wahrscheinlich einen schnittstellenbasierten Logger implementieren (möglicherweise sogar die "gemeinsame" ILogger / ILog-Schnittstelle verwenden), wahrscheinlich basierend auf TraceSource, damit ich ihn injizieren kann. Würden Sie die Schnittstelle beispielsweise über TraceSource implementieren und einfach die System.Diagnostics app.config-Informationen unverändert verwenden?

Etwas wie das:

public class MyLogger : ILogger
{
  private TraceSource ts;
  public MyLogger(string name)
  {
    ts = new TraceSource(name);
  }

  public void ILogger.Log(string msg)
  {
    ts.TraceEvent(msg);
  }
}

Und benutze es so:

private static readonly ILogger logger = new MyLogger("stackoverflow");
logger.Info("Hello world!")

Fahren Sie mit Nummer 2 fort ... Wie wird eine bestimmte benannte Logger-Instanz aufgelöst? Sollte ich nur die app.config-Informationen der von mir ausgewählten Protokollierungsplattform nutzen (dh die Logger basierend auf dem Namensschema in der app.config auflösen)? Könnte ich im Fall von log4net lieber LogManager "injizieren" (beachten Sie, dass ich weiß, dass dies nicht möglich ist, da es sich um ein statisches Objekt handelt)? Ich könnte LogManager umbrechen (MyLogManager nennen), ihm eine ILogManager-Schnittstelle geben und dann die MyLogManager.ILogManager-Schnittstelle auflösen. Meine anderen Objekte können eine Abhängigkeit (Import in MEF-Sprache) von ILogManager haben (Export aus der Assembly, in der sie implementiert sind). Jetzt könnte ich Objekte wie dieses haben:

public class MyClass
{
  private ILogger logger;
  public MyClass([Import(typeof(ILogManager))] logManager)
  {
    logger = logManager.GetLogger("MyClass");
  }
}

Jedes Mal, wenn ILogManager aufgerufen wird, wird es direkt an den LogManager von log4net delegiert. Alternativ könnte der umschlossene LogManager die ILogger-Instanzen, die er basierend auf der app.config erhält, nehmen und sie dem (a?) MEF-Container nach Namen hinzufügen. Wenn später ein gleichnamiger Logger angefordert wird, wird der umschlossene LogManager nach diesem Namen abgefragt. Wenn der ILogger vorhanden ist, wird er auf diese Weise aufgelöst. Wenn dies mit MEF möglich ist, hat dies einen Vorteil?

In diesem Fall wird tatsächlich nur ILogManager "injiziert" und es können ILogger-Instanzen wie in log4net normalerweise ausgegeben werden. Wie ist diese Art der Injektion (im Wesentlichen einer Fabrik) mit der Injektion der genannten Logger-Instanzen zu vergleichen? Dies ermöglicht eine einfachere Nutzung der app.config-Datei von log4net (oder einer anderen Protokollierungsplattform).

Ich weiß, dass ich benannte Instanzen wie folgt aus dem MEF-Container abrufen kann:

var container = new CompositionContainer(<catalogs and other stuff>);
ILogger logger = container.GetExportedValue<ILogger>("ThisLogger");

Aber wie bekomme ich die genannten Instanzen in den Container? Ich kenne das Attribut-basierte Modell, bei dem ich verschiedene Implementierungen von ILogger haben könnte, von denen jede benannt ist (über ein MEF-Attribut), aber das hilft mir nicht wirklich. Gibt es eine Möglichkeit, so etwas wie eine app.config (oder einen Abschnitt darin) zu erstellen, in der die Logger (alle dieselbe Implementierung) nach Namen aufgelistet sind und die MEF lesen kann? Könnte / sollte es einen zentralen "Manager" (wie MyLogManager) geben, der benannte Logger über die zugrunde liegende app.config auflöst und dann den aufgelösten Logger in den MEF-Container einfügt? Auf diese Weise steht es einer anderen Person zur Verfügung, die Zugriff auf denselben MEF-Container hat (allerdings ohne das Wissen des MyLogManager über die Verwendung der app.config-Informationen von log4net).

Das ist schon ziemlich lange her. Ich hoffe es ist kohärent. Bitte teilen Sie uns spezifische Informationen darüber mit, wie Ihre Abhängigkeit eine Protokollierungsplattform (wir erwägen höchstwahrscheinlich log4net, NLog oder etwas (hoffentlich Dünnes), das auf System.Diagnostics basiert) in Ihre Anwendung eingefügt hat.

Haben Sie den "Manager" injiziert und ihn Logger-Instanzen zurückgeben lassen?

Haben Sie einige Ihrer eigenen Konfigurationsinformationen in Ihrem eigenen Konfigurationsabschnitt oder im Konfigurationsabschnitt Ihrer DI-Plattform hinzugefügt, um das direkte Injizieren von Logger-Instanzen zu vereinfachen (dh Ihre Abhängigkeiten sollten eher von ILogger als von ILogManager abhängen).

Wie wäre es mit einem statischen oder globalen Container, der entweder die ILogManager-Schnittstelle oder die Gruppe der benannten ILogger-Instanzen enthält? Anstatt im herkömmlichen Sinne (über Konstruktor-, Eigenschafts- oder Elementdaten) zu injizieren, wird die Protokollierungsabhängigkeit bei Bedarf explizit aufgelöst. Ist dies ein guter oder schlechter Weg, um Abhängigkeit zu injizieren.

Ich markiere dies als Community-Wiki, da es keine Frage mit einer eindeutigen Antwort zu sein scheint. Wenn sich jemand anders fühlt, können Sie dies jederzeit ändern.

Vielen Dank für jede Hilfe!

Lohn
quelle

Antworten:

38

Ich verwende Ninject, um den aktuellen Klassennamen für die Logger-Instanz wie folgt aufzulösen:

kernel.Bind<ILogger>().To<NLogLogger>()
  .WithConstructorArgument("currentClassName", x => x.Request.ParentContext.Request.Service.FullName);

Der Konstruktor einer NLog-Implementierung könnte folgendermaßen aussehen:

public NLogLogger(string currentClassName)
{
  _logger = LogManager.GetLogger(currentClassName);
}

Dieser Ansatz sollte wohl auch mit anderen IOC-Containern funktionieren.

Robin van der Knaap
quelle
6
Ich habe x.Request.ParentContext.Plan.Type.FullNameden konkreten Klassennamen anstelle des Schnittstellennamens erhalten.
Chadwick
Ich bekomme ParentContext immer als null. Ich habe NLogLogger von Loggerbase geerbt, die ILogger implementiert.
Marschall
Wenn sich die Basisklasse in einem anderen Projekt befindet, stellen Sie sicher, dass Sie den Logger erneut wie folgt initialisieren: [Assembly: log4net.Config.XmlConfigurator (Watch = true)]
Orhan
16

Man kann auch die Common.Logging- Fassade oder die Simple Logging-Fassade verwenden .

Beide verwenden ein Dienstlokalisierungsmuster, um einen ILogger abzurufen.

Ehrlich gesagt ist die Protokollierung eine dieser Abhängigkeiten, die ich beim automatischen Injizieren kaum oder gar nicht sehe.

Die meisten meiner Klassen, für die Protokollierungsdienste erforderlich sind, sehen folgendermaßen aus:

public class MyClassThatLogs {
    private readonly ILogger log = Slf.LoggerService.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName);

}

Durch die Verwendung der Simple Logging Facade habe ich ein Projekt von log4net auf NLog umgestellt und zusätzlich zur Protokollierung meiner Anwendung mit NLog die Protokollierung aus einer Drittanbieter-Bibliothek hinzugefügt, die log4net verwendet. Das heißt, die Fassade hat uns gute Dienste geleistet.

Eine Einschränkung, die schwer zu vermeiden ist, ist der Verlust von Funktionen, die für das eine oder andere Protokollierungsframework spezifisch sind. Das vielleicht häufigste Beispiel hierfür sind benutzerdefinierte Protokollierungsstufen.

Quentin-Starin
quelle
Vielen Dank für den Tipp zu SLF und Common.Logging. Ich habe kürzlich tatsächlich mit diesen Bibliotheken experimentiert und sie sind beide ziemlich cool. Letztendlich denke ich, dass unser Projekt DI wahrscheinlich nicht zum Abrufen der Protokollierung verwenden wird, sodass wir möglicherweise SLF oder Common.Logging verwenden.
Lohn
13

Dies ist für alle von Vorteil, die versuchen, herauszufinden, wie eine Logger-Abhängigkeit eingefügt werden kann, wenn dem zu injizierenden Logger eine Protokollierungsplattform wie log4net oder NLog bereitgestellt wird. Mein Problem war, dass ich nicht verstehen konnte, wie ich eine Klasse (z. B. MyClass) von einer ILogger-Schnittstelle abhängig machen kann, wenn ich wusste, dass die Auflösung des spezifischen ILogger davon abhängt, den Typ der Klasse zu kennen, die von ILogger abhängig ist ( zB MyClass). Wie erhält die DI / IoC-Plattform / der DI / IoC-Container den richtigen ILogger?

Nun, ich habe mir die Quelle für Castle und NInject angesehen und gesehen, wie sie funktionieren. Ich habe auch AutoFac und StructureMap gesucht.

Castle und NInject bieten beide eine Implementierung der Protokollierung. Beide unterstützen log4net und NLog. Castle unterstützt auch System.Diagnostics. Wenn die Plattform in beiden Fällen die Abhängigkeiten für ein bestimmtes Objekt auflöst (z. B. wenn die Plattform MyClass erstellt und MyClass von ILogger abhängt), delegiert sie die Erstellung der Abhängigkeit (ILogger) an den ILogger- "Provider" (Resolver ist möglicherweise ein größerer gebräuchlicher Begriff). Die Implementierung des ILogger-Anbieters ist dann dafür verantwortlich, eine Instanz von ILogger tatsächlich zu instanziieren und wieder auszugeben, um sie dann in die abhängige Klasse (z. B. MyClass) zu injizieren. In beiden Fällen kennt der Provider / Resolver den Typ der abhängigen Klasse (z. B. MyClass). Wenn MyClass erstellt wurde und seine Abhängigkeiten aufgelöst werden, weiß der ILogger-Resolver, dass die Klasse MyClass ist. Bei Verwendung der von Castle oder NInject bereitgestellten Protokollierungslösungen bedeutet dies, dass die Protokollierungslösung (implementiert als Wrapper über log4net oder NLog) den Typ (MyClass) erhält, sodass sie an log4net.LogManager.GetLogger () oder delegieren kann NLog.LogManager.GetLogger (). (Syntax für log4net und NLog ist nicht 100% sicher, aber Sie haben die Idee).

AutoFac und StructureMap bieten zwar keine Protokollierungsfunktion (zumindest, die ich anhand des Blicks erkennen konnte), scheinen jedoch die Möglichkeit zu bieten, benutzerdefinierte Resolver zu implementieren. Wenn Sie also Ihre eigene Protokollierungsabstraktionsschicht schreiben möchten, können Sie auch einen entsprechenden benutzerdefinierten Resolver schreiben. Wenn der Container ILogger auflösen möchte, wird Ihr Resolver verwendet, um den richtigen ILogger abzurufen, UND er hat Zugriff auf den aktuellen Kontext (dh welche Abhängigkeiten des Objekts werden derzeit erfüllt - welches Objekt ist von ILogger abhängig). Wenn Sie den Typ des Objekts abrufen, können Sie die Erstellung des ILoggers an die aktuell konfigurierte Protokollierungsplattform delegieren (die Sie wahrscheinlich hinter einer Schnittstelle abstrahiert haben und für die Sie einen Resolver geschrieben haben).

Daher waren einige wichtige Punkte erforderlich, von denen ich vermutete, dass sie erforderlich waren, die ich jedoch vorher nicht vollständig verstanden habe:

  1. Letztendlich muss der DI-Container irgendwie wissen, welche Protokollierungsplattform verwendet werden soll. In der Regel wird dazu festgelegt, dass "ILogger" von einem "Resolver" aufgelöst werden soll, der für eine Protokollierungsplattform spezifisch ist (daher verfügt Castle unter anderem über "Resolver" von log4net, NLog und System.Diagnostics). Die Angabe des zu verwendenden Resolvers kann über die Konfigurationsdatei oder programmgesteuert erfolgen.

  2. Der Resolver muss den Kontext kennen, für den die Abhängigkeit (ILogger) aufgelöst wird. Das heißt, wenn MyClass erstellt wurde und von ILogger abhängig ist, muss der Resolver (der Resolver) den aktuellen Typ (MyClass) kennen, wenn er versucht, den richtigen ILogger zu erstellen. Auf diese Weise kann der Resolver die zugrunde liegende Protokollierungsimplementierung (log4net, NLog usw.) verwenden, um den richtigen Protokollierer zu erhalten.

Diese Punkte mögen für die DI / IoC-Benutzer da draußen offensichtlich sein, aber ich komme gerade darauf an, daher habe ich eine Weile gebraucht, um mich damit zu beschäftigen.

Eine Sache, die ich noch nicht herausgefunden habe, ist, wie oder ob so etwas mit MEF möglich ist. Kann ich ein Objekt von einer Schnittstelle abhängig machen und dann meinen Code ausführen lassen, nachdem MEF das Objekt erstellt hat und während die Schnittstelle / Abhängigkeit aufgelöst wird? Angenommen, ich habe eine Klasse wie diese:

public class MyClass
{
  [Import(ILogger)]
  public ILogger logger;

  public MyClass()
  {
  }

  public void DoSomething()
  {
    logger.Info("Hello World!");
  }
}

Wenn MEF die Importe für MyClass auflöst, kann ich einen Teil meines eigenen Codes (über ein Attribut, über eine zusätzliche Schnittstelle zur Implementierung von ILogger an anderer Stelle ???) den ILogger-Import basierend auf der Tatsache ausführen und auflösen lassen MyClass, das sich derzeit im Kontext befindet und eine (möglicherweise) andere ILogger-Instanz zurückgibt, als sie für YourClass abgerufen würde? Implementiere ich eine Art MEF-Anbieter?

Zu diesem Zeitpunkt weiß ich noch nichts über MEF.

Lohnbuch
quelle
Es scheint, als ob MEF und Unity (IoC aus der Gruppe Patterns & Practices von MS) dem Vergleich von Äpfeln und Orangen ähneln. Möglicherweise wird für dieses spezielle Problem ein echter IoC-Container benötigt, der über die Erweiterungspunkte verfügt, um eine benutzerdefinierte Abhängigkeitsauflösung hinzuzufügen. stackoverflow.com/questions/293051/…
Norman H
Wow ... einige Leute haben einfache Dinge wirklich viel schwieriger gemacht, als sie sein mussten. Ich frage mich, wie nützlich das alles wirklich ist, wenn Sie nur einen Text in eine Datei schreiben wollen ...
Ed S.
5

Ich sehe, Sie haben Ihre eigene Antwort herausgefunden :) Aber für Leute in der Zukunft, die diese Frage haben, wie Sie sich NICHT an ein bestimmtes Protokollierungsframework binden können, hilft diese Bibliothek: Common.Logging bei genau diesem Szenario.

CubanX
quelle
Gleicher Kommentar wie für qstarin. Vielen Dank für Ihren Hinweis auf Common.Logging.
Lohn
0

Ich habe meinen benutzerdefinierten ServiceExportProvider erstellt . Der Anbieter registriert den Log4Net-Logger für die Abhängigkeitsinjektion durch MEF. Als Ergebnis können Sie Logger für verschiedene Arten von Injektionen verwenden.

Beispiel für Injektionen:

[Export]
public class Part
{
    [ImportingConstructor]
    public Part(ILog log)
    {
        Log = log;
    }

    public ILog Log { get; }
}

[Export(typeof(AnotherPart))]
public class AnotherPart
{
    [Import]
    public ILog Log { get; set; }
}

Anwendungsbeispiel:

class Program
{
    static CompositionContainer CreateContainer()
    {
        var logFactoryProvider = new ServiceExportProvider<ILog>(LogManager.GetLogger);
        var catalog = new AssemblyCatalog(typeof(Program).Assembly);
        return new CompositionContainer(catalog, logFactoryProvider);
    }

    static void Main(string[] args)
    {
        log4net.Config.XmlConfigurator.Configure();
        var container = CreateContainer();
        var part = container.GetExport<Part>().Value;
        part.Log.Info("Hello, world! - 1");
        var anotherPart = container.GetExport<AnotherPart>().Value;
        anotherPart.Log.Fatal("Hello, world! - 2");
    }
}

Ergebnis in der Konsole:

2016-11-21 13:55:16,152 INFO  Log4Mef.Part - Hello, world! - 1
2016-11-21 13:55:16,572 FATAL Log4Mef.AnotherPart - Hello, world! - 2

Implementierung von ServiceExportProvider :

public class ServiceExportProvider<TContract> : ExportProvider
{
    private readonly Func<string, TContract> _factoryMethod;

    public ServiceExportProvider(Func<string, TContract> factoryMethod)
    {
        _factoryMethod = factoryMethod;
    }

    protected override IEnumerable<Export> GetExportsCore(ImportDefinition definition, AtomicComposition atomicComposition)
    {
        var cb = definition as ContractBasedImportDefinition;
        if (cb?.RequiredTypeIdentity == typeof(TContract).FullName)
        {
            var ce = definition as ICompositionElement;
            var displayName = ce?.Origin?.DisplayName;
            yield return new Export(definition.ContractName, () => _factoryMethod(displayName));
        }
    }
}
R. Titov
quelle