
И снова здравствуйте! В рамках запуска курса «Разработчик C#» мы провели традиционный открытый урок, посвящённый инструменту Fluent Validation. На вебинаре рассмотрели, как избавиться от кучи if-ов на примере проверки корректности заполнения данных покупателя, изучили внутреннюю реализацию библиотеки и способы применения подхода Fluent Interface на практике. Вебинар провёл Алексей Ягур, Team Lead в компании YouDo.
Зачем нужна валидация?
Википедия говорит нам, что валидация (от лат. validus «здоровый, крепкий, сильный») — это доказательство того, что требования конкретного пользователя, продукта, услуги или системы удовлетворены. Как правило, валидация проводится по мере необходимости, предполагая как анализ заданных условий применения, так и оценку соответствия характеристик продукции имеющимся требованиям. Результатом валидации становится вывод о возможности применения продукции для конкретных условий.
Что касается инструмента Fluent Validation, то его знание позволит нам:
- сэкономить время при решении задач, связанных с валидацией данных;
- привести разрозненные самодельные проверки к единому виду;
- похвастаться своими знаниями о валидации за чашкой кофе коллегам :)
Но это всё теория, давайте лучше перейдём к практике.
Валидация на практическом примере: интерактив
Итак, практическая реализация валидации на языке C# выглядит следующим образом:

У нас есть класс Customer, у которого простейший набор полей: FirstName — имя, LastName — фамилия, Age — возраст. И есть некий класс CustomerManager, который сохраняет, как мы видим, в CustomerRepository нового пользователя (покупателя) и выводит нам в консоль информацию о том, что покупатель успешно добавлен.
Давайте попробуем добавить кастомера и менеджера, который будет управлять кастомерами:
void Main() { var customer = new Customer { FirstName = "Томас Георгиевич", LastName = "Вальдемаров", Age = 57, }; var manager = new CustomerManager(); manager.Add(customer); }
Результатом выполнения станет вывод в консоли следующего текста:
Покупатель Томас Георгиевич Вальдемаров успешно добавлен.Как видим, пока всё хорошо. Но что произойдёт, если в нашей базе данных внезапно начнут появляться «испорченные» данные. Например, если в поля будет вноситься некорректная информация (номер телефона вместо имени, возраст со знаком минус и т. п.):
{ FirstName = "+79123456789", LastName = "valde@mar.ru", Age = -14, };
В результате мы увидим, что кастомер с непонятным набором данных тоже будет добавлен:
Покупатель +79123456789 valde@mar.ru успешно добавлен.Естественно, иметь такие данные в нашем репозитории мы не хотим. Как нам обезопасить себя? Самый простой вариант — возвращать ошибку, если у нас, к примеру, не все символы — буквы. Для этого задаём условие для FirstName с помощью if, а если условие не выполняется — прекращаем работу функции с помощью return и выводим на консоль надпись «Ошибка в имени». То же самое проделываем и с LastName. Что касается Age, то тут делаем проверку диапазона цифр, например:
if (customer.Age < 14 || customer.Age > 180)Теперь давайте предположим, что нам нужно добавить дополнительные поля для покупателя, например, телефон. Мы будем валидировать телефон с помощью условия, согласно которому введённые значения должны начинаться с "+79" и иметь в своём составе только цифры. Всё это уже само по себе будет представлять довольно громоздкую конструкцию, а если мы захотим добавить ещё и e-mail?
Как бы там ни было, после выполнения вышеописанных операций мы получим кучу if-ов и большую простыню кода. Разобраться в таком коде постороннему разработчику будет непросто. Что же делать?
Подключаем Fluent Validation
У LINQPad есть возможность подключить библиотеку Fluent Validation, что мы и делаем. Кроме того, создаём ещё один класс CustomerValidator, который будет валидатором. Соответственно, все необходимые правила прописываем в нём. Вносим дополнительные коррективы, а многочисленные if-ы удаляем, т. к. в них отпадает необходимость.
В результате наш итоговый код будет выглядеть следующим образом:
void Main() { var customer = new Customer { FirstName = "Alex2", LastName = "Petrov1", Age = 10, Phone = "+791234567893", Email = "adsf@fadsf3.com" }; var manager = new CustomerManager(); manager.Add(customer); } class Customer { public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } public string Phone { get; set; } public string Email { get; set; } } class CustomerManager { CustomerRepository _repository; CustomerValidator _validator; public CustomerManager() { _repository = new CustomerRepository(); _validator = new CustomerValidator(); } public void Add(Customer customer) { if (!ValidateCustomer(customer)) { return; } _repository.Add(customer); Console.WriteLine($"Покупатель {customer.FirstName} {customer.LastName} успешно добавлен."); } private bool ValidateCustomer(Customer customer) { var result = _validator.Validate(customer); if (result.IsValid) { return true; } foreach(var error in result.Errors) { Console.WriteLine(error.ErrorMessage); } return false; } } class CustomerValidator : AbstractValidator<Customer> { public CustomerValidator() { var msg = "Ошибка в поле {PropertyName}: значение {PropertyValue}"; RuleFor(c => c.FirstName) .Must(c => c.All(Char.IsLetter)).WithMessage(msg); RuleFor(c => c.LastName) .Must(c => c.All(Char.IsLetter)).WithMessage(msg); RuleFor(c => c.Age) .GreaterThan(14).WithMessage(msg) .LessThan(180).WithMessage(msg); RuleFor(c => c.Phone) .Must(IsPhoneValid).WithMessage(msg) .Length(12).WithMessage("Длина должна быть от {MinLength} до {MaxLength}. Текущая длина: {TotalLength}"); RuleFor(c => c.Email) .NotNull().WithMessage(msg) .EmailAddress(); } private bool IsPhoneValid(string phone) { return !(!phone.StartsWith("+79") || !phone.Substring(1).All(c => Char.IsDigit(c))); } } class CustomerRepository { Random _random; public CustomerRepository() { _random = new Random(); } public void Add(Customer customer) { var sleepInSeconds = _random.Next(2, 7); Thread.Sleep(1000 * sleepInSeconds); } }
И ещё немного теории
Хочется добавить ещё несколько слов про Fluent Validation. Этот инструмент называется именно так за счёт «текучего» интерфейса. Опять же, Википедия нам говорит, что текучий интерфейс — это способ реализации объектно-ориентированного API, нацеленный на повышение читабельности исходного кода программы. Определение, как мы видим, содержит много красивых и длинных слов, что не всегда понятно. Но можно сказать и иначе:
«Текучий интерфейс — это способ реализации объектно-ориентированного API, при котором методы возвращают тот же интерфейс, на котором были вызваны».Что касается самой библиотеки, то она включает в себя следующие составные части:
Алексей Ягур
- Основная логика. Вот ссылка на GitHub, по которой можно посмотреть основную логику.
- Вспомогательная логика. За эту логику отвечает FluentValidation.ValidatorAttribute.
- Контекстно-зависимая часть. Смотрим FluentValidation.AspNetCore, FluentValidation.Mvc5 и FluentValidation.WebApi.
- Тесты. Соответственно, нас интересуют FluentValidation.Tests.AspNetCore, FluentValidation.Tests.Mvc5, FluentValidation.Tests.WebApi и FluentValidation.Tests.
На этом всё, пошаговые этапы написания кода смотрите в видео. Кроме того, возможно вам будет интересен дополнительный интерактив на тему «Переменные в текстах ошибок», который преподаватель провёл ближе к концу вебинара.
До встречи на курсе «Разработчик C#»!
