Валидация в Web API

Данное руководство устарело. Актуальное руководство: Руководство по ASP.NET Core 7

Последнее обновление: 30.12.2019

В прошлой теме было рассмотрено создание представления - визуальной части для работы с Web API. В частности, мы могли создать или отредактировать модель и отправить ее на сервер. Но при этом не учитывалась валидация данных. Более того не учитывался вывод ошибок валидации, чтобы пользователь смог увидеть, что не так, изменить данные и повторить отправку.

Если бы мы работали в ASP.NET Core MVC, то там с валидацией все проще - с помощью значения ModelState.IsValid проверяем корректность модели. Если модель проходит валидацию, то перенаправляем на определенное действие, если не проходит валидацию, то возвращаем представление с ошибками. Однако Web API использует в целом иную модель обработки запросов, а взаимодействие между сервером и клиентом происходит главным образом через Ajax, что накладывает свои ограничения на валидацию данных.

При использовании Web API состояние обработки запроса на сервере мы можем контролировать с помощью статусных кодов:

  • 200: статус Ok. Указывает на удачное выполнение запроса

  • 201: статус Created. Указывает на успешное создание объекта, как правило, используется в запросах POST

  • 204: статус NoContent - запрос прошел успешно, например, после удаления

  • 400: статус BadRequest - ошибка при выполнении запроса

  • 401: статус Unathorized - пользователь не авторизован

  • 403: статус Forbidden - доступ запрещен

  • 404: статус NotFound - ресурс не найден

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

Но мы не ограничены статусными кодами и, как и в MVC, можем использовать для валидации объект ModelState.

В прошлых темах мы работали с моделью User. Теперь добавим в нее атрибуты валидации:

using System;
using System.ComponentModel.DataAnnotations;

namespace WebAPIApp.Models
{
    public class User
    {
        public int Id { get; set; }
        [Required(ErrorMessage = "Укажите имя пользователя")]
        public string Name { get; set; }
        [Range(1, 100, ErrorMessage = "Возраст должен быть в промежутке от 1 до 100")]
        [Required(ErrorMessage = "Укажите возраст пользователя")]
        public int Age { get; set; }
    }
}

Поскольку изменилось определение модели, выполним миграцию базы данных.

Далее добавим в код контроллера валидацию. Для этого изменим метод, обрабатывающий запросы POST:

using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using WebAPIApp.Models;
using System.Threading.Tasks;

namespace WebAPIApp.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class UsersController : ControllerBase
    {
        UsersContext db;
        public UsersController(UsersContext context)
        {
            db = context;
            if (!db.Users.Any())
            {
                db.Users.Add(new User { Name = "Tom", Age = 26 });
                db.Users.Add(new User { Name = "Alice", Age = 31 });
                db.SaveChanges();
            }
        }

        [HttpGet]
        public async Task<ActionResult<IEnumerable<User>>> Get()
        {
            return await db.Users.ToListAsync();
        }

        // GET api/users/5
        [HttpGet("{id}")]
        public async Task<ActionResult<User>> Get(int id)
        {
            User user = await db.Users.FirstOrDefaultAsync(x => x.Id == id);
            if (user == null)
                return NotFound();
            return new ObjectResult(user);
        }

        // POST api/users
        [HttpPost]
        public async Task<ActionResult<User>> Post(User user)
        {
            // обработка частных случаев валидации
            if (user.Age == 99)
                ModelState.AddModelError("Age", "Возраст не должен быть равен 99");

            if (user.Name == "admin")
            {
                ModelState.AddModelError("Name", "Недопустимое имя пользователя - admin");
            }
            // если есть лшибки - возвращаем ошибку 400
            if (!ModelState.IsValid)
                return BadRequest(ModelState);

            // если ошибок нет, сохраняем в базу данных
            db.Users.Add(user);
            await db.SaveChangesAsync();
            return Ok(user);
        }
		// остальные методы
    }
}

С помощью объекта ModelState здесь валидируется полученная модель User. Но кроме проверки свойства ModelState.IsValid мы также можем добавить и еще дополнительные проверки. Например:

if (user.Name == "admin")
{
    ModelState.AddModelError("Name", "Недопустимое имя пользователя - admin");
}

Для добавления дополнительной ошибки используется метод ModelState.AddModelError, первый параметр которого - ключ ошибки, а второй - сообщение об ошибке. В качестве ключа мы можем использовать любое значение, но по умолчанию система сохраняет все ошибки свойств модели по ключу "Название_свойства". Поэтому все ошибки, связанные со свойством Name, сохраняются по ключу "Name". Причем по одному ключу мы можем указать множество ошибок.

Все ошибки валидаци сохраняются в объекте ModelState, который передается в метод BadRequest и, таким образом, отправляется клиенту вместе с ошибкой 400.

Теперь рассмотрим, как мы можем получить эти ошибки на стороне клиента. Изменим код веб-страницы index.html следующим образом:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Список пользователей</title>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.0/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
    <h2>Список пользователей</h2>
    <div id="errors" class="alert alert-danger" style="display:none;"></div>
    <form name="userForm">
        <input type="hidden" name="id" value="0" />
        <div class="form-group col-md-5">
            <label for="name">Имя:</label>
            <input class="form-control" name="name" />
        </div>
        <div class="form-group col-md-5">
            <label for="age">Возраст:</label>
            <input class="form-control" name="age" type="number" />
        </div>
        <div class="panel-body">
            <button type="submit" id="submit" class="btn btn-primary">Сохранить</button>
            <a id="reset" class="btn btn-primary">Сбросить</a>
        </div>
    </form>
    <table class="table table-condensed table-striped  col-md-6">
        <thead><tr><th>Id</th><th>Имя</th><th>возраст</th><th></th></tr></thead>
        <tbody>
        </tbody>
    </table>
    <div>2019 © Metanit.com</div>
    <script>
        // Получение всех пользователей
        async function GetUsers() {
            // отправляет запрос и получаем ответ
            const response = await fetch("/api/users", {
                method: "GET",
                headers: { "Accept": "application/json" }
            });
            // если запрос прошел нормально
            if (response.ok === true) {
                // получаем данные
                const users = await response.json();
                let rows = document.querySelector("tbody");
                users.forEach(user => {
                    // добавляем полученные элементы в таблицу
                    rows.append(row(user));
                });
            }
        }
        // Получение одного пользователя
        async function GetUser(id) {
            const response = await fetch("/api/users/" + id, {
                method: "GET",
                headers: { "Accept": "application/json" }
            });
            if (response.ok === true) {
                const user = await response.json();
                const form = document.forms["userForm"];
                form.elements["id"].value = user.id;
                form.elements["name"].value = user.name;
                form.elements["age"].value = user.age;
            }
        }
        // Добавление пользователя
        async function CreateUser(userName, userAge) {

            const response = await fetch("api/users", {
                method: "POST",
                headers: { "Accept": "application/json", "Content-Type": "application/json" },
                body: JSON.stringify({
                    name: userName,
                    age: parseInt(userAge, 10)
                })
            });
            if (response.ok === true) {
                const user = await response.json();
                reset();
                document.querySelector("tbody").append(row(user));
            }
            else {
                const errorData = await response.json();
                console.log("errors", errorData);
                if (errorData) {
                    // ошибки вследствие валидации по атрибутам
                    if (errorData.errors) {
                         if (errorData.errors["Name"]) {
                            addError(errorData.errors["Name"]);
                        }
                        if (errorData.errors["Age"]) {
                            addError(errorData.errors["Age"]);
                        }
                    }
                    // кастомные ошибки, определенные в контроллере
                    // добавляем ошибки свойства Name
                    if (errorData["Name"]) {
                        addError(errorData["Name"]);
                    }
                    
                    // добавляем ошибки свойства Age
                    if (errorData["Age"]) {
                        addError(errorData["Age"]);
                    }
                }

                document.getElementById("errors").style.display = "block";
            }
        }
        // Изменение пользователя
        async function EditUser(userId, userName, userAge) {
            const response = await fetch("api/users", {
                method: "PUT",
                headers: { "Accept": "application/json", "Content-Type": "application/json" },
                body: JSON.stringify({
                    id: parseInt(userId, 10),
                    name: userName,
                    age: parseInt(userAge, 10)
                })
            });
            if (response.ok === true) {
                const user = await response.json();
                reset();
                document.querySelector("tr[data-rowid='" + user.id + "']").replaceWith(row(user));
            }
        }
        // Удаление пользователя
        async function DeleteUser(id) {
            const response = await fetch("/api/users/" + id, {
                method: "DELETE",
                headers: { "Accept": "application/json" }
            });
            if (response.ok === true) {
                const user = await response.json();
                document.querySelector("tr[data-rowid='" + user.id + "']").remove();
            }
        }

        // сброс формы
        function reset() {
            const form = document.forms["userForm"];
            form.reset();
            form.elements["id"].value = 0;
        }
        function addError(errors) {
            errors.forEach(error => {
                const p = document.createElement("p");
                p.append(error);
                document.getElementById("errors").append(p);
            });
        }
        // создание строки для таблицы
        function row(user) {

            const tr = document.createElement("tr");
            tr.setAttribute("data-rowid", user.id);

            const idTd = document.createElement("td");
            idTd.append(user.id);
            tr.append(idTd);

            const nameTd = document.createElement("td");
            nameTd.append(user.name);
            tr.append(nameTd);

            const ageTd = document.createElement("td");
            ageTd.append(user.age);
            tr.append(ageTd);

            const linksTd = document.createElement("td");

            const editLink = document.createElement("a");
            editLink.setAttribute("data-id", user.id);
            editLink.setAttribute("style", "cursor:pointer;padding:15px;");
            editLink.append("Изменить");
            editLink.addEventListener("click", e => {

                e.preventDefault();
                GetUser(user.id);
            });
            linksTd.append(editLink);

            const removeLink = document.createElement("a");
            removeLink.setAttribute("data-id", user.id);
            removeLink.setAttribute("style", "cursor:pointer;padding:15px;");
            removeLink.append("Удалить");
            removeLink.addEventListener("click", e => {

                e.preventDefault();
                DeleteUser(user.id);
            });

            linksTd.append(removeLink);
            tr.appendChild(linksTd);

            return tr;
        }
        // сброс значений формы
        document.getElementById("reset").addEventListener("click", function (e) {

            e.preventDefault();
            reset();
        })

        // отправка формы
        document.forms["userForm"].addEventListener("submit", e => {
            e.preventDefault();
            document.getElementById("errors").innerHTML="";
            document.getElementById("errors").style.display = "none";

            const form = document.forms["userForm"];
            const id = form.elements["id"].value;
            const name = form.elements["name"].value;
            const age = form.elements["age"].value;
            if (id == 0)
                CreateUser(name, age);
            else
                EditUser(id, name, age);
        });

        // загрузка пользователей
        GetUsers();

    </script>
</body>
</html>

Для вывода ошибок здесь определен специальный блок с id="errors". При получении ошибки в функции CreateUser() мы получаем данные, посланные через объект ModelState.

if (errorData) {
	const errorData = await response.json();
	console.log("errors", errorData);
	(errorData) {
		// ошибки вследствие валидации по атрибутам
		if (errorData.errors) {
			if (errorData.errors["Name"]) {
				addError(errorData.errors["Name"]);
			}
			if (errorData.errors["Age"]) {
				addError(errorData.errors["Age"]);
			}
		}
		// кастомные ошибки, определенные в контроллере
		// добавляем ошибки свойства Name
		if (errorData["Name"]) {
			addError(errorData["Name"]);
		}
		// добавляем ошибки свойства Age
		if (errorData["Age"]) {
			addError(errorData["Age"]);
		}
	}
	document.getElementById("errors").style.display = "block";
}

Но чтобы обратиться к ошибкам, надо пройти несколько уровней вложенности. Ошибки, которые добавляются в результате применения правил атрибутов валидации, можно получить из объекта errorData.errors. Например, чтобы получить ошибки свойства Age, придется использовать вызов errorData.errors["Age"]. Получение сообщения об ошибках, которые были определены в контроллере, производится непосредственно из посланного объекта errorData.["Age"]. Причем каждый из таких вызовов представляет собой массив.

И теперь если мы введем некорретные данные, мы получим сообщения об ошибках.

Валидация в ASP.NET Core Web API
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850