И снова здравствуйте! В рамках запуска курса «Разработчик 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#»!
questor
Простите, но это капец какой короткий пересказ официальной справки!
На том же fluentvalidation непросто сделать вот что. Как-то понадобилось мне в базе держать данные нескольких пользователей (допустим, заметки) и запрещать показывать чужие. Поэтому даже в самом простом запросе
Я либо дважды лезу в базу (на валидаторе в первый раз, на IRequestHandler<Query, NoteDto> — второй), либо отказываюсь от использования валидатора в handler'е и пишу по-старинке, либо пишу громоздкий некрасивый код (потому что надо проверять связку query+полученный из БД результат).
И в общем, это как-то не особо вдохновляет.
Veikedo
Конкретно для вашего случая (если у вас EF например), проще было бы просто условие в фильтре поставить:
Но вообще это можно решить с помощью RuleSet'ов
TimurNes
А это не задача FluentValidation. Это — задача слоя бизнес логики, в вашем примере — QueryHandler'а, который должен либо самостоятельно определить текущего юзера, либо получить эти данные из Query, а затем либо делегировать выборку репозиторию (читай — sql запросу), либо самостоятельно отфильтровать данные.
FluentValidation — это библиотека исключительно для Presentation Layer и предназначена только для первичной валидации данных, пришедших от пользователя. Например, что email — это email, а не случайная строка, или что возраст — больше нуля, но меньше 120 и, в случае ошибки — детально сообщить об этом юзеру. С чем и справляется блестяще.
Бизнес слою же вообще желательно работать только с ValueObject, которые должны быть реализованы так, чтобы их в принципе было невозможно создать невалидными, но это уже оффтопик.
И, к вашему примеру, добавлю еще, что RequestHandler не должен возвращать DTO. Опять же, потому что RequestHandler — это слой бизнес логики (модели), а DTO — это Presentation Layer. По этому меняем NoteDto на Note (Entity), в контроллер добавляем AutoMapper (или собственный маппер Note -> NoteDto) и радуемся тому что слои не залезают друг в дружку, а код — чистый, красивый и лаконичный.