Laden...

Dependency Injection, wo schreibe ich allgemein gültigen Code

Erstellt von IntelliSoft vor einem Jahr Letzter Beitrag vor einem Jahr 451 Views
I
IntelliSoft Themenstarter:in
13 Beiträge seit 2022
vor einem Jahr
Dependency Injection, wo schreibe ich allgemein gültigen Code

Hallo

Ich beginne ein neues Projekt, unter Verwendung von DI (Habe mich für AutoFac entschieden)
Meine Projekt Struktur ist wie folgt aufgeteilt.
Ich habe ein Projekt "Database" und ein Project "Database.Contracts"
Ein Projekt "Logging" und ein Projekt "Logging.Contracts"
Ein Projekt "SharedDI"
Im SharedDI sind alle anderen Projekte verknüpft. Wobei in den Projekten "Database" und "Logging" nur die entsprechenden "Contracts" verbunden sind.
Soweit, so gut.
In meinem Logging Contract Projekt habe ich ein Interface deklariert, dass unter anderem die Methoden "EnterMethod" und "LeaveMethod" beinhalten.
Damit möchte ich eine schöne Struktur in die Log-File bekommen. Dazu habe ich nun eine Klasse geschrieben, die ich instanziere (dabei EnterMethod ausführe) & dann bei Dispose die "LeaveMethod". Damit ist es einfach, das Logging durchzuführen. Der Code dazu sieht so aus:


using System;

public sealed class LoggingService : IDisposable
{
    IntelliSoft.Logging.Contracts.ILogger myLogger;
    string myMethodName = String.Empty;
    string mySourceFilePath = String.Empty;

    public LoggingService(
        IntelliSoft.Logging.Contracts.ILogger logger,
        IntelliSoft.Logging.Contracts.Enums.TypeOfLoggingLevel? level = null,
        [System.Runtime.CompilerServices.CallerMemberName] string methodName = "",
        [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "")
    {
        myLogger = logger;
        if(level != null)
            myLogger.SetLoggingLevel = level.Value;

        myMethodName = methodName;
        mySourceFilePath = sourceFilePath;
        myLogger.EnterMethod(myMethodName, mySourceFilePath);
    }

    public IntelliSoft.Logging.Contracts.ILogger Logger => myLogger;

    public void Dispose() { myLogger.LeaveMethod(myMethodName, mySourceFilePath); }
}

Das Problem, dass ich hier habe ist, dass ich diesen Code aber in allen Projekten "kopieren" müsste, damit er funktioniert (oder per File-Link) Das scheint mir aber keine saubere Lösung zu sein!? Kann mir bitte jemand sagen, wie ich das richtig mache, dass dieser Code überall Gültigkeit hat und die Regeln der DI nicht verletzt.

DANKE im Voraus

4.939 Beiträge seit 2008
vor einem Jahr

Hallo und willkommen,

packe den Code in eine eigene Klassenbibliothek-Assembly und referenziere diese dann in deinen Projekten. Du wirst sicherlich noch anderen "Gemeinsamen Code" haben, den du dann auch dort unterbringen kannst.

Oder soll "SharedDI" dieses Projekt sein, welches von deinen Hauptprojekten eingebunden wird? Dann packe diese Klasse (bzw. Datei) doch dazu.

I
IntelliSoft Themenstarter:in
13 Beiträge seit 2022
vor einem Jahr

Danke fürs Willkommen heißen!

Genau das, was Du geschrieben hast, versuche ich ja zu vermeiden.
Wenn ich DI nutze, dann möchte ich ja keine (oder so gut wie keine) Abhängigkeit zu einem anderen Projekt haben.
Sondern nur auf das Interface.

Um Deine Frage zu beantworten - Das Projekt SharedDI ist das einzige Projekt, dass alle anderen Projekte referenziert.
Die anderen Projekte referenzieren dann "nur" mehr das Projekt, dass das Interface bereitstellt "*.Contracts"

DANKE

4.939 Beiträge seit 2008
vor einem Jahr

Ich nehme an, daß du diese Klasse immer so verwenden möchtest:


using (new LoggingService(logger, ...))
{
  // ...
}

Dann benötigst du in allen Projekten direkt den Zugriff auf diese Klasse. Hier würde ja auch kein eigenes Interface helfen, da das zugehörige Objekt immer wieder neu erstellt und "disposed" werden soll.

Ich persönlich würde diese Klasse sogar direkt in "Logging.Contracts" packen, da es eben nur eine Hilfsklasse zur einfacheren Benutzung ist (es stellt ja keine separate Logik bereit und der Code wird sich wohl auch nicht groß ändern).

I
IntelliSoft Themenstarter:in
13 Beiträge seit 2022
vor einem Jahr

Hallo

Da hast Du wahrscheinlich recht.
Ich habe es soeben getestet & scheint korrekt zu funktionieren.

DANKE für die Hilfe!

2.079 Beiträge seit 2012
vor einem Jahr

Das Problem, was Du hast, ist ein typisches Problem von dem Ansatz mit Contracts-Projekten: Du versuchst es überall zu verfolgen.
Ich würde es nicht überall erzwingen, sondern nur bei den Komponenten, die auch tatsächlich unabhängig vom Rest existieren können und für die es ggf. Erweiterungen oder andere Implementierungen geben kann oder soll. Beim Rest würde ich es einfacher halten und Abstraktion und Implementierung zusammen in einem Projekt lassen - letzteres als internal.
Logging wäre ein gutes Beispiel, wo sich Contracts anbietet, allerdings enthält das vermutlich kaum Logik (Du nutzt ja hoffentlich ein vorhandenes Framework), was diese Aufteilung wieder ziemlich überflüssig macht - deshalb würde ich das auch lassen.

In deinem Fall sehe ich zwei Wege, um deinen Code anzubieten:* Eigenes Logging-Interface, das eine entsprechende LogMethodExecution-Methode anbietet - da Du das Interface schon hast, bietet sich das ja an.

  • Eine Erweiterungsmethode, die die Methode an ein bestehendes Logging-Interface ergänzt.
    Das würde ich aber nur machen, wenn der Code darin einfach ist und vollständig auf Basis der Abstraktion (dem Logging-Interface) funktioniert - was bei dir auch der Fall ist.
    Und vor allem solltest Du es nicht übertreiben 😉

  • Dein Weg - allerdings sehe ich da das Problem, dass es unauffällig ist, man kann es leichter vergessen. Eine Methode am Interface, das man sowieso nutzt, fällt mehr auf, als eine Klasse.

Und die Klasse würde ich einfach zur Abstraktion legen, sie ist ja auch Teil der Abstraktion, da sie eigentlich nur eine bestehende Abstraktion ohne eigene Logik aufruft.
Du hast also Logging-Interface oder Erweiterungsmethode in einem Projekt, die Methode Methode gibt deine Klasse zurück und die liegt ebenfalls im selben Projekt, wie die Abstraktion.

By the way: ggf. ist ein Struct besser, da das ja überall in jeder Methode genutzt wird, recht klein ist (enthält nur drei Referenzen, die sowieso auf den Stack geladen wurden) und nie länger lebt, als die Methode ausgeführt wird. So sparst Du dir ein Objekt pro Methode, das der GC aufräumen muss.
Und ich würde die Klasse/das Struct möglichst dumm halten, also kein Log im Konstruktor. Die LogMethodExecution-Methode ruft das EnterMethode auf und übergibt an die Klasse/das Struct nur alles nötige, um LeaveMethod aufrufen zu können.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

I
IntelliSoft Themenstarter:in
13 Beiträge seit 2022
vor einem Jahr

Danke für Deinen Input!
Interessant & sehr hilfreich!

2.079 Beiträge seit 2012
vor einem Jahr

By the way:


if (level != null)
    myLogger.SetLoggingLevel = level.Value;

Das solltest Du nicht machen 😉
Generell nicht - das LogLevel gehört an die Methode übergeben.
Oder zumindest dieses Beispiel sollte das LogLevel immer manuell angeben, um Seiteneffekte zu vermeiden.

Oder was denkst Du passiert, wenn jemand in der Methode ein anderen LogLevel definiert?
Damit veränderst Du plötzlich auch alle Log-Aufrufe danach.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

I
IntelliSoft Themenstarter:in
13 Beiträge seit 2022
vor einem Jahr

Hallo

SetLoggingLevel kann nur vor dem ersten Log Event einmalig gesetzt werden, da sonst eine entsprechende Exception geworfen wird.
Ist absichtlich so gemacht, damit ich aus der Config auslesen kann, welches Level gerade gesetzt wurde & eben dadurch das Logging sehr fein einstellen kann 😉

Aber danke für den Gedanken

16.833 Beiträge seit 2008
vor einem Jahr

FYI: Logging ist so ein Ding, das man nicht als eigenen Service abstrahieren sollte.
Verwende LoggerMessages und den ILogger direkt - so ist er gedacht.
High-performance logging with LoggerMessage in ASP.NET Core (gilt für alles und nicht nur ASP.NET Core).
Logging sollte keine Performance kosten - Deine Variante ist sehr heavy.

Das, was Du hier im Endeffekt nachprogrammierst, nennst sich Operation Logging.
In vielen Frameworks und Produkten ist das eingebaut, zB. DataDog oder Application Insights.
Im ILogger von Microsoft ist das mit Log Scopes umgesetzt: https://docs.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line#log-scopes
Um kompatibel mit dem .NET Ökosystem zu bleiben wäre es evtl eine bessere Idee, den originalen
ILogger zu verwenden, und nicht versuchen was inkompatibles zu bauen.

Bei mir sieht eine entsprechende Logger-Klasse dann so aus, die im Projekt "FirmenName.PlatformName.Authorization.Logging" liegt.


public static partial class PlatformAuthorizationLog
{
    [LoggerMessage(
       EventId = 1,
       EventName = nameof(AuthorizationSucceeded),
       Level = LogLevel.Trace,
       Message = "Authorization '{requirementName}' for user '{userIdentityId}' was successful.")]
    public static partial void AuthorizationSucceeded(ILogger logger, Guid userIdentityId, string requirementName);

    [LoggerMessage(
       EventId = 2,
       EventName = nameof(AuthorizationFailed),
       Level = LogLevel.Critical,
       Message = "Authorization '{requirementName}' for user '{userIdentityId}' failed.")]
    public static partial void AuthorizationFailed(ILogger logger, Guid userIdentityId, string requirementName);

  // weitere hier..
}

Einen Logging-Namespace gibts dann pro Feature, wie es das .NET Namespace Design vorsieht, zB
FirmenName.PlatformName.Authentication.Logging FirmenName.PlatformName.Authorization.Logging FirmenName.PlatformName.Features.MyFeatureNameHere.Logging FirmenName.PlatformName.Features.MyFeatureNameHere.AspNetCore.Logging FirmenName.PlatformName.AspNetCore.Logging ...