OTUS. Онлайн-образование corporate blog
Programming
C#
22 October 2019

Открытый вебинар «Fluent Validation как инструмент валидации данных»



И снова здравствуйте! В рамках запуска курса «Разработчик 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, при котором методы возвращают тот же интерфейс, на котором были вызваны».
Алексей Ягур
Что касается самой библиотеки, то она включает в себя следующие составные части:

  1. Основная логика. Вот ссылка на GitHub, по которой можно посмотреть основную логику.
  2. Вспомогательная логика. За эту логику отвечает FluentValidation.ValidatorAttribute.
  3. Контекстно-зависимая часть. Смотрим FluentValidation.AspNetCore, FluentValidation.Mvc5 и FluentValidation.WebApi.
  4. Тесты. Соответственно, нас интересуют FluentValidation.Tests.AspNetCore, FluentValidation.Tests.Mvc5, FluentValidation.Tests.WebApi и FluentValidation.Tests.

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

До встречи на курсе «Разработчик C#»!

+5
1k 9
Comments 4
Top of the day