Рассмотренного в прошлых темах материала достаточно для создания примитивного приложения. В этой теме попробуем реализовать простейшее приложение Web API в стиле REST. Архитектура REST предполагает применение следующих методов или типов запросов HTTP для взаимодействия с сервером, где каждый тип запроса отвечает за определенное действие:
GET (получение данных)
POST (добавление данных)
PUT (изменение данных)
DELETE (удаление данных)
Поскольку в приложении ASP.NET Core мы можем легко получить и адрес запроса и тип запроса, то реализовать подобную архитектуру не составит труда.
Вначале определим веб-приложение на ASP.NET Core, которое и будет собственно представлять Web API:
using System.Text.RegularExpressions; // начальные данные List<Person> users = new List<Person> { new() { Id = Guid.NewGuid().ToString(), Name = "Tom", Age = 37 }, new() { Id = Guid.NewGuid().ToString(), Name = "Bob", Age = 41 }, new() { Id = Guid.NewGuid().ToString(), Name = "Sam", Age = 24 } }; var builder = WebApplication.CreateBuilder(); var app = builder.Build(); app.Run(async (context) => { var response = context.Response; var request = context.Request; var path = request.Path; //string expressionForNumber = "^/api/users/([0-9]+)$"; // если id представляет число // 2e752824-1657-4c7f-844b-6ec2e168e99c string expressionForGuid = @"^/api/users/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$"; if (path == "/api/users" && request.Method=="GET") { await GetAllPeople(response); } else if (Regex.IsMatch(path, expressionForGuid) && request.Method == "GET") { // получаем id из адреса url string? id = path.Value?.Split("/")[3]; await GetPerson(id, response); } else if (path == "/api/users" && request.Method == "POST") { await CreatePerson(response, request); } else if (path == "/api/users" && request.Method == "PUT") { await UpdatePerson(response, request); } else if (Regex.IsMatch(path, expressionForGuid) && request.Method == "DELETE") { string? id = path.Value?.Split("/")[3]; await DeletePerson(id, response); } else { response.ContentType = "text/html; charset=utf-8"; await response.SendFileAsync("html/index.html"); } }); app.Run(); // получение всех пользователей async Task GetAllPeople(HttpResponse response) { await response.WriteAsJsonAsync(users); } // получение одного пользователя по id async Task GetPerson(string? id, HttpResponse response) { // получаем пользователя по id Person? user = users.FirstOrDefault((u) => u.Id == id); // если пользователь найден, отправляем его if (user != null) await response.WriteAsJsonAsync(user); // если не найден, отправляем статусный код и сообщение об ошибке else { response.StatusCode = 404; await response.WriteAsJsonAsync(new { message = "Пользователь не найден" }); } } async Task DeletePerson(string? id, HttpResponse response) { // получаем пользователя по id Person? user = users.FirstOrDefault((u) => u.Id == id); // если пользователь найден, удаляем его if (user != null) { users.Remove(user); await response.WriteAsJsonAsync(user); } // если не найден, отправляем статусный код и сообщение об ошибке else { response.StatusCode = 404; await response.WriteAsJsonAsync(new { message = "Пользователь не найден" }); } } async Task CreatePerson(HttpResponse response, HttpRequest request) { try { // получаем данные пользователя var user = await request.ReadFromJsonAsync<Person>(); if (user != null) { // устанавливаем id для нового пользователя user.Id = Guid.NewGuid().ToString(); // добавляем пользователя в список users.Add(user); await response.WriteAsJsonAsync(user); } else { throw new Exception("Некорректные данные"); } } catch (Exception) { response.StatusCode = 400; await response.WriteAsJsonAsync(new { message = "Некорректные данные" }); } } async Task UpdatePerson(HttpResponse response, HttpRequest request) { try { // получаем данные пользователя Person? userData = await request.ReadFromJsonAsync<Person>(); if (userData != null) { // получаем пользователя по id var user = users.FirstOrDefault(u => u.Id == userData.Id); // если пользователь найден, изменяем его данные и отправляем обратно клиенту if (user != null) { user.Age = userData.Age; user.Name = userData.Name; await response.WriteAsJsonAsync(user); } else { response.StatusCode = 404; await response.WriteAsJsonAsync(new { message = "Пользователь не найден" }); } } else { throw new Exception("Некорректные данные"); } } catch (Exception) { response.StatusCode = 400; await response.WriteAsJsonAsync(new { message = "Некорректные данные" }); } } public class Person { public string Id { get; set; } = ""; public string Name { get; set; } = ""; public int Age { get; set; } }
Разберем в общих чертах этот код. Вначале идет определение данных - список объектов Person, с которыми будут работать клиенты:
var users = new List<Person> { new() { Id = Guid.NewGuid().ToString(), Name = "Tom", Age = 37 }, new() { Id = Guid.NewGuid().ToString(), Name = "Bob", Age = 41 }, new() { Id = Guid.NewGuid().ToString(), Name = "Sam", Age = 24 } };
Стоит обратить внимание, что каждый объект Person имеет свойство Id, которое в качестве значения получает Guid - уникальный идентификатор, например "2e752824-1657-4c7f-844b-6ec2e168e99c".
Для упрошения данные определены в виде обычного списка объектов, но в реальной ситуации обычно подобные данные извлекаются из какой-нибудь базы данных.
В методе app.Run()
определяем компонент middleware, который в зависимости от типа запросов (GET/POST/PUT/DELETE) выполняет те или иные действия.
Так, когда приложение получает запрос типа GET по адресу "api/users", то срабатывает следующий код:
if (path == "/api/users" && request.Method=="GET") { await GetAllPeople(response); } //......... // получение всех пользователей async Task GetAllPeople(HttpResponse response) { await response.WriteAsJsonAsync(users); }
Запрос GET предполагает получение объектов, и в данном случае отправляем выше определенный список объектов Person.
Когда клиент обращается к приложению для получения одного объекта по id в запрос типа GET по адресу "api/users/[id]", то срабатывает следующий код:
else if (Regex.IsMatch(path, expressionForGuid) && request.Method == "GET") { // получаем id из адреса url string? id = path.Value?.Split("/")[3]; await GetPerson(id, response); } //.............. // получение одного пользователя по id async Task GetPerson(string? id, HttpResponse response) { // получаем пользователя по id Person? user = users.FirstOrDefault((u) => u.Id == id); // если пользователь найден, отправляем его if (user != null) await response.WriteAsJsonAsync(user); // если не найден, отправляем статусный код и сообщение об ошибке else { response.StatusCode = 404; await response.WriteAsJsonAsync(new { message = "Пользователь не найден" }); } }
Чтобы убедиться, что в запрошенном адресе после "/api/users/" указан id, проверяем соответствие адреса регулярному выражению: "^/api/users/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$"
.
Данное выражение проверяет соответствие последнего сегмента адреса значению Guid, который имеет формат xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
В этом случае нам надо найти нужного пользователя по Id в списке и отправить клиенту. Если же пользователь по Id не был найден, то возвращаем статусный код 404 с некоторым сообщением в формате JSON.
При получении запроса DELETE действует аналогичная логика:
else if (Regex.IsMatch(path, expressionForGuid) && request.Method == "DELETE") { // получаем id из адреса url string? id = path.Value?.Split("/")[3]; await DeletePerson(id, response); } //.............. async Task DeletePerson(string? id, HttpResponse response) { // получаем пользователя по id Person? user = users.FirstOrDefault((u) => u.Id == id); // если пользователь найден, удаляем его if (user != null) { users.Remove(user); await response.WriteAsJsonAsync(user); } // если не найден, отправляем статусный код и сообщение об ошибке else { response.StatusCode = 404; await response.WriteAsJsonAsync(new { message = "Пользователь не найден" }); } }
Только в данном случае, если пользователь найден в списке, удаляем его из списка и посылаем клиенту.
При получении запроса с методом POST по адресу "/api/users" используем метод request.ReadFromJsonAsync()
для извлечения данных из запроса:
else if (path == "/api/users" && request.Method == "POST") { await CreatePerson(response, request); } //.............. async Task CreatePerson(HttpResponse response, HttpRequest request) { try { // получаем данные пользователя var user = await request.ReadFromJsonAsync<Person>(); if (user != null) { // устанавливаем id для нового пользователя user.Id = Guid.NewGuid().ToString(); // добавляем пользователя в список users.Add(user); await response.WriteAsJsonAsync(user); } else { throw new Exception("Некорректные данные"); } } catch (Exception) { response.StatusCode = 400; await response.WriteAsJsonAsync(new { message = "Некорректные данные" }); } }
Поскольку при извлечении данных из запроса может произойти исключение (например, в результате парсинга в JSON), оборачиваем весь код в
try..catch
. И в случае успешного получения данных устанавливаем у нового объекта свойство Id, добавляем его в список users и отправляем обратно клиенту.
Если приложению приходит PUT-запрос, то также с помощью метода request.ReadFromJsonAsync()
получаем отправленные клиентом данные.
Если объект найден в списке, то изменяем его данные и отправляем обратно клиенту, иначе отправляем статусный код 404:
else if (path == "/api/users" && request.Method == "PUT") { await UpdatePerson(response, request); } //.............. async Task UpdatePerson(HttpResponse response, HttpRequest request) { try { // получаем данные пользователя Person? userData = await request.ReadFromJsonAsync<Person>(); if (userData != null) { // получаем пользователя по id var user = users.FirstOrDefault(u => u.Id == userData.Id); // если пользователь найден, изменяем его данные и отправляем обратно клиенту if (user != null) { user.Age = userData.Age; user.Name = userData.Name; await response.WriteAsJsonAsync(user); } else { response.StatusCode = 404; await response.WriteAsJsonAsync(new { message = "Пользователь не найден" }); } } else { throw new Exception("Некорректные данные"); } } catch (Exception) { response.StatusCode = 400; await response.WriteAsJsonAsync(new { message = "Некорректные данные" }); } }
В случае, если запрос идет по другому адресу, то отправляем клиенту веб-страницу index.html, которую мы далее определим:
else { response.ContentType = "text/html; charset=utf-8"; await response.SendFileAsync("html/index.html"); }
Таким образом, мы определили простейший API. Теперь добавим код клиента.
Теперь добавим в проект папку html, в которую добавим новый файл index.html
Определим в файле index.html следующим код для взаимодействия с сервером ASP.NET Core:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> <style> td {padding:5px;} button{margin: 5px;} </style> </head> <body> <h2>Список пользователей</h2> <div> <input type="hidden" id="userId" /> <p> Имя:<br/> <input id="userName" /> </p> <p> Возраст:<br /> <input id="userAge" type="number" /> </p> <p> <button id="saveBtn">Сохранить</button> <button id="resetBtn">Сбросить</button> </p> </div> <table> <thead><tr><th>Имя</th><th>Возраст</th><th></th></tr></thead> <tbody> </tbody> </table> <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(); const 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(); document.getElementById("userId").value = user.id; document.getElementById("userName").value = user.name; document.getElementById("userAge").value = user.age; } else { // если произошла ошибка, получаем сообщение об ошибке const error = await response.json(); console.log(error.message); // и выводим его на консоль } } // Добавление пользователя 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(); document.querySelector("tbody").append(row(user)); } else { const error = await response.json(); console.log(error.message); } } // Изменение пользователя 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: userId, name: userName, age: parseInt(userAge, 10) }) }); if (response.ok === true) { const user = await response.json(); document.querySelector(`tr[data-rowid='${user.id}']`).replaceWith(row(user)); } else { const error = await response.json(); console.log(error.message); } } // Удаление пользователя 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(); } else { const error = await response.json(); console.log(error.message); } } // сброс данных формы после отправки function reset() { document.getElementById("userId").value = document.getElementById("userName").value = document.getElementById("userAge").value = ""; } // создание строки для таблицы function row(user) { const tr = document.createElement("tr"); tr.setAttribute("data-rowid", user.id); 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("button"); editLink.append("Изменить"); editLink.addEventListener("click", async() => await getUser(user.id)); linksTd.append(editLink); const removeLink = document.createElement("button"); removeLink.append("Удалить"); removeLink.addEventListener("click", async () => await deleteUser(user.id)); linksTd.append(removeLink); tr.appendChild(linksTd); return tr; } // сброс значений формы document.getElementById("resetBtn").addEventListener("click", () => reset()); // отправка формы document.getElementById("saveBtn").addEventListener("click", async () => { const id = document.getElementById("userId").value; const name = document.getElementById("userName").value; const age = document.getElementById("userAge").value; if (id === "") await createUser(name, age); else await editUser(id, name, age); reset(); }); // загрузка пользователей getUsers(); </script> </body> </html>
Основная логика здесь заключена в коде javascript. При загрузке страницы в браузере получаем все объекты из БД с помощью функции getUsers()
:
async function getUsers() { // отправляет запрос и получаем ответ const response = await fetch("/api/users", { method: "GET", headers: { "Accept": "application/json" } }); // если запрос прошел нормально if (response.ok === true) { // получаем данные const users = await response.json(); const rows = document.querySelector("tbody"); // добавляем полученные элементы в таблицу users.forEach(user => rows.append(row(user))); } }
Для добавления строк в таблицу используется функция row()
, которая возвращает строку. В этой строке будут определены ссылки для изменения и удаления пользователя.
Ссылка для изменения пользователя с помощью функции getUser()
получает с сервера выделенного пользователя:
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(); document.getElementById("userId").value = user.id; document.getElementById("userName").value = user.name; document.getElementById("userAge").value = user.age; } else { // если произошла ошибка, получаем сообщение об ошибке const error = await response.json(); console.log(error.message); // и выводим его на консоль } }
И выделенный пользователь добавляется в форму над таблицей. Эта же форма применяется и для добавления объекта. С помощью скрытого поля, которое хранит id пользователя, мы можем узнать, какое действие выполняется - добавление или редактирование. Если id не установлен (равен пустой строке), то выполняется функция createUser, которая отправляет данные в POST-запросе:
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(); document.querySelector("tbody").append(row(user)); } else { const error = await response.json(); console.log(error.message); } }
Если же ранее пользователь был загружен на форму, и в скрытом поле сохранился его id, то выполняется функция editUser, которая отправляет PUT-запрос:
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: userId, name: userName, age: parseInt(userAge, 10) }) }); if (response.ok === true) { const user = await response.json(); document.querySelector(`tr[data-rowid='${user.id}']`).replaceWith(row(user)); } else { const error = await response.json(); console.log(error.message); } }
И функция deleteUser()
посылает на сервер запрос типа DELETE на удаление пользователя, и при успешном удалении на сервере
удаляет объект по id из списка объектов Person.
Теперь запустим проект, и по умолчанию приложение отправит браузеру веб-страницу index.html, которая загрузит список объектов:
После этого мы сможем выполнять все базовые операции с пользователями - получение, добавление, изменение, удаление. Например, добавим нового пользователя: