Приходилось ли делать валидацию данных в ASP.NET Core?

«Приходилось ли делать валидацию данных в ASP.NET Core?» — вопрос из категории ASP.NET Core, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Да, постоянно. Валидация — критически важный слой для безопасности и целостности данных. В ASP.NET Core применяю многоуровневый подход.

1. Валидация на уровне модели (Model Validation)

Использую атрибуты из пространства имён System.ComponentModel.DataAnnotations для декларативного описания правил.

public class UserRegistrationDto
{
    [Required(ErrorMessage = "Имя пользователя обязательно.")]
    [StringLength(50, MinimumLength = 3)]
    public string Username { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    [DataType(DataType.Password)]
    [RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*d).{8,}$", 
        ErrorMessage = "Пароль должен содержать минимум 8 символов, заглавную и строчную буквы, цифру.")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Compare(nameof(Password), ErrorMessage = "Пароли не совпадают.")]
    public string ConfirmPassword { get; set; }

    [Range(18, 120)]
    public int Age { get; set; }
}

В контроллере проверка автоматическая:

[HttpPost]
public IActionResult Register(UserRegistrationDto model)
{
    if (!ModelState.IsValid) // Автоматическая проверка атрибутов
    {
        // Возвращаем ошибки клиенту (вместе с состоянием модели для формы)
        return BadRequest(ModelState);
    }
    // Логика регистрации...
}

2. Сложная бизнес-логика с FluentValidation

Для правил, зависящих от контекста или нескольких полей, предпочитаю библиотеку FluentValidation.

public class UserRegistrationValidator : AbstractValidator<UserRegistrationDto>
{
    public UserRegistrationValidator(IUserRepository repository)
    {
        RuleFor(x => x.Username)
            .MustAsync(async (name, cancellation) => 
            {
                return !await repository.UserExistsAsync(name);
            })
            .WithMessage("Имя пользователя уже занято."); // Кастомная асинхронная проверка

        RuleFor(x => x.Age)
            .Must(age => age >= 18).When(x => x.Email.EndsWith(".ru"))
            .WithMessage("Для российских пользователей возраст должен быть 18+"); // Условная валидация
    }
}
// Регистрация валидатора в DI-контейнере
services.AddScoped<IValidator<UserRegistrationDto>, UserRegistrationValidator>();

3. Валидация в сервисном слое

Атрибуты не покрывают всю бизнес-логику. Критичные проверки дублируются в сервисах.

public class UserService : IUserService
{
    public async Task<RegistrationResult> RegisterUserAsync(UserRegistrationDto dto)
    {
        // Повторная проверка "на всякий случай"
        if (dto == null) throw new ArgumentNullException(nameof(dto));

        // Сложная бизнес-логика
        if (await _repository.IsEmailBannedAsync(dto.Email))
        {
            return RegistrationResult.Failed("Email находится в чёрном списке.");
        }
        // ...
    }
}

Важно: Всегда возвращаю клиенту понятные сообщения об ошибках (желательно на его языке) в структурированном формате (например, JSON с полем errors), но никогда не раскрываю внутренние детали реализации или стек-трейсы в продакшене.