Czy powinienem wziąć ILogger, ILogger <T>, ILoggerFactory lub ILoggerProvider dla biblioteki?

113

Może to być nieco związane z przekazywaniem ILogger lub ILoggerFactory do konstruktorów w AspNet Core? jednak dotyczy to w szczególności projektowania bibliotek , a nie sposobu, w jaki aplikacja korzystająca z tych bibliotek implementuje swoje rejestrowanie.

Piszę bibliotekę .net Standard 2.0, która zostanie zainstalowana przez Nuget i aby umożliwić osobom używającym tej biblioteki uzyskanie pewnych informacji debugowania, polegam na Microsoft.Extensions.Logging.Abstractions, aby umożliwić wstrzyknięcie standardowego Loggera.

Jednak widzę wiele interfejsów, a przykładowy kod w Internecie czasami używa ILoggerFactoryi tworzy rejestrator w ctor klasy. Jest też ILoggerProviderwersja Factory tylko do odczytu, ale implementacje mogą, ale nie muszą, implementować oba interfejsy, więc musiałbym wybrać. (Fabryka wydaje się bardziej powszechna niż Dostawca).

Część kodu, który widziałem, używa nieogólnego ILoggerinterfejsu i może nawet współużytkować jedno wystąpienie tego samego rejestratora, a niektóre biorą ILogger<T>swój ctor i oczekują, że kontener DI będzie obsługiwał otwarte typy ogólne lub jawną rejestrację każdej ILogger<T>odmiany mojej biblioteki używa.

W tej chwili uważam, że ILogger<T>jest to właściwe podejście i być może ktor, który nie przyjmuje tego argumentu i zamiast tego przekazuje Null Logger. W ten sposób, jeśli nie jest potrzebne żadne logowanie, żadne nie jest używane. Jednak niektóre kontenery DI wybierają największego ctora i tak i tak by się nie udało.

Ciekaw jestem, co ja powinienem robić, żeby stworzyć najmniejszą ilość utrapieniem dla użytkowników, a jednocześnie zapewni odpowiednie wsparcie rejestrowania w razie potrzeby.

Michael Stum
źródło

Odpowiedzi:

113

Definicja

Mamy 3 interfejsy: ILogger, ILoggerProvideri ILoggerFactory. Spójrzmy na kod źródłowy, aby poznać ich obowiązki:

ILogger : jest odpowiedzialny za napisanie komunikatu dziennika o danym poziomie dziennika .

ILoggerProvider : jest odpowiedzialny za utworzenie instancji ILogger(nie należy używać ILoggerProviderbezpośrednio do tworzenia loggera)

ILoggerFactory : możesz zarejestrować jeden lub więcej plikówILoggerProvider w fabryce, która z kolei używa ich wszystkich do tworzenia instancji ILogger. ILoggerFactoryposiada kolekcję ILoggerProviders.

W poniższym przykładzie rejestrujemy 2 dostawców (konsolę i plik) w fabryce. Kiedy tworzymy program rejestrujący, fabryka używa obu tych dostawców do utworzenia wystąpienia programu rejestrującego:

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

Zatem sam rejestrator utrzymuje kolekcję ILoggers i zapisuje komunikat dziennika do wszystkich z nich. Patrząc na kod źródłowy Loggera możemy potwierdzić, że Loggerposiada tablicę ILoggers(tj. LoggerInformation[]), A jednocześnie implementuje ILoggerinterfejs.


Dependency Injection

Dokumentacja MS podaje 2 metody wstrzyknięcia loggera:

1. Wstrzyknięcie fabryki:

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

tworzy Logger z Category = TodoApi.Controllers.TodoController .

2. Wstrzyknięcie leku generycznego ILogger<T>:

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

tworzy rejestrator z Category = w pełni kwalifikowaną nazwą typu TodoController


Moim zdaniem to, co sprawia, że ​​dokumentacja jest zagmatwana, to fakt, że nie wspomina się w niej nic o wstrzykiwaniu nieogólnego pliku ILogger. W tym samym przykładzie powyżej wstrzykiwamy nieogólny, ITodoRepositorya jednak nie wyjaśnia to, dlaczego nie robimy tego samego dla ILogger.

Według Marka Seemanna :

Konstruktor iniekcji nie powinien robić nic więcej niż odbieranie zależności.

Wstrzyknięcie fabryki do kontrolera nie jest dobrym podejściem, ponieważ kontroler nie jest odpowiedzialny za inicjalizację Loggera (naruszenie SRP). W tym samym czasie wstrzyknięcie leku generycznego ILogger<T>powoduje niepotrzebny hałas. Aby uzyskać więcej informacji, zobacz blog Simple Injector : Co jest nie tak z abstrakcją ASP.NET Core DI?

To, co powinno zostać wstrzyknięte (przynajmniej zgodnie z powyższym artykułem), jest nieogólne ILogger, ale nie jest to coś, co może zrobić Wbudowany kontener DI firmy Microsoft i musisz użyć zewnętrznej biblioteki DI. W tych dwóch dokumentach wyjaśniono, jak można używać bibliotek innych firm w programie .NET Core.


To kolejny artykuł Nikoli Malovica, w którym wyjaśnia on swoje 5 praw IoC.

Czwarte prawo Nikoli IoC

Każdy konstruktor rozwiązywanej klasy nie powinien mieć żadnej implementacji innej niż akceptacja zestawu własnych zależności.

Hooman Bahreini
źródło
3
Twoja odpowiedź i artykuł Stevena są jednocześnie najbardziej poprawne i najbardziej przygnębiające.
bokibeg,
30

Wszystkie są ważne z wyjątkiem ILoggerProvider. ILoggeri ILogger<T>są tym, czego powinieneś używać do logowania. Aby uzyskać ILogger, używasz ILoggerFactory. ILogger<T>jest skrótem do pobrania loggera dla określonej kategorii (skrót dla typu jako kategorii).

Gdy używasz ILoggerdo rejestrowania, każdy zarejestrowany ILoggerProviderma szansę obsłużyć ten komunikat dziennika. Nie jest to naprawdę poprawne do używania kodu do ILoggerProviderbezpośredniego wywoływania .

davidfowl
źródło
Dzięki. To sprawia, że ​​myślę, że ILoggerFactorybyłby świetny jako łatwy sposób na okablowanie DI dla konsumentów, mając tylko jedną zależność ( "Po prostu daj mi fabrykę, a utworzę własne loggery" ), ale uniemożliwiłby użycie istniejącego rejestratora ( chyba że konsument użyje jakiegoś opakowania). Wykorzystanie ILogger- generycznego lub nie - pozwala konsumentowi dać mi specjalnie przygotowany rejestrator, ale sprawia, że ​​konfiguracja DI jest potencjalnie dużo bardziej złożona (chyba że używany jest kontener DI obsługujący Open Generics). Czy to brzmi poprawnie? W takim razie myślę, że pójdę z fabryką.
Michael Stum
3
@MichaelStum - Nie jestem pewien, czy podążam tutaj za twoją logiką. Oczekujesz, że Twoi konsumenci będą korzystać z systemu DI, ale potem chcesz przejąć i ręcznie zarządzać zależnościami w Twojej bibliotece? Dlaczego wydaje się to właściwe podejście?
Damien_The_Unbeliever
@Damien_The_Unbeliever To dobra uwaga. Przejęcie fabryki wydaje się dziwne. Myślę, że zamiast brać ILogger<T>, wezmę ILogger<MyClass>lub po prostu ILoggerzamiast tego - w ten sposób użytkownik może połączyć to tylko z jedną rejestracją i bez konieczności otwierania typów generycznych w swoim kontenerze DI. Skłaniając się ku nieogólnemu ILogger, ale będę dużo eksperymentować przez weekend.
Michael Stum
10

To ILogger<T>był rzeczywisty, który jest stworzony dla DI. ILogger przyszedł, aby ułatwić implementację wzorca fabryki, zamiast pisać samodzielnie całą logikę DI i Factory, która była jedną z najmądrzejszych decyzji w asp.net core.

Masz do wyboru:

ILogger<T>jeśli potrzebujesz użyć wzorców fabryki i DI w swoim kodzie lub możesz użyć ILogger, aby zaimplementować proste rejestrowanie bez konieczności stosowania DI.

biorąc pod uwagę to, The ILoggerProviderjest tylko pomostem do obsługi każdej z zarejestrowanych wiadomości dziennika. Nie ma potrzeby go używać, ponieważ nie powoduje niczego, co powinieneś ingerować w kod, nasłuchuje zarejestrowanego ILoggerProvider i obsługuje komunikaty. O to chodzi.

Barr J.
źródło
1
Co ILogger vs ILogger <T> ma wspólnego z DI? Albo zostanie wstrzyknięty, nie?
Matthew Hostetler
W rzeczywistości jest to ILogger <TCategoryName> w MS Docs. Pochodzi z ILogger i nie dodaje żadnych nowych funkcji, z wyjątkiem nazwy klasy do logowania. Zapewnia to również unikalny typ, dzięki czemu DI wstrzykuje poprawnie nazwany program rejestrujący. Rozszerzenia Microsoft rozszerzają nieogólne this ILogger.
Samuel Danielson
8

Trzymając się pytania, uważam, że ILogger<T>jest to właściwa opcja, biorąc pod uwagę wady innych opcji:

  1. Wstrzykiwanie ILoggerFactoryzmusza użytkownika do przekazania kontroli nad modyfikowalną fabryką globalnych rejestratorów do biblioteki klas. Co więcej, akceptując ILoggerFactoryswoją klasę, możesz teraz zapisywać do logowania z dowolną nazwą kategorii za pomocą CreateLoggermetody. Chociaż ILoggerFactoryjest zwykle dostępny jako singleton w kontenerze DI, ja jako użytkownik wątpię, dlaczego jakakolwiek biblioteka miałaby go używać.
  2. Chociaż metoda ILoggerProvider.CreateLoggerwygląda na to, nie jest przeznaczona do wstrzyknięć. Jest używany z, ILoggerFactory.AddProvideraby fabryka mogła tworzyć zagregowane, ILoggerktóre zapisują do wielu ILoggerutworzonych od każdego zarejestrowanego dostawcy. Jest to jasne, gdy sprawdzasz implementacjęLoggerFactory.CreateLogger
  3. Akceptowanie ILoggerrównież wygląda na właściwą drogę, ale jest niemożliwe w przypadku .NET Core DI. To faktycznie brzmi jak powód, dla którego musieli to zapewnić ILogger<T>w pierwszej kolejności.

Nie mamy więc lepszego wyboru niż ILogger<T>gdybyśmy mieli wybierać z tych zajęć.

Innym podejściem byłoby wstrzyknięcie czegoś innego, co opakowuje nieogólne ILogger, co w tym przypadku powinno być nieogólne. Chodzi o to, że opakowując go własną klasą, przejmujesz pełną kontrolę nad tym, jak użytkownik może ją skonfigurować.

tia
źródło
6

Domyślnym podejściem ma być ILogger<T>. Oznacza to, że w dzienniku logi z określonej klasy będą wyraźnie widoczne, ponieważ będą zawierać pełną nazwę klasy jako kontekst. Na przykład, jeśli pełna nazwa twojej klasy to MyLibrary.MyClass, zobaczysz ją we wpisach dziennika utworzonych przez tę klasę. Na przykład:

MyLibrary.MyClass: Information: Mój dziennik informacji

Powinieneś użyć, ILoggerFactoryjeśli chcesz określić własny kontekst. Na przykład, że wszystkie dzienniki z biblioteki mają ten sam kontekst dziennika zamiast każdej klasy. Na przykład:

loggerFactory.CreateLogger("MyLibrary");

A wtedy dziennik będzie wyglądał następująco:

MyLibrary: Information: Mój dziennik informacji

Jeśli zrobisz to we wszystkich klasach, kontekst będzie po prostu MyLibrary dla wszystkich klas. Wyobrażam sobie, że chciałbyś to zrobić dla biblioteki, jeśli nie chcesz ujawniać wewnętrznej struktury klas w dziennikach.

Odnośnie opcjonalnego logowania. Myślę, że zawsze powinieneś wymagać ILogger lub ILoggerFactory w konstruktorze i pozostawić to konsumentowi biblioteki, aby go wyłączyć lub zapewnić Logger, który nie robi nic w iniekcji zależności, jeśli nie chcą rejestrować. Bardzo łatwo jest wyłączyć rejestrowanie dla określonego kontekstu w konfiguracji. Na przykład:

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning",
      "MyLibrary": "None"
    }
  }
}
AlesD
źródło
5

W przypadku projektowania bibliotek dobrym podejściem byłoby:

Nie zmuszaj konsumentów do wprowadzania loggera do twoich klas. Po prostu stwórz kolejnego ctora przekazującego NullLoggerFactory.

class MyClass
{
    private readonly ILoggerFactory _loggerFactory;

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

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

2. Ogranicz liczbę kategorii używanych podczas tworzenia programów rejestrujących, aby umożliwić użytkownikom łatwe konfigurowanie filtrowania dzienników. this._loggerFactory.CreateLogger(Consts.CategoryName)

Ihar Yakimush
źródło