Данное руководство устарело. Актуальное руководство: Руководство по ASP.NET Core 7
В прошлой теме было рассмотрено создание представления - визуальной части для работы с 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"]
. Причем каждый из таких вызовов представляет собой массив.
И теперь если мы введем некорретные данные, мы получим сообщения об ошибках.