Что мне нужно для библиотеки - ILogger, ILogger <T>, ILoggerFactory или ILoggerProvider?

113

Это может быть несколько связано с передачей ILogger или ILoggerFactory конструкторам в AspNet Core? Однако речь идет именно о дизайне библиотеки , а не о том, как реальное приложение, использующее эти библиотеки, реализует ведение журнала.

Я пишу библиотеку .net Standard 2.0, которая будет установлена ​​через Nuget, и чтобы люди, использующие эту библиотеку, могли получать некоторую отладочную информацию, я полагаюсь на Microsoft.Extensions.Logging.Abstractions, чтобы разрешить внедрение стандартизированного регистратора.

Однако я вижу несколько интерфейсов, а пример кода в Интернете иногда использует ILoggerFactoryи создает регистратор в ctor класса. Также есть ILoggerProviderверсия Factory, доступная только для чтения, но реализации могут реализовывать или не реализовывать оба интерфейса, поэтому мне пришлось бы выбирать. (Factory кажется более распространенным, чем Provider).

Некоторый код, который я видел, использует неуниверсальный ILoggerинтерфейс и может даже совместно использовать один экземпляр одного и того же регистратора, а некоторые берут ILogger<T>в своем ctor и ожидают, что контейнер DI будет поддерживать открытые общие типы или явную регистрацию каждого ILogger<T>варианта моей библиотеки использует.

Прямо сейчас я думаю, что ILogger<T>это правильный подход, и, возможно, оператор, который не принимает этот аргумент и вместо этого просто передает Null Logger. Таким образом, если журналирование не требуется, оно не используется. Однако некоторые контейнеры DI выбирают самый большой ctor и, таким образом, все равно не работают.

Мне любопытно, что я должен делать здесь, чтобы создать наименьшую головную боль для пользователей, при этом обеспечивая при желании надлежащую поддержку ведения журнала.

Майкл Штум
источник

Ответы:

113

Определение

У нас есть 3 интерфейса: ILogger, ILoggerProviderи ILoggerFactory. Давайте посмотрим на исходный код, чтобы узнать их обязанности:

ILogger : отвечает за запись сообщения журнала заданного уровня журнала .

ILoggerProvider : отвечает за создание экземпляра ILogger(вы не должны использовать его ILoggerProviderнапрямую для создания регистратора)

ILoggerFactory : вы можете зарегистрировать один или несколько ILoggerProviders в фабрике, которая, в свою очередь, использует их все для создания экземпляра ILogger. ILoggerFactoryсодержит коллекцию ILoggerProviders.

В приведенном ниже примере мы регистрируем 2 провайдера (консоль и файл) на фабрике. Когда мы создаем регистратор, фабрика использует обоих этих провайдеров для создания экземпляра регистратора:

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

Таким образом, сам регистратор поддерживает коллекцию ILoggers и записывает сообщение журнала для всех из них. Глядя на исходный код Logger, мы можем подтвердить, что у Loggerнего есть массив ILoggers(т.е. LoggerInformation[]), и в то же время он реализует ILoggerинтерфейс.


Внедрение зависимости

Документация MS предоставляет 2 метода для ввода регистратора:

1. Впрыск заводской:

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

создает регистратор с Category = TodoApi.Controllers.TodoController .

2. Введение дженерика ILogger<T>:

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

создает регистратор с категорией = полное имя типа TodoController


На мой взгляд, то , что делает документацию запутанной, что она ничего не знает о инжекции необщего не говоря уже, ILogger. В том же примере, приведенном выше, мы вводим не общий, ITodoRepositoryно он не объясняет, почему мы не делаем то же самое для ILogger.

По словам Марка Земанна :

Конструктор внедрения должен делать не больше, чем получать зависимости.

Внедрение фабрики в Контроллер не является хорошим подходом, потому что Контроллер не отвечает за инициализацию Регистратора (нарушение SRP). В то же время введение дженерика ILogger<T>добавляет ненужного шума. Подробнее см. В блоге Simple Injector : что не так с абстракцией ASP.NET Core DI?

То, что должно быть введено (по крайней мере, в соответствии со статьей выше), не является универсальным ILogger, но тогда это не то, что может сделать встроенный контейнер DI Microsoft, и вам нужно использовать стороннюю библиотеку DI. Эти два документа объясняют, как можно использовать сторонние библиотеки с .NET Core.


Это еще одна статья Николы Маловича, в которой он объясняет свои 5 законов IoC.

4-й закон Николы IoC

Каждый конструктор разрешаемого класса не должен иметь никакой реализации, кроме принятия набора своих собственных зависимостей.

Hooman Bahreini
источник
3
Ваш ответ и статья Стивена самые правильные и в то же время самые удручающие.
bokibeg
30

Все они действительны, кроме ILoggerProvider. ILoggerи ILogger<T>это то, что вы должны использовать для ведения журнала. Чтобы получить файл ILogger, используйте расширение ILoggerFactory. ILogger<T>- это ярлык для получения регистратора для определенной категории (ярлык для типа как категории).

Когда вы используете ILoggerдля ведения журнала, каждый зарегистрированный ILoggerProviderполучает возможность обработать это сообщение журнала. На самом деле недопустимо для использования кода ILoggerProviderнапрямую вызывать .

davidfowl
источник
Спасибо. Это заставляет меня думать, что an ILoggerFactoryбыло бы отличным простым способом подключить DI для потребителей, имея только одну зависимость ( «Просто дайте мне Factory, и я создам свои собственные регистраторы» ), но предотвратит использование существующего регистратора ( если потребитель не использует какую-то оболочку). Взятие ILogger- общего или нет - позволяет потребителю предоставить мне специально подготовленный регистратор, но делает настройку DI потенциально намного более сложной (если не используется контейнер DI, поддерживающий Open Generics). Это звучит правильно? В таком случае, думаю, я пойду с завода.
Майкл Штум
3
@MichaelStum - Я не уверен, что следую твоей логике. Вы ожидаете, что ваши потребители будут использовать систему DI, но затем хотите взять на себя управление и вручную управлять зависимостями в вашей библиотеке? Почему это может показаться правильным?
Damien_The_Unbeliever
@Damien_The_Unbeliever Это хороший аргумент. Брать завод кажется странным. Я думаю, что вместо того, чтобы брать ILogger<T>, я возьму ILogger<MyClass>или даже просто ILoggerвместо этого - таким образом, пользователь может подключить его с помощью только одной регистрации и без необходимости открытия дженериков в своем контейнере DI. Склоняюсь к не-универсальному ILogger, но много экспериментирую в выходные.
Майкл Штум
10

Это ILogger<T>был тот самый, который сделан для DI. ILogger появился для того, чтобы упростить реализацию фабричного паттерна, вместо того, чтобы вы писали самостоятельно всю логику DI и Factory, что было одним из самых умных решений в ядре asp.net.

Вы можете выбирать между:

ILogger<T>если вам необходимо использовать в коде шаблоны factory и DI, или вы можете использовать ILogger, для реализации простого ведения журнала без необходимости DI.

учитывая это, ILoggerProviderэто просто мост для обработки каждого зарегистрированного сообщения журнала. Нет необходимости использовать его, так как он не влияет на то, что вы должны вмешивать в код. Он прослушивает зарегистрированный ILoggerProvider и обрабатывает сообщения. Вот и все.

Барр Дж.
источник
1
Какое отношение имеет ILogger vs ILogger <T> к DI? Либо введут, не так ли?
Мэтью Хостетлер,
На самом деле это ILogger <TCategoryName> в MS Docs. Он является производным от ILogger и не добавляет никаких новых функций, кроме имени класса для журнала. Это также обеспечивает уникальный тип, поэтому DI вводит правильно названный регистратор. Расширения Microsoft расширяют неуниверсальный this ILogger.
Сэмюэл Дэниэлсон
8

Придерживаясь вопроса, я считаю, ILogger<T>что это правильный вариант, учитывая обратную сторону других вариантов:

  1. Внедрение ILoggerFactoryзаставляет вашего пользователя передать управление изменяемой фабрикой глобального регистратора вашей библиотеке классов. Более того, приняв ILoggerFactoryваш класс, вы можете писать в журнал с любым произвольным именем категории с помощью CreateLoggerметода. Хотя ILoggerFactoryобычно он доступен как синглтон в контейнере DI, я, как пользователь, сомневаюсь, зачем какой-либо библиотеке его использовать.
  2. Хотя метод ILoggerProvider.CreateLoggerвыглядит так, он не предназначен для инъекций. Он используется, ILoggerFactory.AddProviderчтобы фабрика могла создавать агрегированные данные, ILoggerкоторые записываются в несколько ILoggerсозданных от каждого зарегистрированного провайдера. Это становится ясно, когда вы проверяете реализациюLoggerFactory.CreateLogger
  3. Принятие ILoggerтоже выглядит приемлемым, но с .NET Core DI это невозможно. Это действительно похоже на причину, по которой они должны были предоставить ILogger<T>в первую очередь.

В конце концов, у нас нет лучшего выбора, чем ILogger<T>если бы мы выбирали из этих классов.

Другой подход заключался бы во внедрении чего-то еще, что обертывает неуниверсальный ILogger, который в данном случае не должен быть универсальным. Идея состоит в том, что, обернув его своим собственным классом, вы полностью контролируете, как пользователь может его настроить.

тиа
источник
6

Подход по умолчанию должен быть ILogger<T>. Это означает, что в журнале будут четко видны журналы конкретного класса, поскольку они будут включать полное имя класса в качестве контекста. Например, если у вашего класса полное имя, MyLibrary.MyClassвы получите его в записях журнала, созданных этим классом. Например:

MyLibrary.MyClass: Информация: Мой информационный журнал

Вы должны использовать, ILoggerFactoryесли хотите указать свой собственный контекст. Например, все журналы из вашей библиотеки имеют одинаковый контекст журнала вместо каждого класса. Например:

loggerFactory.CreateLogger("MyLibrary");

И тогда журнал будет выглядеть так:

MyLibrary: Информация: Мой информационный журнал

Если вы сделаете это во всех классах, тогда контекстом будет просто MyLibrary для всех классов. Я полагаю, вы захотите сделать это для библиотеки, если вы не хотите раскрывать внутреннюю структуру классов в журналах.

По поводу необязательного ведения журнала. Я думаю, вам всегда следует требовать ILogger или ILoggerFactory в конструкторе и оставлять его потребителю библиотеки, чтобы отключить его или предоставить Logger, который ничего не делает при внедрении зависимостей, если они не хотят вести журнал. Очень легко отключить ведение журнала для определенного контекста в конфигурации. Например:

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning",
      "MyLibrary": "None"
    }
  }
}
AlesD
источник
5

Для дизайна библиотеки хорошим подходом будет:

1. Не заставляйте потребителей внедрять логгер в ваши классы. Просто создайте еще один ctor, передав NullLoggerFactory.

class MyClass
{
    private readonly ILoggerFactory _loggerFactory;

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

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

2. Ограничьте количество категорий, которые вы используете при создании регистраторов, чтобы потребители могли легко настраивать фильтрацию журналов. this._loggerFactory.CreateLogger(Consts.CategoryName)

Игорь Якимуш
источник