Willkommen auf myCSharp.de! Anmelden | kostenlos registrieren
 | Suche | FAQ

Hauptmenü
myCSharp.de
» Startseite
» Forum
» Suche
» Regeln
» Wie poste ich richtig?

Mitglieder
» Liste / Suche
» Wer ist online?

Ressourcen
» FAQ
» Artikel
» C#-Snippets
» Jobbörse
» Microsoft Docs

Team
» Kontakt
» Cookies
» Spenden
» Datenschutz
» Impressum

  • »
  • Community
  • |
  • Diskussionsforum
Dependency Injection, wo schreibe ich allgemein gültigen Code
IntelliSoft
myCSharp.de - Member



Dabei seit:
Beiträge: 13

Themenstarter:

Dependency Injection, wo schreibe ich allgemein gültigen Code

beantworten | zitieren | melden

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
private Nachricht | Beiträge des Benutzers
Th69
myCSharp.de - Experte

Avatar #avatar-2578.jpg


Dabei seit:
Beiträge: 4.355

beantworten | zitieren | melden

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.
private Nachricht | Beiträge des Benutzers
IntelliSoft
myCSharp.de - Member



Dabei seit:
Beiträge: 13

Themenstarter:

beantworten | zitieren | melden

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
private Nachricht | Beiträge des Benutzers
Th69
myCSharp.de - Experte

Avatar #avatar-2578.jpg


Dabei seit:
Beiträge: 4.355

beantworten | zitieren | melden

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).
private Nachricht | Beiträge des Benutzers
IntelliSoft
myCSharp.de - Member



Dabei seit:
Beiträge: 13

Themenstarter:

beantworten | zitieren | melden

Hallo

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

DANKE für die Hilfe!
private Nachricht | Beiträge des Benutzers
Palladin007
myCSharp.de - Member

Avatar #avatar-4140.png


Dabei seit:
Beiträge: 1.782
Herkunft: Düsseldorf

beantworten | zitieren | melden

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.
private Nachricht | Beiträge des Benutzers
IntelliSoft
myCSharp.de - Member



Dabei seit:
Beiträge: 13

Themenstarter:

beantworten | zitieren | melden

Danke für Deinen Input!
Interessant & sehr hilfreich!
private Nachricht | Beiträge des Benutzers
Palladin007
myCSharp.de - Member

Avatar #avatar-4140.png


Dabei seit:
Beiträge: 1.782
Herkunft: Düsseldorf

beantworten | zitieren | melden

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.
private Nachricht | Beiträge des Benutzers
IntelliSoft
myCSharp.de - Member



Dabei seit:
Beiträge: 13

Themenstarter:

beantworten | zitieren | melden

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
private Nachricht | Beiträge des Benutzers
Abt
myCSharp.de - Team

Avatar #avatar-4119.png


Dabei seit:
Beiträge: 15.832

beantworten | zitieren | melden

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
...
private Nachricht | Beiträge des Benutzers