Рассмотренного в прошлых темах материала достаточно для создания примитивного приложения. В этой теме попробуем реализовать простейшее приложение Web API в стиле REST. Архитектура REST предполагает применение следующих методов или типов запросов HTTP для взаимодействия с сервером, где каждый тип запроса отвечает за определенное действие:
GET (получение данных)
POST (добавление данных)
PUT (изменение данных)
DELETE (удаление данных)
Поскольку в приложении ASP.NET Core мы можем легко получить и адрес запроса и тип запроса, то реализовать подобную архитектуру не составит труда.
Вначале определим веб-приложение на ASP.NET Core, которое и будет собственно представлять Web API:
Imports Microsoft.AspNetCore.Builder Imports Microsoft.AspNetCore.Http Imports System.Text.RegularExpressions Module Program 'начальные данные Dim tom = New Person With {.Id = Guid.NewGuid().ToString(), .Name = "Tom", .Age = 37} Dim bob = New Person With {.Id = Guid.NewGuid().ToString(), .Name = "Bob", .Age = 41} Dim sam = New Person With {.Id = Guid.NewGuid().ToString(), .Name = "Sam", .Age = 24} Dim users As List(Of Person) = New List(Of Person) From {tom, bob, sam} Sub Main(args As String()) Dim builder = WebApplication.CreateBuilder(args) Dim app = builder.Build() app.Run(Async Function(context As HttpContext) As Task Dim response = context.Response Dim request = context.Request Dim path = request.Path.Value 'Dim expressionForNumber = "^/api/users/([0 - 9]+)$"; 'если id представляет число '2e752824-1657-4c7f-844b-6ec2e168e99c Dim expressionForGuid = "^/api/users/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$" If path = "/api/users" AndAlso request.Method = "GET" Then Await GetAllPeople(response) ElseIf Regex.IsMatch(path, expressionForGuid) AndAlso request.Method = "GET" Then 'получаем id из адреса url Dim id As String = path.Split("/")(3) Await GetPerson(id, response, request) ElseIf path = "/api/users" AndAlso request.Method = "POST" Then Await CreatePerson(response, request) ElseIf path = "/api/users" AndAlso request.Method = "PUT" Then Await UpdatePerson(response, request) ElseIf Regex.IsMatch(path, expressionForGuid) AndAlso request.Method = "DELETE" Then Dim id = path.Split("/")(3) Await DeletePerson(id, response) Else response.ContentType = "text/html; charset=utf-8" Await response.SendFileAsync("html/index.html") End If End Function) app.Run() End Sub 'получение всех пользователей Async Function GetAllPeople(response As HttpResponse) As Task Await response.WriteAsJsonAsync(users) End Function 'получение одного пользователя по id Async Function GetPerson(id As String, response As HttpResponse, request As HttpRequest) As Task 'получаем пользователя по id Dim user = users.FirstOrDefault(Function(u) u.Id = id) 'если пользователь найден, отправляем его If user IsNot Nothing Then Await response.WriteAsJsonAsync(user) 'если не найден, отправляем статусный код и сообщение об ошибке Else response.StatusCode = 404 Await response.WriteAsJsonAsync(New With {.message = "Пользователь не найден"}) End If End Function Async Function DeletePerson(id As String, response As HttpResponse) As Task 'получаем пользователя по id Dim user = users.FirstOrDefault(Function(u) u.Id = id) 'если пользователь найден, удаляем его If user IsNot Nothing Then users.Remove(user) Await response.WriteAsJsonAsync(user) 'если не найден, отправляем статусный код и сообщение об ошибке Else response.StatusCode = 404 Await response.WriteAsJsonAsync(New With {.message = "Пользователь не найден"}) End If End Function Async Function CreatePerson(response As HttpResponse, request As HttpRequest) As Task 'Отправляемый в ответ клиенту объект Dim objectToSent As Object = New With {.message = "Некорректные данные"} ' значение по умолчанию Try 'получаем данные пользователя Dim user = Await request.ReadFromJsonAsync(Of Person)() If user IsNot Nothing Then 'устанавливаем id для нового пользователя user.Id = Guid.NewGuid().ToString() 'добавляем пользователя в список users.Add(user) objectToSent = user Else Throw New Exception("Некорректные данные") End If Catch ex As Exception response.StatusCode = 400 End Try Await response.WriteAsJsonAsync(objectToSent) End Function Async Function UpdatePerson(response As HttpResponse, request As HttpRequest) As Task 'Отправляемый в ответ клиенту объект Dim objectToSent As Object = New With {.message = "Некорректные данные"} ' значение по умолчанию Try 'получаем данные пользователя Dim userData = Await request.ReadFromJsonAsync(Of Person)() If userData IsNot Nothing Then 'получаем пользователя по id Dim user = users.FirstOrDefault(Function(u) u.Id = userData.Id) 'если пользователь найден, изменяем его данные и отправляем обратно клиенту If user IsNot Nothing Then user.Age = userData.Age user.Name = userData.Name objectToSent = user Else response.StatusCode = 404 objectToSent = New With {.message = "Пользователь не найден"} End If Else Throw New Exception("Некорректные данные") End If Catch response.StatusCode = 400 End Try Await response.WriteAsJsonAsync(objectToSent) End Function Public Class Person Public Property Id() As String Public Property Name() As String Public Property Age() As Integer End Class End Module
Разберем в общих чертах этот код. Вначале идет определение данных - список объектов Person, с которыми будут работать клиенты:
Dim tom = New Person With {.Id = Guid.NewGuid().ToString(), .Name = "Tom", .Age = 37} Dim bob = New Person With {.Id = Guid.NewGuid().ToString(), .Name = "Bob", .Age = 41} Dim sam = New Person With {.Id = Guid.NewGuid().ToString(), .Name = "Sam", .Age = 24} Dim users As List(Of Person) = New List(Of Person) From {tom, bob, sam}
Стоит обратить внимание, что каждый объект Person имеет свойство Id, которое в качестве значения получает Guid - уникальный идентификатор, например "2e752824-1657-4c7f-844b-6ec2e168e99c".
Для упрошения данные определены в виде обычного списка объектов, но в реальной ситуации обычно подобные данные извлекаются из какой-нибудь базы данных.
В методе app.Run()
определяем компонент middleware, который в зависимости от типа запросов (GET/POST/PUT/DELETE) выполняет те или иные действия.
Так, когда приложение получает запрос типа GET по адресу "api/users", то срабатывает следующий код:
If path = "/api/users" AndAlso request.Method = "GET" Then Await GetAllPeople(response) ................ 'получение всех пользователей Async Function GetAllPeople(response As HttpResponse) As Task Await response.WriteAsJsonAsync(users) End Function
Запрос GET предполагает получение объектов, и в данном случае отправляем выше определенный список объектов Person.
Когда клиент обращается к приложению для получения одного объекта по id в запрос типа GET по адресу "api/users/[id]", то срабатывает следующий код:
ElseIf Regex.IsMatch(path, expressionForGuid) AndAlso request.Method = "GET" Then 'получаем id из адреса url Dim id As String = path.Split("/")(3) Await GetPerson(id, response, request) '.............. 'получение одного пользователя по id Async Function GetPerson(id As String, response As HttpResponse, request As HttpRequest) As Task 'получаем пользователя по id Dim user = users.FirstOrDefault(Function(u) u.Id = id) 'если пользователь найден, отправляем его If user IsNot Nothing Then Await response.WriteAsJsonAsync(user) 'если не найден, отправляем статусный код и сообщение об ошибке Else response.StatusCode = 404 Await response.WriteAsJsonAsync(New With {.message = "Пользователь не найден"}) End If End Function
Чтобы убедиться, что в запрошенном адресе после "/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 действует аналогичная логика:
ElseIf Regex.IsMatch(path, expressionForGuid) AndAlso request.Method = "DELETE" Then Dim id = path.Split("/")(3) Await DeletePerson(id, response) '.............. Async Function DeletePerson(id As String, response As HttpResponse) As Task 'получаем пользователя по id Dim user = users.FirstOrDefault(Function(u) u.Id = id) 'если пользователь найден, удаляем его If user IsNot Nothing Then users.Remove(user) Await response.WriteAsJsonAsync(user) 'если не найден, отправляем статусный код и сообщение об ошибке Else response.StatusCode = 404 Await response.WriteAsJsonAsync(New With {.message = "Пользователь не найден"}) End If End Function
Только в данном случае, если пользователь найден в списке, удаляем его из списка и посылаем клиенту.
При получении запроса с методом POST по адресу "/api/users" используем метод request.ReadFromJsonAsync()
для извлечения данных из запроса:
ElseIf path = "/api/users" AndAlso request.Method = "POST" Then Await CreatePerson(response, request) '.............. Async Function CreatePerson(response As HttpResponse, request As HttpRequest) As Task 'Отправляемый в ответ клиенту объект Dim objectToSent As Object = New With {.message = "Некорректные данные"} ' значение по умолчанию Try 'получаем данные пользователя Dim user = Await request.ReadFromJsonAsync(Of Person)() If user IsNot Nothing Then 'устанавливаем id для нового пользователя user.Id = Guid.NewGuid().ToString() 'добавляем пользователя в список users.Add(user) objectToSent = user Else Throw New Exception("Некорректные данные") End If Catch ex As Exception response.StatusCode = 400 End Try Await response.WriteAsJsonAsync(objectToSent) End Function
Поскольку при извлечении данных из запроса может произойти исключение (например, в результате парсинга в JSON), оборачиваем весь код в
Try..Catch
. И в случае успешного получения данных устанавливаем у нового объекта свойство Id, добавляем его в список users и отправляем обратно клиенту.
Если приложению приходит PUT-запрос, то также с помощью метода request.ReadFromJsonAsync()
получаем отправленные клиентом данные.
Если объект найден в списке, то изменяем его данные и отправляем обратно клиенту, иначе отправляем статусный код 404:
ElseIf path = "/api/users" AndAlso request.Method = "PUT" Then Await UpdatePerson(response, request) '.............. Async Function UpdatePerson(response As HttpResponse, request As HttpRequest) As Task 'Отправляемый в ответ клиенту объект Dim objectToSent As Object = New With {.message = "Некорректные данные"} 'значение по умолчанию Try 'получаем данные пользователя Dim userData = Await request.ReadFromJsonAsync(Of Person)() If userData IsNot Nothing Then 'получаем пользователя по id Dim user = users.FirstOrDefault(Function(u) u.Id = userData.Id) 'если пользователь найден, изменяем его данные и отправляем обратно клиенту If user IsNot Nothing Then user.Age = userData.Age user.Name = userData.Name objectToSent = user Else response.StatusCode = 404 objectToSent = New With {.message = "Пользователь не найден"} End If Else Throw New Exception("Некорректные данные") End If Catch response.StatusCode = 400 End Try Await response.WriteAsJsonAsync(objectToSent) End Function
В случае, если запрос идет по другому адресу, то отправляем клиенту веб-страницу index.html, которую мы далее определим:
Else response.ContentType = "text/html; charset=utf-8" Await response.SendFileAsync("html/index.html") End If
Таким образом, мы определили простейший API. Теперь добавим код клиента.
Теперь добавим в проект папку html, в которую добавим новый файл index.html
Определим в файле index.html следующим код для взаимодействия с сервером ASP.NET Core:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>Список пользователей</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" /> </head> <body> <h2>Список пользователей</h2> <form name="userForm"> <input type="hidden" name="id" value="0" /> <div class="mb-3"> <label class="form-label" for="name">Имя:</label> <input class="form-control" name="name" /> </div> <div class="mb-3"> <label for="age" class="form-label">Возраст:</label> <input class="form-control" name="age" /> </div> <div class="mb-3"> <button type="submit" class="btn btn-sm btn-primary">Сохранить</button> <a id="reset" class="btn btn-sm btn-primary">Сбросить</a> </div> </form> <table class="table table-condensed table-striped table-bordered"> <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) { // получаем данные 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) { 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; } 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) { const user = await response.json(); reset(); 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) { const user = await response.json(); reset(); 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) { 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() { const form = document.forms["userForm"]; form.reset(); form.elements["id"].value = 0; } // создание строки для таблицы 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("a"); 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("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", e => { e.preventDefault(); reset(); }) // отправка формы document.forms["userForm"].addEventListener("submit", e => { e.preventDefault(); 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>
Основная логика здесь заключена в коде javascript. При загрузке страницы в браузере получаем все объекты из БД с помощью функции getUsers()
:
async function getUsers() { // отправляет запрос и получаем ответ const response = await fetch("/api/users", { method: "GET", headers: { "Accept": "application/json" } }); // если запрос прошел нормально if (response.ok) { // получаем данные const users = await response.json(); let 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) { 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; } else { // если произошла ошибка, получаем сообщение об ошибке const error = await response.json(); console.log(error.message); // и выводим его на консоль } }
И выделенный пользователь добавляется в форму над таблицей. Эта же форма применяется и для добавления объекта. С помощью скрытого поля, которое хранит id пользователя, мы можем узнать, какое действие выполняется - добавление или редактирование. Если id равен 0, то выполняется функция 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) { const user = await response.json(); reset(); 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) { const user = await response.json(); reset(); document.querySelector("tr[data-rowid='" + user.id + "']").replaceWith(row(user)); } else { const error = await response.json(); console.log(error.message); } }
И функция deleteUser()
посылает приложению ASP.NET Core запрос типа DELETE на удаление пользователя, и при успешном удалении на сервере
удаляет пользователя по id из таблицы пользователей.
Теперь запустим проект, и по умолчанию приложение отправит браузеру веб-страницу index.html, которая загрузит список объектов:
После этого мы сможем выполнять все базовые операции с пользователями - получение, добавление, изменение, удаление. Например, добавим нового пользователя: