Что такое код asp cpuenablelogging


Содержание

Логирование¶

В ASP.NET Core есть встроенная поддержка логирования, и это позволяет разработчикам легко работать с желаемым функционалом логирования. Реализация логирования требует минимального количества кода. После этого логирование может быть добавлено в любом нужном месте.

+.. contents:: Разделы: + :local: + :depth: 1

Реализация логирования¶

Чтобы добавить логирование в компонент приложения, нужно запросить либо ILoggerFactory ,либо ILogger через Внедрение зависимостей (Dependency Injection) . Если запрашивается ILoggerFactory , логирование создается с помощью метода CreateLogger . В следующем примере показано, как сделать это:

После создания логирования нужно назвать категорию. Имя категории указывает на источник событий логирования. По соглашению эта строка имеет иерархическую структуру, и категории разделены точками ( . ). У некоторых параметров логирования есть фильтрующая поддержка, которая упрощает определение местоположения результата нужного логирования. В примере выше логирование использует встроенный ConsoleLogger (см. Настройка логирования). Чтобы увидеть, как работает консольное логирование, запустите пример приложения с помощью команды dotnet run и сделайте запрос к настроенному URL ( localhost:5000 ). Вы должны увидеть схожий результат:

Вы можете увидеть несколько логов для одного веб запроса, который вы делаете в браузере, поскольку большинство браузеров будут делать несколько запросов (например, к файлу favicon) при попытке загрузить страницу. Обратите внимание, что в консоли отобразился уровень лога ( info на рисунке сверху), за которым следует категория ( [Catchall Endpoint] ), а затем загруженное сообщение.

Метод логирования может использовать форматную строку с названными метками-заменителями (например, ). Эти заменители заполняются в том порядке, в котором они появляются как значения аргументов, переданных методу. Некоторые провайдеры логирования сохраняют эти имена и значения в словаре, который позже можно запросить. В примере ниже путь запроса передается как метка-заменитель:

В реальных приложениях вы будете добавлять логирование на уровне приложения, а не на уровне фреймворка, событий. Например, если вы создали Web API приложение для управления задачами To-Do (см. Создание первого Web API с помощью MVC 6), вы можете добавить логирование для различных операций, касающихся этих задач.

Логика для API содержится внутри TodoController , который использует Внедрение зависимостей (Dependency Injection) , чтобы запрашивать сервисы, которые ему требуются, через конструктор. В идеале классы должны следовать этому примеру и использовать свой конструктор, чтобы недвусмысленно определять свои зависимости в качестве параметров. Вместо того чтобы напрямую запрашивать ILoggerFactory и создавать экземпляр ILogger , TodoController показывает другой способ работы с логированием — вы запрашиваете ILogger (где T — это класс, запрашивающий логи).

В каждом методе действия контроллера логирование реализуется в локальном поле, _logger , как показано на строке 17. Эта технология не ограничена контроллерами, она может быть использована любым сервисом приложения, который пользуется Внедрение зависимостей (Dependency Injection) .

Работа с ILogger ¶

Как мы только что видели, приложение может запрашивать экземпляр ILogger в качестве зависимости в конструктор класса, где T — это тип, запрашивающий логирование. С помощью TodoController показан пример такого подхода. Когда работает такая технология, при логировании автоматически будет использоваться имя типа в качестве имени категории. При запросе экземпляра ILogger вашему классу не нужно будет создавать экземпляр логгера через ILoggerFactory . Вы можете использовать такой подход везде, если вам не нужен дополнительный функционал ILoggerFactory .

Уровни конкретизации логирования (Verbosity Levels)¶

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

ASP.NET Core определяет 6 уровней конкретизации логов:

Trace Используется для наиболее детальных логов, и они особенно полезны при отладке. Такие сообщения могут содержать чувствительные данные, так что они не должны входить в производственную среду. Disabled by default. Пример: Credentials: <"User":"someuser", "Password":"P@ssword">Debug Эти сообщения обладают краткосрочной пользой в процессе разработки. Они содержат информацию, которая может быть полезна при отладке, но не имеют долгосрочной ценности. Пример: Entering method Configure with flag set to true Information Эти сообщения используются для отслеживания общего потока приложения. Они обладают долгосрочными значениями, в отличии от Verbose . Пример: Request received for path /foo Warning Данные сообщения используются для отслеживания необычных или неожиданных событий в потоке приложения. Это включает в себя ошибки или другие условия, которые не останавливают приложение, но с которыми, возможно, нужно будет разобраться в будущем. При обработке исключений стоит пользоваться уровнем Warning. Примеры: Login failed for IP 127.0.0.1 или FileNotFoundException for file foo.txt Error Данные сообщения используются тогда, когда текущий поток приложения должен быть остановлен из-за ошибки, например, исключения, которое не может быть обработано или из-за которого приложение не может быть восстановлено. Такие сообщения должны указывать на сбой в текущей операции (например, текущем HTTP запросе), а не сбое на уровне приложения. Пример: Cannot insert record due to duplicate key violation Critical Такие сообщения должны использоваться для отслеживания неисправимых сбоев приложения или системы, а также катастрофических сбоев, которые требуют немедленного вмешательства. Пример: потеря данных, нехватка памяти на диске

В пакете Logging есть вспомогательные методы расширения для каждого из этих стандартных значений LogLevel , которые позволяет вам вызывать LogInformation , вместо того чтобы вызывать более подробный метод Log(LogLevel.Information, . ). У каждого метода расширения для определенного LogLevel есть несколько перегруженных вариантов, куда вы можете передавать все или только некоторые следующие параметры:

string data Сообщение лога. EventId eventId Числовой id предназначается для работы с логом, который используется для связывания набора событий логирования с другим таким набором. Событийные ID должны быть статическими и конкретными для определенного вида событий, для которых ведутся логи. Например, событие при использовании покупательской корзины может быть обозначено как событие с id 1000, а завершение покупки как событие с id 1001. Это позволяет фильтровать и обрабатывать логи. string format Формат строки для сообщения лога. object[] args Массив объектов для форматирования. Exception error Исключение для логирования.

Тип EventId можно легко привести к int .

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

В примере с TodoController > Information , а не найденные результаты логируются как Warning (обработка ошибок не показывается).

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

Чтобы просмотреть более детальное логирование на уровне фреймворка, вы можете установить LogLevel на Debug или Trace . Например, если AddConsole вызывает метод Configure , который использует LogLevel.Trace и запускает приложение, то вы увидите такой результат:

Консольный логер прибавляет префикс “dbug: ” результату debug.

Уровень лога Префикс
Critical crit
Error fail
Warning warn
Information info
Debug dbug
Trace trce

Scope¶

Вы можете группировать логические операции внутри scope. scope — это тип IDisposable , возвращаемый при вызове метода BeginScopeImpl , который работает с момента создания до момента размещения. Встроенный логгер `TraceSource`_ возвращает экземпляр scope, который отвечает за запуск и остановку трассирующих операций. Любое состояние логов, например, id трансакции, прикрепляется к scope во время создания.

Scope-типы не стоит использовать часто. Лучше всего использовать их в операциях, которые начинаются и завершают работу в разных местах, например, трансакциях, где задействованы несколько ресурсов.

Настройка логирования¶

Чтобы настроить логирование, вам нужно использовать ILoggerFactory в методе Configure класса Startup . ASP.NET автоматически создаст экземпляр ILoggerFactory с помощью Внедрение зависимостей (Dependency Injection) , когда вы добавите параметр в метод Configure . Как только вы добавите ILoggerFactory в качестве параметра, вы настроите логгеры внутри метода Configure , вызвав методы (или методы расширения) для фабрики логгеров. Мы уже видели такую настройку в начале статьи, когда добавляли консольное логирование с помощью loggerFactory.AddConsole .

Экземпляр LoggerFactory можно дополнительно настроить с помощью пользовательского FilterLoggerSettings . См. пример ниже.

Вы также можете указать уровень конкретизации логов для каждого провайдера. Например, метод расширения AddConsole поддерживает дополнительный параметр для настройки минимального LogLevel .

Вы также можете указать, включить или нет информацию по scope в результат, установив includeScopes на true . Также вы можете указать функцию для фильтрации уровней логов, которые вы хотите включить (например, l => l >= LogLevel.Warning ) или функцию для фильтрации, основываясь на уровнях логов и строках категорий (например, (category,loglevel) => category.Contains(«MyController») && loglevel >= LogLevel.Trace ).

Настройка логирования TraceSource¶

При запуске полной версии .NET Framework вы можете настроить логирование, чтобы можно было использовать существующие библиотеки и провайдеры System.Diagnostics.TraceSource. TraceSource позволяет вам передавать сообщения множеству слушателей и уже используется многими организациями.

Во-первых, добавьте в проект пакет Microsoft.Extensions.Logging.TraceSource (в project.json ), наряду с любыми другими пакетами, которые вы будете использовать (в данном случае, TextWriterTraceListener ):

В следующем примере вы увидите, как настроить экземпляр TraceSourceLogger , который создает логи приоритета Warning или выше. AddTraceSource принимает TraceListener . Вызов настраивает TextWriterTraceListener .

sourceSwitch настроен на использование SourceLevels.Warning , так что TraceListener подхватывает только сообщения Warning (или выше).

Действие API логирует предупреждение, если указанный id не найден:

Чтобы протестировать код, запустите приложение с консоли и перейдите по http://localhost:5000/api/Todo/0 . Вы увидите схожий результат:

Желтая строка с префиксом “warn: ” — это результат ConsoleLogger . Следующая строка, начинающаяся с “TodoApi.Controllers.TodoController” — это результат TraceSource. Есть много других слушателей TraceSource, и можно настроить TextWriterTraceListener , чтобы он использовал любой экземпляр TextWriter .

Настройка других провайдеров¶

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

  • elmah.io — provider for the elmah.io service
  • Loggr — provider for the Loggr service
  • NLog — provider for the NLog library

  • Serilog — provider for the Serilog library

Вы также можете создавать свои провайдеры логирования, чтобы поддерживать другие фреймворки логирования по своим требованиям.

Рекомендации по логированию¶

Вот некоторые полезные рекомендации для реализации логирования в ASP.NET приложениях.

  1. Используйте корректный LogLevel . Это позволит вам видеть логи в соответствии с их важностью.
  2. Используйте информацию о логах, которую легко распознать. Избегайте устаревшей или не соответствующей информации.
  3. Старайтесь быть краткими, не утаивая важную информацию.
  4. Ограничивайте методы логирования, чтобы предотвратить их дополнительные вызовы и перегрузку, особенно при выполнении критических методов.
  5. Называйте логгеры с определенным префиксом, чтобы их легко можно было отключить или отфильтровать. Помните, что Create создаст логгеры, именованные полным именем класса.
  6. Аккуратно используйте Scope, и только для тех действий, которые ограничены началом и концом. Например, ограничивайте провайдеры фреймворка действиями MVC. Избегайте многократного использования scope.
  7. Логирование должно касаться бизнес-решений. Логи, касающиеся фреймворка должны быть более конкретными, нежели логи, касающиеся реализации.

Новые стандартные механизмы .NET. Часть 3. Логирование

Вводная

Это небольшая серия статей про новые стандартные механизмы в .NET. Вместе с выпуском .NET Standard Microsoft выпустили большое количество обвязок, которые должны привести в порядок некоторый зоопарк используемых технологий.

В первой статье (IoC-контейнер) мы поговорили про то, почему это круто — иметь реализацию DI-контейнера по умолчанию, и почему в большинстве случаев стоит использовать именно его. Также написали простую реализацию биндинга по атрибутам.

Во второй (Конфигурация) я рассказал про недостатки старого способа конфигурирования приложения (через app.config/web.config) и про то, как они исправлены в новом подходе.

В этой — третьей — посмотрим на стандартный интерфейс логирования и прикрутим к нему привычный NLog.

Далее следует план статей по очереди их публикации. Все статьи доступны в нашем блоге.

  1. IoC-контейнер.
  2. Конфигурация.
  3. Логирование.

Текущая статья посвящена третьей теме плана — Логирование.

Логирование

Зачем вообще новый инструмент для логирования?

У нас уже есть NLog, log4net, Serilog. Все это замечательные инструменты, но очень грустно, когда в большом решении встречаются они все. Тогда приходится либо переводить все на один, либо впиливать какую-то абстракцию. Еще не очень хорошо использовать конкретное решение без прослойки в распространяемой библиотеке, потому что это опять приведет конечных потребителей к необходимости использовать ваш инструмент (а они уже могут использовать другой).

У нас уже есть Common.Logging, если мы хотим отвязаться от конкретного инструмента (например, в библиотеках). Common.Logging — это, конечно, хорошо, но не факт, что конечные потребители вашей библиотеки захотят его использовать. Еще большой минус, что вы не будете использовать Common.Logging по умолчанию везде, гораздо чаще просто возьмете NLog и забудете о проблеме, пока она не всплывет.

Microsoft.Extensions.Logging отлично решает эти проблемы. Это такой Common.Logging, который используется по умолчанию в Core приложениях, что приводит нас к тому, что мы точно будем использовать этот способ. Теперь не абстракция подстраивается под инструменты (как Common.Logging), а инструмент под абстракцию.

Посмотрим, как это работает. Создадим консольное приложение .NET Core и установим пакеты:

И напишем следующий код, чтобы разобраться, что происходит.

Здесь мы используем самый простой способ инициализации и использования логирования.
Сначала создается фабрика логов LoggerFactory. Затем добавляются провайдеры через методы-расширения AddConsole (вывод на консоль) и AddDebug (вывод в отладочную консоль). Параметры задают предикат записи логов в провайдер. В случае AddConsole мы передали лямбду, которая принимает строку сообщения и уровень протоколирования, а возвращает bool. В AddDebug — передали минимально возможный уровень. В выводе консоли отладки увидим:

В консоль выведется:

Все правильно, для консоли пишем все, для отладки — только Debug и выше

AddConsole и AddDebug — это просто обертки над void AddProvider(ILoggerProvider provider), который добавляет провайдер к этой фабрике логгеров. Соответственно, мы можем добавить консольный логгер следующим образом:

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

Еще стоит упомянуть такую вещь, как Scopes. Они позволяют лучше привязать логирование к бизнес-задачам. Как пример, напишем такую программу:

Вывод на консоль будет таким:

Видно, что скоупы влияют на вывод, причем влияют на все логгеры.

Теперь попробуем прикрутить NLog в ASP.NET Core-приложение. Для начала создадим проект по шаблону «Web-приложение ASP.NET Core», «Пустой».

На момент написания статьи NLog для .NET Core находится в бете, поэтому ставим следующей командой:

После этого поставим пакет NLog.Web.AspNetCore, который содержит рендереры для веб-приложений.

Создадим в корне проекта файл NLog.config и выставим ему правило копирования в выходной каталог — Всегда копировать. Содержимое файла:

В layout мы использовали несколько ASP.NET-специфичных рендереров — например,
$ , для отображения URL запроса.

Теперь изменим наш Startup.cs, добавив туда поддержку NLog.

В метод ConfigureServices добавим:

Это позволит нам инжектить IHttpContextAccessor в singleton-сервисы и иметь доступ к текущему HttpContext.

А в метод Configure добавим следующее:

Метод
AddNLogWeb сохраняет в статическую переменную DI-контейнер (ServiceProvider), из которого NLog будет доставать требуемые ему вещи (например, тот же IHttpContextAccessor).

loggerFactory.AddNlog — добавляет провайдер Nlog-а

После запуска приложения и выполнения любого запроса мы увидим в нашем файле строки, типа такой:


Здесь мы видим все, что требуется для понимания, какой это был запрос.

Сами логгеры органично встроены в инфраструктуру ASP.NET Core — вы можете инжектить ILogger в любой сервис, и он разрезолвится. Если вы хотите работать как в классических приложениях и создавать логгер руками, то просто сохраните экземпляр ILoggerFactory в статическое поле и дергайте CreateLogger, когда вам захочется. Желательно не создавать статических логгеров, а иметь индивидуальный экземпляр логгера на экземпляр класса.

Таким образом, инфраструктура логирования претерпела некоторые изменения, и теперь вам придется использовать именно ее. Протоколирование поднято на новый уровень абстракции, и вы можете выбрать, какой способ логирования использовать. Все системные инструменты/фреймворки (такие, как ASP.NET Core) используют именно такой подход. Все это органично встроено в общую экосистему и не требует от разработчика каких-то особых знаний.

С первой и второй частями цикла можно ознакомиться по ссылкам:

ASP.NET Core — Написание ясного кода в ASP.NET Core с использованием встраивания зависимостей

Эта статья основана на ASP.NET Core 1.0 предварительной версии RC1. Некоторая информация может быть изменена при выпуске версии RC2.

Продукты и технологии:

ASP.NET Core 1.0

В статье рассматриваются:

  • примеры жесткого связывания;
  • написание «честных» классов;
  • встраивание зависимостей в ASP.NET Core;
  • код с возможностью модульного тестирования;
  • тестирование обязанностей контроллера.

ASP.NET Core 1.0 является полностью переписанной ASP.NET, и одной из основных целей в этой новой инфраструктуре является более модульная архитектура. То есть приложения должны иметь возможность использовать только те части инфраструктуры, которые им нужны, причем инфраструктура предоставляет зависимости по мере того, как они запрашиваются. Более того, разработчики, создающие приложения на основе ASP.NET Core, должны иметь возможность использовать ту же функциональность, чтобы поддерживать свои приложения свободно связанными и модульными. С появлением ASP.NET MVC группа ASP.NET значительно улучшила поддержку со стороны инфраструктуры в написании свободно связанного кода, но все равно было очень легко попасть в ловушку жесткого связывания, особенно в классах контроллеров.

Жесткое связывание

Жесткое связывание (tight coupling) хорошо подходит для демонстрационного программного обеспечения. Если вы посмотрите на типичное приложение-пример, показывающее, как создавать сайты на основе ASP.NET MVC (версий 3–5), то скорее всего найдете код, подобный следующему (взят из класса DinnersController приложения-примера NerdDinner, использующего MVC 4):

Глядя на код, чтобы оценить степень его связывания, помните фразу «new является связующим».

Этот тип кода очень сложен в модульном тестировании, потому что NerdDinnerContext создается в процессе конструирования класса и требует подключения к базе данных. Неудивительно, что такие демонстрационные приложения нечасто включают какие-либо модульные тесты. Однако ваше приложение может выиграть от нескольких модульных тестов, даже если вы не занимаетесь разработкой на основе тестов, так что было бы лучше писать код, который можно было бы протестировать. Более того, этот код нарушает принцип Don’t Repeat Yourself (DRY) (принцип «не повторяйся»), поскольку каждый класс контроллера, так или иначе обращающийся к данным, содержит один и тот же код для создания EF-контекста базы данных (Entity Framework). Это делает внесение будущих изменений более дорогостоящим и подверженным ошибкам, особенно по мере развития приложения в течение длительного времени.

Глядя на код, чтобы оценить степень его связывания, помните фразу «new является связующим». То есть везде, где экземпляр класса создается с помощью ключевого слова new, ваша реализация связывается конкретно с этим кодом реализации. Dependency Inversion Principle (принцип инверсии зависимостей) (bit.ly/DI-Principle) утверждает: «Абстракции не должны зависеть от деталей — детали должны зависеть от абстракций». В этом примере детали того, как контроллер извлекает данные для передачи представлению, зависят от деталей того, как эти данные извлекаются, а именно от EF.

Вдобавок к ключевому слову new «статическое сцепление» является еще одним источником жесткого связывания, которое затрудняет тестирование и сопровождение приложений. В предыдущем примере имеется зависимость от системных часов компьютера в виде вызова DateTime.Now. Это связывание усложнило бы создание набора тестовых Dinners для использования в некоторых модульных тестах, так как их свойства EventDate пришлось бы устанавливать относительно текущему показанию часов. Это связывание можно было бы удалить из данного метода несколькими способами, самый простой из которых — переложить заботу об этом на какую-то новую абстракцию, возвращающую Dinners, чтобы это больше не было частью метода. В качестве альтернативы я мог бы сделать значение параметром, чтобы метод возвращал все Dinners после предоставленного параметра DateTime вместо использования только DateTime.Now. Наконец, можно было бы создать абстракцию для текущего времени и ссылаться на текущее время через эту абстракцию. Это может оказаться хорошим подходом, если приложение часто ссылается на DateTime.Now. (Кроме того, заметьте, что, поскольку эти обеды [dinners] предположительно происходят в разных часовых поясах, тип DateTimeOffset может быть более эффективным выбором в реальном приложении.)

Будьте честны

Другая проблема с сопровождением кода вроде этого — он нечестен со взаимодействующими с ним объектами. Вы должны избегать написания классов, экземпляры которых можно создавать в недопустимых состояниях, так как это является частым источником ошибок. Таким образом, все, что необходимо вашему классу для выполнения своих задач, должно предоставляться через его конструктор. Как утверждает Explicit Dependencies Principle (принцип явных зависимостей) (bit.ly/ED-Principle), «методы и классы должны явным образом требовать любые взаимодействующие объекты (collaborating objects), нужные им для корректной работы». Класс DinnersController имеет лишь конструктор по умолчанию, а это подразумевает, что ему не надо взаимодействовать с какими-либо объектами для корректной работы. Но что будет, если вы подвергнете его тесту? Что сделает этот код, если вы запустите его из нового консольного приложения, которое ссылается на MVC-проект?

Первое, что не удастся в этом случае, — попытка создать экземпляр EF-контекста. Код сгенерирует исключение InvalidOperationException: «No connection string named ‘NerdDinnerContext’ could be found in the application config file.» (в конфигурационном файле приложения не найдена строка подключения с именем ‘NerdDinnerContext’). Меня обманули! Для работы этому классу нужно больше, чем заявляет его конструктор! Если классу необходим какой-то способ доступа к наборам экземпляров Dinner, он должен запрашивать их через свой конструктор (или как параметры в своих методах).

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

Встраивание зависимостей (dependency injection, DI) относится к передаче зависимостей класса или метода в виде параметров вместо «зашивания» в код этих связей через new или статические вызовы. Это набирающий популярность метод в .NET-разработке из-за обеспечения им разъединения приложений, в которых он применяется. В ранних версиях ASP.NET преимущества DI не использовались, и, хотя в ASP.NET MVC и Web API наблюдается прогресс в отношении его поддержки, ни одна из инфраструктур до сих пор не предоставляет полной поддержки, в том числе контейнер для управления зависимостями и жизненными циклами их объектов. В ASP.NET Core 1.0 DI не просто полностью поддерживается — оно повсеместно применяется в самом продукте.

В ASP.NET Core 1.0 DI не просто полностью поддерживается — оно повсеместно применяется в самом продукте.

ASP.NET Core не только поддерживает DI, но и включает DI-контейнер, также называемый контейнером Inversion of Control (IoC) или контейнером сервисов. Каждое приложение ASP.NET Core конфигурирует свои зависимости, используя этот контейнер, в методе ConfigureServices класса Startup. Данный контейнер обеспечивает необходимую базовую поддержку, но может быть заменен пользовательской реализацией, если в этом есть потребность. Более того, EF Core также имеет встроенную поддержку DI, поэтому ее конфигурирование в приложении ASP.NET Core сводится к простому вызову метода расширения. Для этой статьи я создал ответвление NerdDinner с именем GeekDinner. EF Core конфигурируется, как показано ниже:

После этого довольно легко запросить через DI экземпляр GeekDinnerDbContext от класса контроллера вроде DinnersController:

Заметьте, что нет ни одного экземпляра ключевого слова new; все зависимости, нужные контроллеру, передаются через его контроллер, и заботится об этом за меня DI-контейнер ASP.NET. В процессе написания приложения мне незачем беспокоиться об инфраструктуре, участвующей в разрешении зависимостей, запрашиваемых моими классами через свои конструкторы. Конечно, при желании можно изменить это поведение, даже заменить контейнер по умолчанию другой реализацией. Поскольку теперь мой класс контроллера следует принципу явных зависимостей, я знаю, что для его работы нужно предоставить экземпляр GeekDinnerDbContext. Выполнив небольшую подготовку для DbContext, я могу создать экземпляр контроллера сам по себе, как демонстрирует это консольное приложение:

При конструировании DbContext в EF Core требуется несколько больше работы, чем в EF6, где просто принималась строка подключения. Дело в том, что, как и ASP.NET Core, EF Core спроектирован в расчете на большую модульность. Обычно вам не понадобится иметь дело напрямую с DbContextOptionsBuilder, так как он используется «за кулисами», когда вы конфигурируете EF через методы расширения вроде AddEntityFramework и AddSqlServer.

А можно ли это протестировать?

Тестирование приложения вручную является важным — вам нужна возможность запустить его и убедиться, что оно действительно запускается и дает ожидаемый вывод. Но поступать так всякий раз, когда вносится какое-то изменение, — пустая трата времени. Одно из значимых преимуществ свободно связанных приложений в том, что они, как правило, лучше поддаются модульному тестированию, чем жестко связанные. Еще важнее, что ASP.NET Core и EF Core гораздо проще в тестировании, чем их предшественники. Для начала я напишу простой тест, который напрямую передает в контроллер какой-то DbContext, который был сконфигурирован на использование хранилища в памяти. Я настрою GeekDinnerDbContext, используя параметр DbContextOptions, который предоставляется этим контекстом через конструктор:

Сконфигурировав это в тестовом классе, легко написать тест, показывающий, что Model в ViewResult возвращает корректные данные:

Конечно, здесь пока что мало логики для тестирования, поэтому данный тест ничего особенного и не проверяет. Критики возразили бы, что этот тест малозначим, и я согласился бы с ними. Однако это отправная точка для будущего теста, когда появится больше логики, что и будет сделано в самом ближайшем будущем. Но сначала, хоть EF Core и поддерживает модульное тестирование в памяти, я все же позабочусь о предотвращении прямого связывания с EF в своем контроллере. Нет никаких причин для связывания обязанностей UI с обязанностями инфраструктуры доступа к данным — по сути, это нарушило бы другой принцип, Separation of Concerns (принцип разделения обязанностей).

Избегайте зависимости от того, что не используется

Принцип отделения интерфейса (Interface Segregation Principle) (bit.ly/LS-Principle) утверждает, что классы должны зависеть только от той функциональности, которую они действительно используют. В случае нового DinnersController с поддержкой DI тот по-прежнему зависит от всего DbContext. Вместо склеивания реализации контроллера с EF можно было бы задействовать некую абстракцию, которая предоставляла бы необходимую функциональность.

Что на самом деле нужно этому методу действия (action method) для должного функционирования? Определенно не весь DbContext. Ему даже не требуется доступ к полному свойству Dinners контекста. Ему достаточно возможности отображать экземпляры Dinner соответствующей страницы. Простейшая .NET-абстракция, представляющая это, — IEnumerable . Поэтому я определю интерфейс, который просто возвращает IEnumerable , и это удовлетворит (большую часть) требований метода Index:

Я называю это репозитарием, поскольку он следует такому шаблону: он абстрагирует доступ к данным интерфейсом, подобным набору. Если по какой-то причине вам не нравится шаблон репозитария или имя, вы можете назвать его IGetDinners, IDinnerService или как угодно иначе (мой рецензент предложил ICanHasDinner). Независимо от того, как вы назовете этот тип, он будет служить той же цели.

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

Покончив с этим, теперь подстроим DinnersController для приема IDinnerRepository в качестве параметра конструктора вместо GeekDinnerDbContext и вызова метода List вместо прямого обращения к Dinners DbSet:

К этому моменту можно скомпилировать и запустить ваше веб-приложение, но вы столкнетесь с исключением, если перейдете к /Dinners: InvalidOperationException («Unable to resolve service for type ‘GeekDinner.Core.Interfaces.IdinnerRepository’ while attempting to activate GeekDinner.Controllers.DinnersController.») («Не удалось разрешить сервис для типа ‘GeekDinner.Core.Interfaces.IdinnerRepository’ при попытке активировать GeekDinner.Controllers.DinnersController.»). Я пока что не реализовал интерфейс, и, как только это будет сделано, мне также понадобится сконфигурировать свою реализацию для использования, когда DI будет выполнять запросы к IDinnerRepository. Реализация этого интерфейса тривиальна:

Заметьте, что прямое связывание реализации репозитария с EF совершенно нормально. Если мне понадобится заменить EF, я просто создам новую реализацию интерфейса. Этот класс реализации является частью инфраструктуры моего приложения, которая представляет собой одно из мест в приложении, где мои классы зависят от конкретных реализаций.

Чтобы сконфигурировать ASP.NET Core на встраивание правильной реализации, когда классы запрашивают IDinnerRepository, нужно добавить следующую строку кода в конец ранее показанного метода ConfigureServices:

Это выражение инструктирует DI-контейнер ASP.NET Core использовать экземпляр DinnerRepository всякий раз, когда требуется разрешить тип, зависимый от экземпляра IDinnerRepository. Scoped означает, что для каждого веб-запроса, обрабатываемого ASP.NET, будет использоваться один экземпляр. Также можно добавлять сервисы, указывая жизненные циклы Transient или Singleton. В данном случае подходит Scoped, поскольку мой DinnerRepository зависит от DbContext, который тоже использует жизненный цикл Scoped. Вот краткое описание доступных сроков жизни объектов.

  • Transient Используется новый экземпляр типа всякий раз, когда запрашивается этот тип.
  • Scoped При первом запросе в рамках данного HTTP-запроса создается новый экземпляр типа, а затем он повторно используется для всех последующих типов, разрешаемых при этом HTTP-запросе.
  • Singleton Единственный экземпляр типа создается один раз и используется всеми последующими запросами для этого типа.

Встроенный контейнер поддерживает несколько способов конструирования типов, которые он будет предоставлять. Самый типичный случай — простая передача типа контейнеру, после чего тот попытается создать экземпляр этого типа, предоставляя ему любые зависимости, которые тип запрашивает по мере выполнения. Вы также можете указать лямбда-выражение для конструирования типа или — в случае жизненного цикла Singleton — передать полностью сконструированный экземпляр в ConfigureServices при его регистрации.

После подключения DI приложение выполняется, как и раньше. Теперь, как показано на рис. 1, я могу тестировать его с применением новой абстракции, используя имитацию или заглушку реализации интерфейса IDinnerRepository вместо того, чтобы напрямую полагаться на EF в коде теста.

Рис. 1. Тестирование DinnersController, используя имитирующий объект


Этот тест работает независимо от того, откуда берется список экземпляров Dinner. Вы могли бы переписать код для доступа к данным, чтобы использовать другую базу данных, Azure Table Storage или XML-файлы, а контроллер все равно работал бы точно так же. Конечно, в данном случае он не делает ничего особенного, поэтому вам, возможно, интересно…

А как насчет реальной логики?

До сих пор я не реализовал никакой реальной бизнес-логики — у меня были лишь простые методы, возвращающие несложные наборы данных. Истинная ценность тестирования проявляется только при наличии логики и особых случаев, в которых вы должны убедиться, что приложение будет вести себя ожидаемым образом. Чтобы продемонстрировать это, я добавлю некоторые требования к своему сайту GeekDinner. Сайт будет предоставлять API, который позволит кому угодно отвечать на приглашение на обед. Однако для числа приглашаемых будет дополнительно задан максимум, и количество ответов на приглашение (RSVP) не должно превышать этот максимум. Пользователи, запрашивающие RSVP, когда максимум уже достигнут, должны добавляться в список ожидания. Наконец, желающие пообедать могут указывать крайний срок относительно их начального времени, по истечении которого они не станут принимать приглашения.

Я мог бы закодировать всю эту логику в метод действия (action), но считаю, что тогда на один метод было бы возложено слишком много обязанностей, особенно на UI-метод, который должен концентрироваться на задачах, связанных с UI, а не на бизнес-логике. Контроллер должен проверять, что ему передаются допустимые входные аргументы, и гарантировать возврат ответов, подходящих для клиента. Решения, выходящие за рамки этих обязанностей, и особенно бизнес-логика не относятся к компетенции контроллеров.

Лучшее место для размещения бизнес-логики — модель предметной области приложения, которая не должна зависеть от обязанностей инфраструктуры (вроде работы с базами данных или UI). Класс Dinner подходит для управления RSVP, описанными в требованиях, поскольку он будет хранить максимальное количество участников мероприятия и знать, сколько RSVP было выдано на данный момент. Однако часть логики также зависит от того, когда появляется RSVP (до или после крайнего срока), поэтому методу понадобится доступ к текущему времени.

Я мог бы просто использовать DateTime.Now, но это затруднило бы тестирование моей логики и связало бы модель предметной области с системными часами. Другой вариант — задействовать абстракцию IDateTime и встроить ее в сущность Dinner. Однако, как показывает мой опыт, лучше всего сохранять сущности вроде Dinner свободными от зависимостей, особенно если вы планируете применять какое-то O/RM-средство наподобие EF для извлечения этих сущностей с уровня хранения. В связи с этим я не хочу заполнять сущности зависимостями, да и EF определенно не смогла бы иметь с этим дело без дополнительного кода с моей стороны. Распространенный подход — изъятие логики из сущности Dinner и перенос ее в какой-либо сервис (вроде DinnerService или RsvpService), в который можно легко встраивать зависимости. Однако это обычно приводит к антишаблону анемичной модели предметной области (anemic domain model antipattern) (bit.ly/anemic-model), в которой сущности имеют очень мало логики (или вообще не имеют ее) и являются просто контейнерами состояния. Нет, в данном случае решение прямолинейное: метод может просто принимать текущее время как параметр и позволять вызывающему коду передавать его.

При таком подходе логика добавления RSVP проста (рис. 2). Для этого метода есть ряд тестов, которые демонстрируют, что он ведет себя, как ожидалось; тесты доступны в проекте-примере, сопутствующем этой статье.

Рис. 2. Бизнес-логика в модели предметной области

Переместив эту логику в модель предметной области, я добился того, что API-метод моего контроллера останется компактным и сконцентрированным на собственных обязанностях. В итоге можно легко протестировать, что контроллер делает то, что ему положено, поскольку через этот метод проходит сравнительно немного путей выполнения.

Обязанности контроллера

Часть обязанностей контроллера — проверка ModelState и обеспечение его допустимости. Для ясности я делаю это в методе действия, но в более крупном приложении я исключил бы этот повторяющийся код в каждом методе действия, используя Action Filter:

Предполагая, что ModelState допустим, метод действия должен извлечь соответствующий экземпляр Dinner по идентификатору, переданному в запросе. Если метод не может найти экземпляр Dinner с совпадающим идентификатором, он должен вернуть результат Not Found:

По завершении этих проверок метод действия может делегировать бизнес-операцию, представленную запросом, модели предметной области, вызвав метод AddRsvp класса Dinner, который вы видели ранее, и сохранив обновленное состояние модели предметной области (в данном случае экземпляр dinner и его набор RSVP); после этого он возвращает ответ OK:

Вспомните: я решил, что у класса Dinner не должно быть зависимости от системных часов, и предпочел передавать текущее время в его метод. В контроллере я передаю _systemClock.Now как параметр currentDateTime. Это локальное поле, заполняемое через DI, что избавляет и контроллер от жесткого связывания с системными часами. Использовать DI в контроллере вполне разумно в противоположность сущности предметной области, так как контроллеры всегда создаются контейнерами сервисов ASP.NET; это подключает любые зависимости, объявленные контроллером в его конструкторе. Поле _systemClock имеет тип IDateTime, что определяется и реализуется всего несколькими строками кода:

Конечно, мне также нужно сконфигурировать контейнер ASP.NET так, чтобы он использовал MachineClockDateTime всякий раз, когда классу требуется экземпляр IDateTime. Это делается в ConfigureServices класса Startup, и, хотя подойдет любой жизненный цикл объекта, в данном случае я предпочел использовать Singleton, так как один экземпляр MachineClockDateTime будет обслуживать все приложение:

Включив эту простую абстракцию, я могу тестировать поведение контроллера в зависимости от того, передан ли крайний срок для RSVP, и гарантировать возврат корректного результата. Поскольку у меня уже есть тесты для метода Dinner.AddRsvp, проверяющие корректность его поведения, мне не понадобится очень много тестов для проверки того же поведения через контроллер, чтобы быть уверенным, что при совместной работе контроллер и модель предметной области функционируют правильно.

Следующие шаги

Скачайте сопутствующий этой статье проект-пример и посмотрите модульные тесты для Dinner и DinnersController. Помните, что свободно связанный код, как правило, гораздо проще в модульном тестировании, чем жестко связанный код, напичканный ключевыми словами new или вызовами статических методов, зависящих от обязанностей инфраструктуры. «New является связующим», так что ключевое слово new следует использовать в приложении осознанно, а не по воле случая. Узнать больше о ASP.NET Core и ее поддержке встраивания зависимостей можно на docs.asp.net.

Стив Смит (Steve Smith) — независимый тренер, преподаватель и консультант, а также обладатель звания ASP.NET MVP. Написал десятки статей для официальной документации ASP.NET Core (docs.asp.net) и работает с группами, осваивающими эту технологию. С ним можно связаться через сайт ardalis.com, также следите за его заметками в Twitter (@ardalis).

Выражаю благодарность за рецензирование статьи эксперту Microsoft Дугу Бантингу (Doug Bunting).

Remove console and debug loggers in ASP.NET Core 2.0 when in production mode

In ASP.NET Core 2.0 we have this

That CreateDefaultBuilder(args) has many helpful defaults. However it contains this:

So the console and debug logging providers are always registered.

I used to register them like this

How do I remove/unregister them when running in production mode? I don’t mean changing the logging level, I mean I don’t want them registered at all in production mode.

3 Answers 3

I would say the designed way to do this would be by changing the logging configuration not to log anything to those providers. But I understand that you want to remove any calls for production; and you can still do this properly in code.

You can simply access the hosting environment from the HostBuilderContext that gets passed to the ConfigureLogging lambda:

Obviously, this alone does not help to undo what the CreateDefaultBuilder call already set up. First, you would need to unregister those providers. For that, you can use the new ILoggingBuilder.ClearProviders method:

This was introduced in response to this logging issue on GitHub.

Логирование проекта с помощью NLog Framework

Введение

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

Всем нужна статистика и слежение за проектами. Итак, что же насчет логирования, так это процесс записи всех сведений о проекте, а именно: информации о работе тех или иных элементах приложения, предупреждение о критической нагрузке, всяческие ошибки и т.д. Для . NET приложений был разработан очень удобный фреймворк под название NLog , с его помощью можно вести учет о состоянии всего приложения. Есть поддержка записи в файл, в базу данных.

Настройка данной платформы очень удобна и легка, есть два способа:

    через конфигурационный файл;

через конфигурационный объект LoggingConfiguration ;

Первый способ самый простой, так как зондирование проекта уже встроено в саму библиотеку NLog. Вся работа основа на объекте Logger – парне, который занимается ведением учета состояния нашего проекта.

Для того чтобы продемонстрировать работу NLog, создадим проект по шаблону консольного приложения и назовем его NLogUnderstanding . Изначально наш проект выглядит следующим образом:

static void Main( string [] args)

Чтобы начать работу данного фреймворка в нашем проекте, нужно установить следующие библиотеки через Package Manager Console (или же через сам менеджер расширений):

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

  1. Настройка через конфигурационный файл:

Первое, что нужно сделать- это установить данный пакет:


После это у нас в проекте появится указанный файлик NLog . config :

Начальное содержимое файла выглядеть будет примерно так:

Все, после того как мы подготовили данную библиотеку, начинаем настройку объекта Logger. Первое, что мы должны сделать, это указать ему, куда мы будем писать те или иные сообщения. Все эти файлы указываются в разделе targets > . Первое, что мы добавим, так это все возможные записи, которые мы сможем проводить:

Шесть возможных вариантов ведения учета:

  • Информация о состоянии элементов;
  • Информация, запущенная в режиме debug для отладки проекта (можно применять в тестах);
  • Всяческие предупреждения (например, связанные с нагрузкой);
  • Информация об исключениях;
  • Информация об ошибках, которые привели к критическому завершению приложения.

Для каждого сообщения присутствует свой метод все в том же объекте Logger , который мы чуть позже будем разбирать. Основные атрибуты, которые нужно заполнить, это:

  • name – название файла, нам оно понадобиться для организации правил, по которым мы будем писать именно в этот файл;
  • fileName – указываем файл и путь к файлу, в который будем писать наши логи;
  • layout – шаблон, по которому будет заполнятся наш файл.

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

  • $ – вернет базовую директорию вашего приложения. При компиляции этот маркер вернет изначальный путь (папку bin);
  • $ < shortdate >/ $ < longdate >– маркеры подстановки устанавливают текущую дату и время в зависимости от маркера (полную дату и время или же только дату);
  • $< uppercase :$< level >> – интересное использование вложения маркеров. Как Вы поняли, маркер $ < level >будет указываться уровень сообщения (мы их перечислили ранее), приводим в верхний регистр;
  • $ < message >– под данный маркер подставляется сообщение, указанное в аргументных скобках методов (об этом далее);
  • $ < logger >– название класса, от которого поступило сообщение.

После настройки целей для записи наших сообщений мы приступаем к организации правил, по которым будем заполнять наши файлы:

Тут все намного проще, единственное, что нужно заполнить — это основные атрибуты, т.к. minlevel (минимальный уровень заполнения файла, имя которого указанного в атрибуте writeTo ). После того как настроили конфигурационный файл, приступаем к работе с проектом и нашим Logger . Первое, что нужно — это создать экземпляр Logger . Это можно сделать двумя способами:

  • Создать через первый фабричный метод LogManager . GetLogger ( » Example » ) , в аргументах указываем название логгера, менее эффективный способ, т.к. всегда нужно указывать название класса, в котором происходит запись в журнал;
  • Создание через второй фабричный метод LogManager . GetCurrentClassLogger () , пользуясь данным методом, мы предоставляем возможность экземпляру логгера самому узнать полное квалификационное название класса, в котором произошла запись в журнал.

Теперь привнесем изменения в наш созданный проект:

После компиляции проекта у нас создается файл с текущей датой и в него внесутся следующий записи:

Теперь можно приступать к внедрению NLog в Ваш проект, и отслеживать состояние ваших объектов.

Логгирование

Ведение лога и ILogger

ASP.NET Core имеет встроенную поддержку логгирования, что позволяет применять логгирование с минимальными вкраплениями кода в функционал приложения.

Для логгирования данных нам необходим объект ILogger . По умолчанию среда ASP NET Core через механизм внедрения зависимостей уже предоставляет нам такой объект. Например, возьмем стандартный проект по типу Empty и добавим механизм логгирования. Для этого перейдем к классу Startup и изменим его метод Configure() :

Средой выполнения в метод Configure передается объект ILogger, который представляет логгер. А метод логгера logger.LogInformation передает на консоль некоторую информацию. По умолчанию информация логгируется на консоль, поэтому для тестирования логгера нам надо запустить приложение как консольное:

При обращении к приложению с помощью следующего запроса http://localhost:xxxxx/index на консоль будет выведена информация, переданная логгером:

Важно, что, если мы используем класс Startup для логгирования, то получить объект логгера мы можем только в методе Configure() , но никак не в методе ConfigureServices() или конструкторе класса Startup, поскольку инфраструктура логгирования зависит от конфигурации и контейнера DI, который окончательно устанавливаются лишь после завершения работы метода ConfigureServices.

Категория логгера

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

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

Debug : для вывода информации, которая может быть полезной в процессе разработки и отладки приложения

Information : уровень сообщений, позволяющий просто отследить поток выполнения приложения

Warning : используется для вывода сообщений о неожиданных событиях, например, ошибках, которые не влияют не останавливают выполнение приложения, но в то же время должны быть иследованы

Error : информация об ошибках, вследствие которых приложение должно быть остановлено

Critical : уровень критических ошибок, которые могут быть связаны с какими-то ситуациями извне — ошибками операционной системы, потерей данных в бд, переполнение памяти диска и т.д.

None : вывод информации в лог не применяется

Для вывода соответствующего уровня информации у объекта ILogger определены соответствующие методы расширения:

Так, в примере выше для вывода информации на консоль использовался метод LogInformation() .

Вывод сообщений уровня Trace по умолчанию отключен.

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

string data : строковое сообщение для лога

int eventId : числовой идентификатор, который связан с логом. Идентификатор должен быть статическим и специфическим для определенной части логгируемых событий.

string format : строковое сообщения для лога, которое моет содержать параметры

object[] args : набор параметров для строкового сообщения

Exception error : логгируемый объект исключения

Также для логгирования определен общий метод Log() , который позволяет определить уровень логгера через один из параметров:

При стандартном логгировании на консоль для каждого уровня/метода определен своя метка и цветовой маркер, которые позволяют сразу выделить сообщение соответствующего уровня. Например, при запуске следующего кода:

Путь ASP.NET Core [уровень 1] Основы


ASP.NET Core — новейший фреймворк для кроссплатформенной веб разработки. Пока его популярность (как и количество вакансий) только начинает набирать обороты самое время узнать о нем побольше. Ну а для того, чтобы все знания не испарились сразу после прочтения — добавим существенную практическую часть. Создадим простое приложение, для тестирования прочитанного.

Если вы считаете, что уже достаточно круты в новом фреймворке — можете попробовать пройти тест до того, как прочтете статью. Линк. Весь код проекта можно посмотреть на гитхабе.

Первая часть включает:

  • Что такое .NET Core и ASP.NET Core?
  • Основы создания приложения и его структура
  • Добавление новых элементов, скаффолдинг
  • Основы встроенного Dependency Injection
  • Деплоймент в Azure

Разберемся в терминах. Один из наиболее не понятных моментов — это зависимость между старым фреймворком ASP.NET MVC и новым ASP.NET Core, а также в чем отличия .NET и .NET Core. Начнем с последнего. .NET Core — это общая платформа для разработки программного обеспечения. Фактически это еще одна реализация стандарта .NET (другие реализации — .NET, Mono). Отличия и особенности этой реализации (.NET Core) в том, что она:

  • С открытым исходным кодом
  • Кроссплатформенная
  • Гибкая в установке — может быть внутри приложения и можно поставить несколько версий на одной и той же машине
  • Все сценарии работы поддерживаются с помощью консольных инструментов

Перейдем к ASP.NET Core. Это новый фреймворк от Microsoft для разработки Веб приложений, который появился вследствие редизайна ранее существующего фреймворка ASP.NET MVC. Нужно понимать, что ASP.NET Core не обязательно должен базироваться на .NET Core. Можно создать ASP.NET Core приложение на основе старого доброго .NET. Такая опция есть в стандартном диалоге создания нового проекта:

В чем же тогда особенности и отличия ASP.NET Core от предыдущего ASP.NET? Некоторые из них это:

  • Возможность намного лучше контролировать нужные модули, сборки. Например, нет жесткой привязки к IIS, System.Web.dll
  • Встроенный функционал для внедрения зависимостей
  • Открытый исходный код

Кроме того, Core приложение теперь унифицировано с другими типами приложений. Теперь, оно таким же образом включает метод Main, который вызывается при запуске приложения, а тот в свою очередь просто запускает Веб часть. Минимальное приложение выглядит примерно таким образом:

Класс Statup можно, в какой-то степени, охарактеризовать как новый вариант Global.asax (Это класс для глобальной настройки всего приложения в предыдущей версии ASP.NET). Грубо говоря, можно сказать, что метод ConfigureServices нужен для конфигурации контейнера для внедрения зависимостей и его сервисов, а метод Configure для конфигурации конвейера обработки запросов.

Приступим к практической реализации

Для того, чтобы лучше понять все новшества, создадим ASP.NET Core приложение на основе .NET Core.

Чтобы облегчить себе жизнь, выберем Web Application и поменяем аутентификацию на Individual User Accounts. Таким образом Visual Studio уже сгенерирует весь нужный код для базового приложения.

Рассмотрим детальней что же нового появилось в ASP.NET Core. С точки зрения разработки вся концепция осталась прежней. Структура проекта базируется на паттерне MVC. Для работы с данными по умолчанию используем Entity Framework, логика описана в классах-контроллерах, на уровне представлений используем синтаксис cshtml + новая фишка tag helpers.

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

Дополним модель базы данных сущностями для создания и прохождения тестов. Будем использовать следующие сущности: Набор тестовых вопросов — TestPackage, Сам вопрос (тест) — TestItem, Результат теста — TestResult. Пример можно посмотреть тут. Радует, что EntityFramework Core уже поддерживает большинство функционала и можно полноценно пользоваться Code First миграциями.

Добавляем логику

Теперь, когда у нас есть модель базы данных, мы можем приступить к созданию логики для нашего приложения. Самый простой способ создания админки — это механизм scaffolding. Для этого, кликаем правой кнопкой мыши по папке контроллеров и выбираем Add → New Scaffold Item:

Выбираем «MVC Controller с представлениями, с использованием Entity Framework». Этот шаблон позволяет нам быстро создать контроллер и вьюхи для управления одной конкретной моделью. Проделаем такой трюк для TestPackage и TestItem. В результате у нас есть готовый прототип админки для нашей системы. Можно запустить проект и зайти на страницы этих контроллеров, просто добавить его имя без слова Controller в конец адреса, например, /testpackages. Конечно в ней еще не все идеально, поэтому нужно допилить некоторые моменты и сделать их более удобными.

После создания модели и простого функционала для работы с ней, можно перейти к самому процессу прохождения тестов. В общем, все выглядит просто. Сервер присылает вопрос, варианты, мы отсылаем назад ответ. Сервер сохраняет его. В конце теста показываем результат. Добавим код для этого поведения.

В общем, все что нужно для теста у нас есть.

Основы Dependency Injection в ASP.NET Core

Важным новшеством новой версии ASP.NET так же является встроенный механизм внедрения зависимостей. В 2020 году уже никого не удивишь тем, что механизм внедрения зависимостей можно перенести внутрь фреймворка. Мало какое серьёзное приложение пишут без использование этого подхода. DI в ASP.NET Core реализован достаточно базово, но в то же время позволяет решить большинство задач управления зависимостями.

Конфигурация контейнера осуществляется в методе ConfigureServices класса Startup. Пример:

Можно заметить, что для контекста базы данных и Identity фреймворка есть дополнительные, не типичные методы их регистрации. Это позволяет более гибко их сконфигурировать. В этот подход регистрации сервисов очень красиво вписываются extension-методы.

После регистрации сервисов, самый легкий способ получить из в коде (например, в контроллерах) — это просто добавить параметр конструктора типа, который был зарегистрирован. Такие контроллеры по умолчанию создаются скаффолдингом.

Деплой

Одним из самых простых способов деплоймента остается Microsoft Azure. Нам достаточно самых базовых настроек для полноценной работы. Развертывание сайта на сервере все так же просто — с помощью нескольких кликов, начиная с контекстного меню на файле проекта.

Выводы

Пока не известно будущее «классического» .NET фреймворка, так как он, все же, является более стабильным и проверенным, поэтому может существовать еще довольно долго (привет, Python 2), хотя возможна и ситуация быстрой миграции большинства девелоперов на Core версии (не такой уже .NET и старый — 14 лет всего лишь).

Корректный ASP.NET Core

Специально для любителей книг из серии «С++ за 24 часа» решил написать статью про ASP.NET Core.

Если вы раньше не разрабатывали под .NET или под какую-то аналогичную платформу, то смысла заходить под кат для вас нет. А вот если вам интересно узнать что такое IoC, DI, DIP, Interseptors, Middleware, Filters (то есть все то, чем отличается Core от классического .NET), то вам определенно есть смысл нажать на «Читать дальше», так как заниматься разработкой без понимания всего этого явно не корректно.

IoC, DI, DIP

Если театр начинается с вешалки, то ASP.NET Core начинается с Dependency Injection. Для того, чтобы разобраться с DI нужно понять, что такое IoC.

Говоря о IoC очень часто вспоминают голливудский принцип «Don’t call us, we’ll call you». Что означает «Не нужно звонить нам мы позвоним вам сами».

Различные источники приводят различные паттерны, к которым может быть применен IoC. И скорее всего они все правы и просто дополняют друг друга. Вот некоторые их этих паттернов: factory, service locator, template method, observer, strategy.

Давайте разберем IoC на примере простого консольного приложения.

Допустим у нас есть два простых класса, реализующих интерфейс с одним методом:

Они оба зависят от абстракции (в данном случае в виде абстракции выступает интерфейс).

И, допустим, у нас есть объект более высокого уровня, использующий эти классы:

В зависимости от параметра конструктора переменная _instance инициализируется определенным классом. Ну и далее при вызове Write будет совершен вывод на консоль или в Debug. Все вроде бы неплохо и даже, казалось бы, соответствует первой части принципа Dependency Inversion

Объекты более высокого уровня не зависят от объектов более низкого уровня. И те, и те зависят от абстракций.

В качестве абстракции в нашем случае выступает ILayer.

Но у нас должен быть еще и объект еще более высокого уровня. Тот, который использует класс Logging


Инициализируя Logging с помощью 1 мы получаем в классе Logging экземпляр класса, выводящего данные на консоль. Если мы инициализируем Logging любым другим числом, то log.Write будет выводить данные в Debug. Все, казалось бы, работает, но работает плохо. Наш объект более высокого уровня Main зависит от деталей кода объекта более низкого уровня – класса Logging. Если мы в этом классе что-то изменим, то нам необходимо будет изменять и код класса Main. Чтобы это не происходило мы сделаем инверсию контроля – Inversion of Control. Сделаем так чтобы класс Main контролировал то, что происходит в классе Logging. Класс Logging будет получать в виде параметра конструктора экземпляр класса, реализующего интерфейс интерфейс ILayer

И теперь нас класс Main будет выглядеть таким образом:

Фактически мы декорируем наш объект Logging с помощью необходимого для нас объекта.

Теперь наше приложение соответствует и второй части принципа Dependency Inversion:

Абстракции не зависят от деталей. Детали зависят от абстракций. Т.е. мы не знаем деталей того, что происходит в классе Logging, мы просто передаем туда класс, реализующий необходимую абстракцию.

Есть такой термин tight coupling – тесная связь. Чем слабее связи между компонентами в приложении, тем лучше. Хотелось бы заметить, что данный пример простого приложения немного не дотягивает до идеала. Почему? Да потому что в классе самого высокого уровня в Main у нас дважды используется создание экземпляров класса с помощью new. А есть такая мнемоническая фраза «New is a clue» — что означает чем меньше вы используется new, тем меньше тесных связей компонентов в приложении и тем лучше. В идеале мы не должны были использовать new DebugLayer, а должны были получить DebugLayer каким-нибудь другим способом. Каким? Например, из IoC контейнера или с помощью рефлексии из параметра передаваемого Main.

Теперь мы разобрались с тем, что такое Inversion of Control (IoC) и что такое принцип Dependency Inversion (DIP). Осталось разобраться с тем, что такое Dependency Injection (DI). IoC представляет собой парадигму дизайна. Dependency Injection это паттерн. Это то, что у нас теперь происходит в конструкторе класса Logging. Мы получаем экземпляр определенной зависимости (dependency). Класс Logging зависит от экземпляра класса, реализующего ILayer. И это экземпляр внедряется (injected) через конструктор.

IoC container

IoC контейнер это такой объект, который содержит в себе множество каких-то определенных зависимостей (dependency). Зависимость можно иначе назвать сервисом – как правило это класс с определенным функционалом. При необходимости из контейнера можно получить зависимость необходимого типа. Внедрение dependency в контейнер — это Inject. Извлечение – Resolve. Приведу пример самого простого самостоятельно написанного IoC контейнера:

Всего дюжина строчек кода, а уже можно использовать (не в продакшн, разумеется, а в учебных целях).

Зарегистрировать зависимость (допустим, ConsoleLayer или DebugLayer которые мы использовали в прошлом примере) можно так:

А извлечь из контейнера в необходимом месте программы так:

В реальных контейнерах еще реализуется и Dispose(), позволяющий уничтожать ставшие ненужными ресурсы.

Кстати, имя IoC контейнер не совсем точно передает смысл, так как термин IoC гораздо шире по применению. Поэтому в последнее время все чаще применяется термин DI контейнер (так как все-таки применяется dependency injection).

Service lifetimes + various extension methods in Composition Root

Приложения ASP.NET Core содержат файл Startup.cs который является отправной точкой приложения, позволяющей настроить DI. Настраивается DI в методе ConfigureServices.

Этот код добавит в DI контейнер класс SomeRepository, реализующий интерфейс ISomeRepository. То, что сервис добавлен в контейнер с помощью AddScoped означает, что экземпляр класса будет создаваться при каждом запросе страницы.
Добавить сервис в контейнер можно и без указания интерфейса.

Но такой способ не рекомендуется, так как у вашего приложения теряется гибкость и появляются тесные связи. Рекомендуется всегда указывать интерфейс, так как в таком случае в любой момент времени можно заменить одну реализацию интерфейса другой. И если реализации поддерживают принцип Liskov substitution, то «легким движением руки» сменив название класса реализации вы измените и функционал всего приложения.

Есть еще 2 варианта добавить сервис – AddSingleton и AddTransient.
При использовании AddSingleton сервис создается один раз и при использовании приложения обращение идет к одному и тому же экземпляру. Использовать этот способ нужно особенно осторожно, так как возможны утечки памяти и проблемы с многопоточностью.

У AddSingleton есть небольшая особенность. Он может быть инициализирован либо при первом обращении к нему

либо сразу же при добавлении в конструктор

Вторым способом можно даже добавить параметр в конструктор.
Если хочется добавить параметр в конструктор сервиса, добавленного не только с помощью AddSingleton, но и с помощью AddTransient/AddScoped, то можно использовать лямбда выражение:

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

Если с AddSingleton и AddScoped все должно быть более-менее понятно, то AddTransient требует разъяснений. Официальная документация приводит пример, в котором определенный сервис добавлен в DI контейнер и в качестве параметра конструктора другого сервиса и отдельно самостоятельно. И вот в случае, если он добавлен отдельно с помощью AddTransient, он создает свой экземпляр 2 раза. Приведу очень-очень упрощенный пример. В реальной жизни к применению не рекомендуется, т.к. классы для упрощения не наследуют интерфейсы. Допустим у нас есть простой класс:

И есть второй класс, который содержит первый как зависимый сервис и получает эту зависимость в качестве параметра конструктора:

Теперь совершаем inject двух сервисов:

И в каком-нибудь контроллере в Action добавим получение наших зависимостей и вывод значений в окно Debug.

Так вот в результате мы получим 2 разных значения Guid. А вот если мы заменим AddTransient на AddScoped, то в результате мы получим 2 одинаковых значения.

В IoC контейнере приложений ASP.NET Core по умолчанию содержатся уже некоторые сервисы. Например, IConfiguration – сервис с помощью которого можно получить настройки приложения из файлов appsettings.json и appsettings.Development.json. IHostingEnvironment и ILoggerFactory с помощью которых можно получить текущую конфигурацию и вспомогательный класс, позволяющий проводить логирование.

Извлекают классы из контейнера с помощью следующей типичной конструкции (самый банальный пример):

В области видимости контроллера создается переменная с модификаторами доступа private readonly. Зависимость получается из контейнера в конструкторе класса и присваивается приватной переменной. Далее эту переменную можно использовать в любых методах или Action контроллера.
Иногда не хочется создавать переменную для того, чтобы использовать ее только в одном Action. Тогда можно использовать атрибут [FromServices]. Пример:

Выглядит странно, но для того, чтобы в коде не вызывать метод статического класса DateTime.Now() иногда делают так, что значение времени получается из сервиса в качестве параметра. Таким образом появляется возможность передать любое время в качестве параметра, а значит становится легче писать тесты и, как правило, становится проще вносить изменения в приложение.
Нельзя сказать, что static – это зло. Статические методы выполняются быстрее. И скорее всего static может использоваться где-то в самом IoC контейнере. Но если мы избавим наше приложение от всего статического и new, то получим большую гибкость.

Сторонние DI контейнеры

То, что мы рассматривали и то, что фактически реализует ASP.NET Core DI контейнер по умолчанию, — constructor injection. Имеется еще возможность внедрить зависимость в property с помощью так называемого property injection, но эта возможность отсутствует у встроенного в ASP.NET Core контейнера. Например, у нас может быть какой-то класс, который вы внедряем как зависимость, и у этого класса есть какое-то public property. Теперь представьте себе, что во время или после того как мы внедряем зависимость, нам нужно задать значение property. Вернемся к примеру похожему на пример, который мы недавно рассматривали.
Если у нас есть такой вот класс:

который мы можем внедрить как зависимость,

то используя стандартный контейнер задать значение для свойства мы не можем.
Если вы захотите использовать такую возможность задать значение для свойства OperationId, то вы можете использовать какой-то сторонний DI контейнер, поддерживающий property injection. К слову сказать property injection не особо рекомендуется использовать. Однако, существуют еще Method Injection и Setter Method Injection, которые вполне могут вам пригодится и которые также не поддерживаются стандартным контейнером.

У сторонних контейнеров могут быть и другие очень полезные возможности. Например, с помощью стороннего контейнера можно внедрять зависимость только в контролеры, у которых в названии присутствует определенное слово. И довольно часто используемый кейс – DI контейнеры, оптимизированные на быстродействие.
Вот список некоторых сторонних DI контейнеров, поддерживаемых ASP.NET Core: Autofac, Castle Windsor, LightInject, DryIoC, StructureMap, Unity

Хоть при использовании стандартного DI контейнера и нельзя использовать property/method injection, но зато можно внедрить зависимый сервис в качестве параметра конструктора реализовав паттерн «Фабрика» следующим образом:

В данном случае GetService вернет null если зависимый сервис не найден. Есть вариация GetRequiredService, которая выбросит исключение в случае, если зависимый сервис не найден.
Процесс получения зависимого сервиса с помощью GetService фактически применяет паттерн Service locator.

Autofac

Разберем Autofac на практическом примере. Удобно то, что сервисы из контейнера можно будет регистрировать и получать, как и дефолтным способом, так и с помощью Autofac.

Установим NuGet пакет Autofac.Extensions.DependencyInjection.
Изменим возвращаемое методом ConfigureServices значение с void на IServiceProvider. И добавим property

После этого станет возможным добавить в конец метода ConfigureServices класса Startup код вроде следующего (это лишь один из вариантов регистрации сервисов):

Здесь builder.Populate(services); добавляет в контейнер сервисы из IServiceCollection. Ну и далее уже можно регистрировать сервисы с помощью builder.RegisterType. Ах, да. Чуть не забыл. Необходимо изменить с void на IServiceProvider возвращаемое значение метода ConfigureServices.

AOP с помощью ASP.NET Core — Autofac Interseptors

Говоря про аспектно-ориентированное программирование, упоминают другой термин – cross-cutting concerns. Concern – это какая-то часть информации, которая влияет на код. В русском варианте употребляют слово ответственность. Ну а cross-cutting concerns это ответственности, которые влияют на другие ответственности. А в идеале ведь они не должны влиять друг на друга, так ведь? Когда они влияют на друг друга, то становится сложнее изменять программу. Удобнее, когда у нас все операции происходят по отдельности. Логирование, транзакции, кеширование и многое другое можно совершать с помощью AOP не изменяя код самих классов и методов.

В мире .NET часто применяется способ, когда AOP код внедряется с помощью пост-процессора в уже откомпилированный код приложения (PostSharp) Или же альтернативно можно применять интерцепторы – это такие перехватчики событий, которые можно добавлять в код приложения. Эти перехватчики, как правило, используют для своей работы уже рассмотренный нами паттерн декоратор.


Давайте создадим свой интерцептор. Самый простой и типичный пример, который проще всего воспроизвести — это логирование.
Установим дополнительно к пакету Autofac.Extensions.DependencyInjection еще и пакет Autofac.Extras.DynamicProxy
Установили? Добавим простенький класс лога, который будет вызываться при обращении к определенным сервисам.

Добавляем в нашу регистрацию Autofac регистрацию интерцептора:

И теперь при каждом обращении к классу будет вызван метод Intercept класса Logger.
Таким образом мы можем упростить себе жизнь и не писать в начале каждого метода запись в лог. Она у нас будет вестись автоматически. И при желании нам будет несложно ее изменить или отключить для всего приложения.

Также мы можем убрать .InterceptedBy(typeof(Logger)); и добавить перехват вызовов только для конкретных сервисов приложения с помощью атрибута [Intercept(typeof(Logger))] – необходимо указать его перед заголовком класса.

Middleware

В ASP.NET существует определенная цепочка вызовов кода, которая происходит при каждом request. Еще до того, как загрузился UI/MVC выполняются определенные действия.

То есть, например, если мы добавим в начало метода Configure класса Startup.cs код

то мы сможем посмотреть в консоли дебага какие файлы запрашивает наше приложение. Фактически мы получаем возможности AOP “out of box”
Немного useless, но понятный и познавательный пример использования middleware я вам сейчас покажу:

При каждом запросе начинает выполнятся цепочка вызовов. Из каждого app.Use после вызова next.invoke() совершается переход ко следующему вызову. И все завершается после того как отработает app.Run.
Можно выполнять какой-то код только при обращении к определенному route.
Сделать это можно с помощью app.Map:

Теперь если просто перейти на страницу сайта, то можно будет увидеть текст “Hello!”, а если добавить к строке адреса /Goodbye, то вам будет отображено Goodbye.

Кроме Use и Map можно использовать UseWhen или MapWhen для того, чтобы добавлять код в цепочку middleware только при каких-то определенных условиях.

До сих пор были все еще useless примеры, правда? Вот вам нормальный пример:

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

Или же вот пример локализации:

Теперь если вы к адресу страницы добавите параметр ?culture=fr то вы сможете переключить язык приложения на французский (если в ваше приложение добавлена локализация, то все сработает)

Filters

Если цепочка middleware относятся к процессам до MVC, то фильтры работают вместе с MVC.
Следующее схематическое изображение показывает, как отрабатывают фильтры

Сначала отрабатывают фильтры авторизации. Т.е. можно создать какой-то фильтр или несколько фильтров и вписать в них какой-то свой код авторизации, который будет отрабатывать при запросах.

Затем отрабатывают фильтры ресурсов. С помощью этих фильтров можно, например, вернуть какую-то информацию из кеша.

Затем происходит привязка данных и выполняются Action фильтры. С их помощью можно манипулировать параметрами передаваемыми Action и возвращаемым результатом.

Exception фильтры как намекает название позволяют добавить какую-то общую обработку ошибок для приложения. Должно быть довольно удобно обрабатывать ошибки везде одинаково. Эдакий AOP-шный плюс.

Result фильтры позволяют совершить какие-то действия до выполнения Action контроллера или после. Они довольно похожи на Action фильтры, но выполняются только в случае отсутствия ошибок. Подходят для логики завязанной на View.

Все должно быть более понятно на примере. А у меня под рукой как раз есть пример упрощенного авторизационного фильтра:

Добавляете этот класс в DI контейнер (как обычно в Startup.cs)

И теперь становится возможным добавить какую-то свою авторизацию любому Action добавив следующий атрибут

Забавная штука – можно создать свое middleware и добавлять его каким-то action в качестве фильтра. Для того чтобы сделать так нужно создать класс с произвольным названием и методом Configure

Теперь этот класс можно добавлять Action-ам с помощью следующего атрибута

Введение в ASP.NET Core¶

В данной теме представлены новые концепции в ASP.NET Core, и здесь рассказывается, как разрабатывать современные веб приложения.

Что такое ASP.NET Core?¶

ASP.NET Core — это кроссплатформенный фреймворк с открытым исходным кодом для создания современных облачных веб приложений. Приложения ASP.NET Core могут быть запущены под`.NET Core `__ или под полной версией .NET Framework. Фреймворк состоит из модульных компонентов, что дает вам гибкость при создании решений. Вы можете разрабатывать и запускать ASP.NET Core приложения под Windows, Mac и Linux. ASP.NET Core имеет открытый исходный код на GitHub.

Почему ASP.NET Core?¶

Первый предварительный релиз ASP.NET появился почти 15 лет назад как часть .NET Framework. С тех пор миллионы разработчиков использовали его для создания и запуска веб приложений, и теперь он обладает гораздо большими возможностями.

В ASP.NET Core произошло большое число архитектурных изменений, в результате чего фреймворк стал компактным и модульным. ASP.NET Core не основывается на System.Web.dll. Он основывается на наборе пакетов NuGet. Это позволяет вам оптимизировать приложение, чтобы оно включало только те пакеты NuGet, которые вам нужны.

С ASP.NET Core вы получаете следующие фундаментальные улучшения:

  • Единую историю для сборки веб UI и веб API
  • Интеграцию современных клиентских фреймворков и рабочих процессов разработки
  • Облачную конфигурационную систему
  • Встроенное внедрение зависимостей
  • Новый легкий модульный поток HTTP запросов
  • Возможность хостинга на IIS или хостинга в самом процессе
  • Встроенный `.NET Core`_
  • Конструкцию в виде пакетов `NuGet`_
  • Новый инструментарий, который упрощает разработку
  • Возможность кроссплатформенного запуска ASP.NET приложений под Windows, Mac и Linux
  • Открытый исходный код

Анатомия приложения¶

Приложение ASP.NET Core — это просто консольное приложение, которое создает веб сервер в своем методе Main :

Microsoft.AspNetCore.Hosting.WebHostBuilder` , который следует паттерну сборки для создания хоста веб приложения. У паттерна есть методы, которые определяют веб сервер (например, UseKestrel ) и класс для запуска ( UseStartup ). В примере выше используется веб сервер Kestrel, но мы можем указать и другие серверы. В следующем разделе мы подробнее рассмотрим UseStartup . WebHostBuilder предлагает много дополнительных методов, включая UseIISIntegration для хостинга на IIS и IIS Express и UseContentRoot для указания корневой директории контента. Методы Build и Run создают IWebHost , который будет хостить приложение, и оно начнет слушать входящие HTTP запросы.

Startup¶

Метод UseStartup для WebHostBuilder указывает класс Startup для вашего приложения.

В классе Startup вы определяете поток обработки запросов, а также здесь настраиваются все сервисы, необходимые вашему приложению. Класс Startup должен быть открытым и содержать следующие методы:

  • ConfigureServices определяет, используемые вашим приложением (например, ASP.NET MVC Core, Entity Framework Core, > Configure определяет связующее ПО в потоке запросов
  • См. Запуск приложения


Сервисы¶

Сервис — это компонент для общего пользования в приложении. Сервисы доступны благодаря внедрению зависимостей. ASP.NET Core включает в себя встроенный IoC контейнер, который по умолчанию поддерживает внедрение конструктора, но вы можете легко заменить его IoC контейнером по вашему выбору. В дополнение к преимуществу слабого связывания, DI делает так, что сервисы доступны всему приложению. Например, везде доступно логирование . См. Внедрение зависимостей (Dependency Injection) .

Связующее ПО¶

В ASP.NET Core сы составляете поток запросов, используя Связующее ПО (Middleware) . Связующее ПО ASP.NET Core выполняет асинхронную логику для HttpContext , а затем либо вызывает следующее связующее ПО в цепочки, либо напрямую обрывает запрос. Обычно для связующего ПО используется “Use”, принимая зависимость для пакета NuGet и вызывая соответствующий метод расширения UseXYZ для IApplicationBuilder в методе Configure .

ASP.NET Core предлагает богатый набор связующего ПО:

С ASP.NET Core можно использовать любое связующее ПО, основанное на OWIN. См. Open Web Interface for .NET (OWIN) .

Серверы¶

Хостинговая модель ASP.NET Core напрямую не слушает запросы — она полагается на серверную реализацию HTTP, чтобы передавать запросы приложению. Переданный запрос представляется как набор интерфейсов feature, которые приложение затем компонует в HttpContext . ASP.NET Core включает в себя кроссплатформенный веб сервер, Kestrel , который обычно запускается за производственным веб сервером, таким как IIS или nginx.

Корневая директория контента¶

Корневая директория — это основной путь к любому контенту, используемому в приложении, например, его представления и веб контент. По умолчанию корневая директория контента — это то же самое, что и основной путь приложения для выполняемого хостинга; альтернативу можно указать с помощью WebHostBuilder .

Корневая директория веб¶

Корневая директория веб (web root) — это директория для открытых статических ресурсов, таких как файлов css, js и файлов изображений. Связующее ПО статических файлов по умолчанию отрабатывает файлы только из этой директории (и поддиректорий). Путем директории является /wwwroot , но вы можете указать и другой путь с помощью WebHostBuilder .

Главное в .NET — Протоколирование в .NET Core

В номере за февраль я углубился в новый API конфигурации, включенный в новую платформу .NET Core 1.0 (bit.ly/1OoqmkJ). (Полагаю, что большинство читателей слышало о недавно переименованной .NET Core 1.0, которая ранее обозначалась как .NET Core 5 и являлась частью платформы ASP.NET 5 [bit.ly/1Ooq7WI].) В той статье я использовал модульное тестирование, чтобы исследовать Microsoft.Extensions.Configuration API. В этой статье я предприму похожий подход, но в отношении Microsoft.Extensions.Logging. Ключевое отличие в моем подходе в том, что сейчас я буду тестировать API из .NET 4.6 CSPROJ-файла, а не из проекта ASP.NET Core. Это подчеркивает тот факт, что .NET Core уже доступна вам для немедленного применения, даже если вы не перешли на проекты ASP.NET Core.

Протоколирование? С какой стати нам понадобилась новая инфраструктура протоколирования? У нас уже есть NLog, Log4Net, Loggr, Serilog и встроенная Microsoft.Diagnostics.Trace/Debug/TraceSource, не говоря уже о других. Оказывается, тот факт, что на рынке так много инфраструктур протоколирования, и послужил одним из стимулов к созданию Microsoft.Extensions.Logging. Как разработчик, столкнувшийся с широчайшим выбором, вы скорее всего выберете что-то одно, понимая, что позднее вам, возможно, придется переключиться на другое. Поэтому у вас может возникнуть соблазн написать собственную оболочку API протоколирования, которая обращается к любой конкретной инфраструктуре протоколирования, выбранной вами или вашей компанией на этой неделе. Аналогично вы можете использовать некую конкретную инфраструктуру протоколирования в своем приложении и обнаружить только то, что одна из библиотек, применяемых вами, использует другую библиотеку, вынуждая вас писать слушатель, принимающий сообщения от одной библиотеки к другой.

Microsoft предоставляет с Microsoft.Extensions.Logging оболочку, поэтому никому больше не понадобится писать собственную. Эта оболочка содержит один набор API, которые затем пересылают вызовы выбранному вами провайдеру. Хотя Microsoft включает провайдеры для консоли (Microsoft.Extensions.Logging.Console), отладки (Microsoft.Extensions.Logging.Debug), журнала событий (Microsoft.Extensions.Logging.EventLog) и TraceSource (Microsoft.Estensions.Logging.TraceSource), она также сотрудничает с различными группами, создающими другие инфраструктуры протоколирования (включая сторонние вроде NLog, Serilog, Loggr, Log4Net и др.), так что в Microsoft.Extensions.Logging есть совместимые провайдеры и от них.

Приступаем к работе

Операции протоколирования начинаются с фабрики журналов (log factory), как показано на рис. 1.

Рис. 1. Как использовать Microsoft.Extensions.Logging

Как демонстрирует этот код, вы начинаете с создания экземпляра Microsoft.Extensions.Logging.LoggerFactory, который реализует ILoggerFactory в том же пространстве имен. Затем вы указываете, какие провайдеры вы хотите задействовать, вызывая метод расширения ILoggerFactory. На рис. 1 я намеренно использую Microsoft.Extensions.Logging.ConsoleLoggerExtensions.AddConsole и Microsoft.Extensions.Logging.DebugLoggerFactoryExtensions.AddDebug. (Хотя оба эти класса находятся в пространстве имен Microsoft.Extensions.Logging, они на самом деле содержатся в NuGet-пакетах Microsoft.Extensions.Logging.Console и Microsoft.Extensions.Logging.Debug соответственно.)

Методы расширения являются просто удобными сокращениями для более универсального способа добавления провайдера: ILoggerFactory.AddProvider(ILoggerProvider provider). Сокращение в том, что метод AddProvider принимает экземпляр провайдера журнала (возможно, того, конструктор которого требует передачи выражения фильтра уровня ведения журнала), а методы расширения предоставляют умолчания для таких выражений. Например, сигнатура конструктора для ConsoleLoggerProvider выглядит так:

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

Например, вы могли бы вызвать AddProvider с конкретным экземпляром ConsoleLoggerProvider, который был сконструирован из фильтра, пропускающего все сообщения с более высоким значением (более значимые), чем LogLevel.Information:

(Любопытно, что в отличие от методов расширения, возвращающих ILoggerFactory, AddProvider возвращает void, что предотвращает использование текучего синтаксиса типов, показанного на рис. 1.)

Важно понимать, что, к сожалению, между провайдерами журналов существует некоторая несогласованность в отношении того, как рассматривать высокое значение уровня ведения журнала (log-level value) — как более или менее значимое? Указывает ли уровень 6 на критическую ошибку или это просто степень детализации диагностического сообщения? Microsoft.Extensions.Logging.LogLevel использует высокие значения для указания на более высокий приоритет с помощью следующего объявления перечисления LogLevel:

Поэтому, создавая экземпляр ConsoleLoggerProvider, который записывает сообщения, только когда logLevel >= LogLevel.Verbose, вы исключаете из вывода лишь сообщения уровня Debug.

Заметьте, что можно добавить несколько провайдеров к фабрике журналов, даже несколько провайдеров одного и того же типа. Следовательно, если я добавлю вызов ILoggerFactory.AddProvider в код на рис. 1, вызов ILogger.LogInformation выведет сообщение в консоль дважды. Первый провайдер консоли (добавленный AddConsole) по умолчанию отображает все, что находится на уровне LogLevel.Information или выше. Однако вызов ILogger.LogVerbose проявится лишь раз, когда единственный дополнительный провайдер (добавленный методом AddProvider) успешно пройдет фильтрацию.

Шаблоны протоколирования

Как демонстрирует рис. 1, любое протоколирование начинается с фабрики журналов, из которой можно запрашивать ILogger методом ILoggerFactory.CreateLogger . Обобщенный тип T в этом методе идентифицирует класс, в котором выполняется код, поэтому можно выводить имя класса, куда средство ведения журнала пишет сообщения. Иначе говоря, вызвав loggerFactory.CreateLogger

, вы, по сути, инициируете средство ведения журнала, специфичное для класса Program, так чтобы при каждой записи сообщения, можно было записывать и контекст выполнения как находящийся в классе Program. Таким образом, консольный вывод кода с рис. 1 выглядит так:

Этот вывод основан на следующем:

  • info вытекает из того факта, что это вызов метода LogInformation;
  • SampleWebConsoleApp.Program определяется по T;
  • [0] является eventId — значением, которое я не указывал, поэтому по умолчанию оно равно 0;
  • текст «This is a test of the emergency broadcast system.» — это аргумент messages, переданный в LogInformation.

Поскольку значение Program указывает контекст уровня класса, вы скорее всего захотите создавать другой экземпляр средства ведения журналов для каждого класса, из которого вам нужно протоколировать сообщения. Например, если Program создает и вызывает экземпляр класса Controller, вы предпочтете новый экземпляр средства ведения журналов в классе Controller, созданный другим вызовом метода, где T теперь является Controller:

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

Решение — сохранить единственный статический ILoggerFactory в качестве статического свойства, доступного всем классам при создании специфичного для их объекта экземпляра ILoggger. Рассмотрим, к примеру, добавление статического класса ApplicationLogging, включающего статический экземпляр ILoggerFactory:

Очевидная озабоченность в таком классе — является ли LoggerFactory безопасным в многопоточной среде. И, к счастью, как демонстрирует метод AddProvider на рис. 2, он безопасен.

Рис. 2. Реализация Microsoft.Extensions.Logging.LoggerFactory AddProvider

Поскольку единственные данные в экземпляре ILogger определяются обобщенным типом T, вы могли бы возразить, что каждый класс мог бы иметь статический ILogger, используемый объектом каждого класса. Однако, согласно стандарту программирования, гарантирующего безопасность в многопоточной среде для всех статических членов, такой подход потребовал бы контроля параллельной обработки в реализации ILogger (чего по умолчанию в ней нет) и скорее всего вызвал бы появление значимого узкого места из-за необходимости захватывать и освобождать блокировки. По этой причине и рекомендуется иметь индивидуальный экземпляр ILogger для каждого экземпляра какого-либо класса. Поэтому в итоге вы видите свойство ILogger в каждом классе, который должен поддерживать протоколирование (рис. 3).

Рис. 3. Добавление экземпляра ILogger в каждый объект, подлежащий протоколированию

Понимание областей

Зачастую провайдеры поддерживают концепцию области (scope), так чтобы вы могли (к примеру) протоколировать, как ваш ход проходит по цепочке вызовов. Продолжая этот пример, если Program вызывает какой-то метод в классе Controller, тот класс в свою очередь создает экземпляр собственного средства ведения журналов со своим контекстом типа T. Однако вместо того, чтобы просто отображать контекст сообщения в виде info: SampleWebConsoleApp.Program[0], за которым следует info: SampleWebConsoleApp.Controller[0], возможно, вы захотите протоколировать сообщения от Controller, вызванного Program, и даже включать имена самих методов. Для этого вы активируете концепцию областей в провайдере. На рис. 3 приведен пример этого в методе Initialize через вызов Logger.BeginScopeImpl.

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

Рис. 4. Обновленная реализация класса Program

Вывод кода с рис. 3 в сочетании с кодом с рис. 4 показан на рис. 5.

Рис. 5. Консольный вывод протоколирования с включением областей

Заметьте, как области автоматически раскручиваются, чтобы больше не включать Initialize или Main. Эта функциональность предоставляется благодаря тому, что BeginScopeImpl возвращает экземпляр IDisposable, который автоматически раскручивает область, когда выражение using вызывает Dispose.

Применение стороннего провайдера

Чтобы сделать доступными некоторые из наиболее известных сторонних инфраструктур протоколирования, Microsoft сотрудничала с их разработчиками и предусмотрела провайдеры для каждой из таких инфраструктур. Рассмотрим, как подключить инфраструктуру NLog (рис. 6).

Рис. 6. Конфигурирование NLog в качестве провайдера Microsoft.Extensions.Logging

Большая часть кода хорошо известна тем, кто знаком с NLog. Сначала я создаю экземпляр мишени NLog типа NLog.Targets.MemoryTarget и конфигурирую его. (Существуют многочисленные мишени [targets] NLog, и каждую из них можно идентифицировать и настроить в конфигурационном файле NLog — в дополнение к использованию конфигурационного кода, приведенного на рис. 6.) Заметьте, что при всей схожести Layout присваивается литеральное значение $, а не строковое интерполированное значение.

После добавления к LoggerFactory и настройки код идентичен коду для любого другого провайдера.

Обработка исключений

Конечно, одна из самых распространенных причин протоколирования — регистрация того, где произошло исключение, а точнее, когда исключение обрабатывается, а не повторно генерируется или когда оно никак не обрабатывается (bit.ly/1LYGBVS). Как и следовало бы ожидать, Microsoft.Extensions.Logging имеет специфические методы для обработки исключений. Большинство таких методов реализовано в Microsoft.Extensions.Logging.LoggerExtensions как методы расширения ILogger. И именно в этом классе реализуется каждый метод, специфичный для конкретного уровня ведения журнала (ILogger.LogInformation, ILogger.LogDebug, ILogger.LogCritical и т. д.). Например, если вы хотите записывать в журнал сообщение LogLevel.Critical, относящееся к исключению (возможно, перед корректным закрытием приложения), то должны вызвать:

Другой важный аспект протоколирования и обработки исключений заключается в том, что ведение журнала, особенно при обработке исключений, не должно приводить к генерации какого-либо исключения. Если исключение генерируется при ведении журнала, очевидно, что сообщение или исключение никогда не будет зарегистрировано в журнале и потенциально может ускользнуть от внимания независимо от того, насколько оно критичное. К сожалению, готовая реализация ILogger — Microsoft.Extensions.Logging.Logger — не имеет такой обработки исключений, поэтому, если произойдет исключение, вызывающему коду понадобится обработать его — и делать так при каждом вызове Logger.LogX. Универсальный подход к решению этой задачи — возможное обертывание Logger для перехвата исключения. Но не исключено, что вы предпочтете реализовать свои версии ILogger и ILoggerFactory (пример см. по ссылке bit.ly/1LYHq0Q). Учитывая, что .NET Core имеет открытый исходный код, вы могли бы даже клонировать класс и сознательно реализовать обработку исключений в собственных реализациях LoggerFactory и ILogger.

Заключение

Я начал с вопроса: «С какой стати нам понадобилась новая инфраструктура протоколирования?». Надеюсь, теперь это понятно. Новая инфраструктура создает уровень абстракции или оболочку, которая позволяет вам использовать любую инфраструктуру протоколирования в качестве провайдера. Это обеспечивает вам максимальную гибкость в вашей работе. Более того, хотя это доступно только в .NET Core, ссылки на NuGet-пакеты .NET Core вроде Microsoft.Extensions.Logging в стандартном проекте Visual Studio для .NET 4.6 не являются проблемой.

Марк Михейлис (Mark Michaelis) — учредитель IntelliTect, где является главным техническим архитектором и тренером. Почти два десятилетия был Microsoft MVP и региональным директором Microsoft с 2007 года. Работал в нескольких группах рецензирования проектов программного обеспечения Microsoft, в том числе C#, Microsoft Azure, SharePoint и Visual Studio ALM. Выступает на конференциях разработчиков, автор множества книг, последняя из которых — «Essential C# 6.0 (5th Edition)» (itl.tc/EssentialCSharp). С ним можно связаться в Facebook (facebook.com/Mark.Michaelis), через его блог (IntelliTect.com/Mark), в Twitter (@markmichaelis) или по электронной почте mark@IntelliTect.com.

Выражаю благодарность за рецензирование статьи экспертам IntelliTect Кевину Босту (Kevin Bost), Крису Финлейсону (Chris Finlayson) и Майклу Стоуксбери (Michael Stokesbary).

Илон Маск рекомендует:  Статьи о движках сайтов
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL