В предыдущем посте (оригинал, перевод) я описал как можно использовать атрибуты DataAnnotation и новый метод ValidateOnStart(), чтобы проверить валидность вашей конфигурации на старте приложения
В этом посте я покажу как сделать то же самое используя популярную библиотеку с открытым исходным кодом FluentValidation. Для этого понадобится добавить несколько вспомогательных классов.
Предыдущий пост включает описание строго типизированной конфигурации в целом и все возможные варианты получить ошибку, так что если вы сталкиваетесь с этой темой впервые, предлагаю сначала ознакомиться с предыдущей статьёй. Здесь, перед тем, как перейти к FluentValidation я быстренько повторюсь как валидация IOptions работает с атрибутами DataAnnotation.
Проверка IOptions на старте приложения
В .NET проверка конфигурации появилась ещё в .NET Core 2.2 с методами Validate<> и ValidateDataAnnotations(), но они вызывались не на старте приложения, а после попытки доступа к объекту конфигурации.
В .NET 6 появился новый метод, ValidateOnStart(), который запускает валидацию сразу же после запуска приложения.
Чтобы использовать такую валидацию, нужно сделать четыре вещи:
Связать объект конфигурации из файла с IOptions<T>
Добавить атрибуты валидации к объекту конфигурации
Вызвать ValidateDateAnnotations()OptionsBuilder’а, возвращённого из AddOptions<T>()
Вызвать ValidateOnStart()OptionsBuilder’а.
В примере ниже, я настроил валидацию конфига для объекта SlackApiSettings:
Теперь создадим файл конфигурации с ошибкой, например, удалив обязательное значение DisplayName:
Теперь, если вы запустите приложение, то получите исключение сразу, а не в момент обращения к конфигурации:
Валидация IOptions с использованием FluentValidation
Атрибуты DataAnnotation это довольно простой способ проверки данных, который перестаёт работать в более сложных случаях. Поэтому заменим его на популярную альтернативу FluentValidation
Эта статья не учебник по FluentValidation, а лишь простой пример минимально необходимой настройки для валидации на старте
1. Создание проекта
Создадим минимальное API для тестирования и добавим зависимость FluentValidation:
Затем заменим содержимое Program.cs простым API, которое использует строго типизированный объект конфигурации (SlackApiSettings) и выводит его значение при запросе:
В этом простом приложении мы связываем объект SlackApiSettings с секцией SlackApi в конфигурации. Простое API возвращает содержимое конфига в формате JSON:
2. Добавление валидатора FluentValidator
В документации можно подробнее прочитать про то, как создавать валидаторы и правила. Ниже приведён пример того, как можно переписать валидацию на DataAnnotations из начала статьи с помощью AbstractValidator<T>
Важный момент: Хотя это (скорее всего) не станет проблемой, но стоит держать в голове, что здесь нельзя использовать асинхронные правила, так как интерфейс IValidateOptions<T>, который мы будем использовать дальше только синхронный.
3. Создание метода расширения ValidateFluentValidation
Следующий этап самый важный. Нужно добавить альтернативу метода ValidateDataAnnotations для FluentValidation, который я назвал ValidateFluentValidation. Это расширение довольно простое и похоже на версию для DataAnnotation:
Этот метод расширения OptionsBuilder<T> добавляет новый сервис FluentValidationOptions<T> в DI-контейнер и регистрирует его как IValidateOptions<T>. В FluentValidationOptions<T> и происходит вся магия. Здесь кода сильно больше, поэтому всё прокомментировано:
Код выше немного сложнее оригинального IValidateOptions, так как добавляет две особенности:
IOptions<T> поддерживает именованные параметры. Они используются редко; чаще всего в аутентификации, например. Больше о них можно прочитать в статье автора.
IValidateOptions вызывается из IOptionsMonitor, который регистрируется как синглтон. Поэтому наш объект FluentValidationOptions тоже должен регистрироваться как синглтон. Однако обычно валидаторы FluentValidation регистрируются как scoped. Из-за этого несоответствия мы не можем внедрить IValidator<T> в конструктор FluentValidationOptions и должны сначала создать IServiceScope
За исключением предыдущих двух пунктов, код довольно простой. Он запускает validator.Validate() и возвращает соответствующий результат.
NB: Этот класс требует того, чтобы IValidator<T> был зарегистрирован для конкретного типа в DI
Теперь мы готовы собрать все кусочки пазла воедино, чтобы валидировать конфигурацию на старте приложения.
Собираем всё вместе
Если мы соединим весь код из предыдущих шагов, зарегистрируем валидатор, полное приложение будет выглядеть как-то так:
Теперь, если мы запустим приложение с ошибочной конфигурацией, получим исключение на старте приложения, как мы и хотели:
Пишем метод расширения
Сейчас нужно помнить добавлять валидатор для настроек, включать валидацию для объекта конфига и включать валидацию на старте. Если вы хотите, вместо этого можно написать метод расширения, который будет делать это за вас:
Тогда ваше приложение станет настолько простым:
Вывод
В этом посте я показал вам, как можно использовать FluentValidation для ваших строго типизированных объектов конфигурации IOptions<> в ASP.NET Core. Я создал версию ValidateDataAnnotations() как метод расширения ValidateFluentValidation(). В сочетании с ValidateOnStart() (и зарегистрированным IValidator<T>), получаем валидацию конфигов на старте приложения. Это позволяет быть уверенным в том, что ошибки конфигурации всплывут как можно раньше, вместо появления ошибок в рантайме.