Валидация модели

Rachel Appel

Введение в валидацию моделей

Прежде чем приложение начинает хранить данные в БД, оно должно их валидировать. Данные нужно проверять на предмет потенциальной угрозы для безопасности, на предмет должного форматирирования по типу и размеру, а также они должны соответствовать вашим правилам. Валидация необходима, хотя порой ее сложно реализовать. В MVC валидация происходит как и на стороне клиента, так и на стороне сервера.

В счастью, в .NET валидация абстрагирована в атрибуты валидации. Эти атрибуты содержат валидационны код, то есть, это уменьшает то число кода, который вы должны написать.

Атрибуты валидации

Атрибуты валидации - это способ настроить валидацию моделей, чтобы она стала похожа на валидацию полей в таблицах БД. Сюда включены ограничения, например, при присвоении типов данных или заполнении необходимых полей. Другой вид валидации включает в себя применение к данным паттернов при реализации бизнес правил, например, при работе с номером телефона, адресом или имейлом.

Далее представлена модель Movie из приложения, в котором храниться информация о фильмах и TV шоу. Здесь есть несколько свойств, а для некоторых строковых свойств нужны требования по длине. Кроме того, для свойства Price необходимо числовое ограничение от 0 до $999.99, а также некоторые пользовательские атрибуты валидации.

public class Movie
{
    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; }

    [Required]
    [ClassicMovie(1960)]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; }

    [Required]
    [Range(0, 999.99)]
    public decimal Price { get; set; }

    [Required]
    public Genre Genre { get; set; }

    public bool Preorder { get; set; }
}

Вот несколько популярных встроенных атрибутов валидации:

  • [CreditCard]: проверяет, чтобы у свойства был необходимый формат для кредитных карт.
  • [Compare]: проверяет, чтобы два свойства в модели совпадали.
  • [EmailAddress]: проверяет, чтобы у свойства был формат имейла.
  • [Phone]: проверяет, чтобы у свойства был формат телефона.
  • [Range]: проверяет, чтобы значение свойства лежало в определенном диапазоне.
  • [RegularExpression]: проверяет, чтобы данные соответствовали указанному регулярному выражению.
  • [Required]: делает свойство обязательным.
  • [StringLength]: устанавливает максимальную длину для строкового свойства.
  • [Url]: проверяет, чтобы у свойства был формат URL.

Для валидации MVC поддерживает любой атрибут, который наследуется от ValidationAttribute. Много полезных атрибутов можно найти в пространстве имен System.ComponentModel.DataAnnotations.

Бывают случаи, когда вам нужно больше возможностей, чем могут предложить встроенные атрибуты. Тогда вы можете создавать пользовательские атрибуты валидации с помощью наследования от ValidationAttribute или изменив модель, чтобы она реализовывала IValidatableObject.

Состояние модели

Состояние модели представляет ошибки валидации в предоставленных значениях HTML формы.

MVC продолжит валидировать поля, пока не достигнет максимального числа ошибок (200 по умолчанию). Вы можете выяснить это число, если вставите следующий код в метод ConfigureServices класса Startup.cs:

services.AddMvc(options => options.MaxModelValidationErrors = 50);

Обработка ошибок состояния модели

Валидация модели происходит до вызова методов действия любого контроллера, и это ответственность методов действия - изучить ModelState.IsValid и отреагировать соответствующим образом. В большинстве случаев соответствующая реакция заключается в возвращении какой-либо ошибки, также в идеале происходит пояснение, почему валидация модели провалилась.

Как правило, в большинстве приложений используется стандартная обработка ошибок валидации, и в таких случаях местом для реализации данной политики может быть фильтр.

Валидация вручную

После завершения связывания моделей и валидации, возможно, вы захотите где-то повторить их части. Например, пользователь может ввести текст в поле, где ожидается целочисленное значение.

Иногда нам требуется запускать валидацию вручную. Чтобы сделать это, запустите метод TryValidateModel, как показано вот здесь:

TryValidateModel(movie);

Пользовательская валидация

Атрибуты валидации подходят для большинства валидационных нужд. Однако некоторые правила валидации нужны именно вашему приложению. Для таких сценариев лучшим вариантом являются пользовательские атрибуты валидации. Сделать это в MVC довольно просто: только наследовать от ValidationAttribute и переопределить метод IsValid. Метод IsValid принимает два параметра: первый - это объект с именем value, а второй - это объект ValidationContext с именем validationContext. Value касается действительного значения из поля, которое проверяет ваш пользовательский валидатор.

В следующем примере в бизнес правиле сказано, что пользователи не могут установить жанр на Classic для фильма, выпущенного после 1960 года. Атрибут [ClassicMovie] сперва проверяет жанр, а затем если жанр является классическим, тогда он проверяет дату выхода. Если дата выхода оказывается больше 1960, валидация не проходит. Атрибут принимает целочисленный параметр, представляющий год. Значение параметра можно словить в конструкторе атрибута, как показано здесь:

public class ClassicMovieAttribute : ValidationAttribute, IClientModelValidator
{
    private int _year;

    public ClassicMovieAttribute(int Year)
    {
        _year = Year;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        Movie movie = (Movie)validationContext.ObjectInstance;

        if (movie.Genre == Genre.Classic && movie.ReleaseDate.Year > _year)
        {
            return new ValidationResult(GetErrorMessage());
        }

        return ValidationResult.Success;
    }

Переменная movie представляет объект Movie, который содержит данные из формы. В данном случае код валидации проверяет дату и жанр в методе IsValid класса ClassicMovieAttribute. При успешной валидации IsValid возвращает код ValidationResult.Success, а если валидация не прошла - ValidationResult с сообщением об ошибке. Если пользователь меняет поле Genre и отправляет форму, метод IsValid класса ClassicMovieAttribute будет проверять, является ли фильм классическим. Применяйте ClassicMovieAttribute как и любой другой встроенный атрибут, к таким свойствам как ReleaseDate, чтобы убедиться, что валидация прошла, как показано в примере выше. Поскольку в примере мы работаем только с типами Movie, лучше использовать IValidatableObject, как показано далее.

Как вариант, тот же самый код можно разместить в модели, реализовав метод Validate для интерфейса IValidatableObject. Тогда как пользовательские атрибуты валидации отлично работают с валидацией пользовательских свойств, IValidatableObject можно использовать для реализации валидации на уровне класса, как показано здесь.

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
    if (Genre == Genre.Classic && ReleaseDate.Year > _classicYear)
    {
        yield return new ValidationResult(
            "Classic movies must have a release year earlier than " + _classicYear,
            new[] { "ReleaseDate" });
    }
}

Валидация на стороне клиента

Валидация на стороне клиента очень удобна для пользователей, поскольку она экономит время из-за того, что не обращается к серверу. Простая немедленная валидация позволяет пользователям работать более эффективно.

У вам должно быть представление с необходимым JavaScript скриптом, как вы здесь видите.

<script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.11.3.min.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.14.0/jquery.validate.min.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validation.unobtrusive/3.2.6/jquery.validate.unobtrusive.min.js"></script>

MVC использует атрибуты валидации в дополнение к метаданным из свойств модели, чтобы проверять данные и отображать сообщения об ошибках с помощью JavaScript. Если вы используете MVC для представления элементов из модели с помощью тег-хелперов или HTML хелперов, он добавит HTML 5 атрибуты data- в элементы формы, которым нужна валидация, как показано ниже. MVC генерирует атрибуты data- и для встроенных, и для пользовательских атрибутов. Вы можете отображать ошибки валидации на стороне клиента с помощью релевантных тег-хелперов, как показано здесь:

<div class="form-group">
    <label asp-for="ReleaseDate" class="col-md-2 control-label"></label>
    <div class="col-md-10">
        <input asp-for="ReleaseDate" class="form-control" />
        <span asp-validation-for="ReleaseDate" class="text-danger"></span>
    </div>
</div>

Тег-хелперы представляют следующий HTML. Обратите внимание, что data- атрибуты в выходном HTML соответствуют атрибутам валидации для свойства ReleaseDate. Атрибут data-val-required содержит сообщение об ошибке, которое отобразится, если пользователь не заполнил поле с датой выхода, и это сообщение размещено внутри элемента <span>.

<form action="/movies/Create" method="post">
  <div class="form-horizontal">
    <h4>Movie</h4>
    <div class="text-danger"></div>
    <div class="form-group">
      <label class="col-md-2 control-label" for="ReleaseDate">ReleaseDate</label>
      <div class="col-md-10">
        <input class="form-control" type="datetime"
        data-val="true" data-val-required="The ReleaseDate field is required."
        id="ReleaseDate" name="ReleaseDate" value="" />
        <span class="text-danger field-validation-valid"
        data-valmsg-for="ReleaseDate" data-valmsg-replace="true"></span>
      </div>
    </div>
    </div>
</form>

Валидация на стороне клиента не отправляет форму, пока она не будет валидной. Кнопка Submit запускает JavaScript, который либо отправляет форму, либо выдает сообщение об ошибке.

MVC определяет значения атрибутов, основываясь на типе .NET свойства, которое может быть переопределено с помощью атрибутов [DataType]. Базовый атрибут [DataType] не проводит реальную валидацию со стороны сервера. Браузеры выбирают свои сообщения об ошибках и отображают их, хотя пакет jQuery Validation Unobtrusive может переопределить эти сообщения.

IClientModelValidator

Вы можете создать логику для пользовательских атрибутов, и валидация автоматически выполнит ее на стороне клиента. Сперва надо проконтролировать, какие data- атрибуты добавлены, реализовав интерфейс IClientModelValidator:

public void AddValidation(ClientModelValidationContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException(nameof(context));
    }

    MergeAttribute(context.Attributes, "data-val", "true");
    MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage());

    var year = _year.ToString(CultureInfo.InvariantCulture);
    MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
}

Атрибуты, которые реализуют этот интерфейс, могут добавлять в сгенерированные поля HTML атрибуты. Выходные данные ReleaseDate показаны в HTML и они похожи на данные из предыдущего примера, разве что здесь есть атрибут data-val-classicmovie, который был определен в методе AddValidation класса IClientModelValidator.

<input class="form-control" type="datetime"
data-val="true"
data-val-classicmovie="Classic movies must have a release year earlier than 1960"
data-val-classicmovie-year="1960"
data-val-required="The ReleaseDate field is required."
id="ReleaseDate" name="ReleaseDate" value="" />

Простая валидация использует данные в data- атрибутах для отображения сообщений об ошибках. Однако jQuery ничего не знает о правилах или сообщениях, пока вы не добавите это в jQuery объект validator. В примере ниже добавлен метод classicmovie, который содержит пользовательскую валидацию на стороне клиента для объекта jQuery validator.

$(function () {
    jQuery.validator.addMethod('classicmovie',
        function (value, element, params) {
            // Get element value. Classic genre has value '0'.
            var genre = $(params[0]).val(),
                year = params[1],
                date = new Date(value);
            if (genre && genre.length > 0 && genre[0] === '0') {
                // Since this is a classic movie, invalid if release date is after given year.
                return date.getFullYear() <= year;
            }

            return true;
        });

    jQuery.validator.unobtrusive.adapters.add('classicmovie',
        [ 'element', 'year' ],
        function (options) {
            var element = $(options.form).find('select#Genre')[0];
            options.rules['classicmovie'] = [element, parseInt(options.params['year'])];
            options.messages['classicmovie'] = options.message;
        });
}(jQuery));

Теперь у jQuery есть информация для выполнения пользовательской JavaScript валидации.

Удаленная валидация

Удаленная валидация отлично работает тогда, когда вам нужно провалидировать данные на стороне клиента и сравнить их с данными на стороне сервера. Например, вам нужно проверить, используются ли уже имейл или имя пользователя, и в таком случае вам требуется запрашивать большое число данных. Скачивание большого набора данных для валидации одного или пары полей требует слишком большого количества ресурсов. Таким образом также может быть раскрыта секретная информация. В качестве альтернативы вы можете провести запрос по кругу, чтобы провести валидацию поля.

Вы можете реализовать удаленную валидацию в два этапа. Во-первых, вам нужно обозначить модель с помощью атрибута [Remote]. У атрибута [Remote] есть несколько переопределенных вариантов. В примере используется метод действия VerifyEmail контроллера Users.

public class User
{
    [Remote(action: "VerifyEmail", controller: "Users")]
    public string Email { get; set; }
}

Затем в соответствующий метод действия вставляется валидационный код, определенный в атрибуте [Remote]. Он возвращает JsonResult, который потом может быть использован на стороне клиента.

[AcceptVerbs("Get", "Post")]
public IActionResult VerifyEmail(string email)
{
    if (!_userRepository.VerifyEmail(email))
    {
        return Json(data: $"Email {email} is already in use.");
    }

    return Json(data: true);
}

И теперь с результатом работает JS.

Поделись хорошей новостью с друзьями!
Следи за новостями!