Рассмотрим взаимодействие проекта Blazor с простейшим сервисом Web API, при котором приложение на Blazor реализует простейший CRUD. Для этого определим следующий проект:
В файле Program.cs определим следующий код программы:
using BlazorApp.Components; // начальные данные List<Person> users = [ 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(args); builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); builder.Services.AddHttpClient(); var app = builder.Build(); app.UseAntiforgery(); app.MapGet("/api/users", () => users); app.MapGet("/api/users/{id}", (string id) => { // получаем пользователя по id Person? user = users.FirstOrDefault(u => u.Id == id); // если не найден, отправляем статусный код и сообщение об ошибке if (user == null) return Results.NotFound("Пользователь не найден"); // если пользователь найден, отправляем его return Results.Json(user); }); app.MapDelete("/api/users/{id}", (string id) => { // получаем пользователя по id Person? user = users.FirstOrDefault(u => u.Id == id); // если не найден, отправляем статусный код и сообщение об ошибке if (user == null) return Results.NotFound("Пользователь не найден"); // если пользователь найден, удаляем его users.Remove(user); return Results.Json(user); }); app.MapPost("/api/users", (Person user) => { // устанавливаем id для нового пользователя user.Id = Guid.NewGuid().ToString(); // добавляем пользователя в список users.Add(user); return user; }); app.MapPut("/api/users", (Person userData) => { // получаем пользователя по id var user = users.FirstOrDefault(u => u.Id == userData.Id); // если не найден, отправляем статусный код и сообщение об ошибке if (user == null) return Results.NotFound("Пользователь не найден"); // если пользователь найден, изменяем его данные и отправляем обратно клиенту user.Age = userData.Age; user.Name = userData.Name; return Results.Json(user); }); app.MapRazorComponents<App>() .AddInteractiveServerRenderMode(); app.Run(); 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".
Для упрошения данные определены в виде обычного списка объектов, но в реальной ситуации обычно подобные данные извлекаются из какой-нибудь базы данных.
Затем с помощью методов MapGet/MapPost/MapPut/MapDelete определяется набор конечных точек, которые будут обрабатывать разные типы запросов.
Вначале добавляется конечная точка, которая обрабатывает запрос типа GET по маршруту "api/users":
app.MapGet("/api/users", () => users);
Запрос GET предполагает получение объектов, и в данном случае отправляем выше определенный список объектов Person.
Когда клиент обращается к приложению для получения одного объекта по id в запрос типа GET по адресу "api/users/{id}", то срабатывает другая конечная точка:
app.MapGet("/api/users/{id}", (string id) => { // получаем пользователя по id Person? user = users.FirstOrDefault(u => u.Id == id); // если не найден, отправляем статусный код и сообщение об ошибке if (user == null) return Results.NotFound("Пользователь не найден"); // если пользователь найден, отправляем его return Results.Json(user); });
Здесь через параметр id получаем из пути запроса идентификатор объекта Person и по этому идентификатору ищем нужный объект в списке users.
Если объект по Id не был найден, то возвращаем с помощью метода Results.NotFound()
статусный код 404 с некоторым сообщением.
Если объект найден, то с помощью метода Results.Json()
отправляет найденный объект клиенту.
При получении запроса типа DELETE по маршруту "/api/users/{id}" срабатывает другая конечная точка:
app.MapDelete("/api/users/{id}", (string id) => { // получаем пользователя по id Person? user = users.FirstOrDefault(u => u.Id == id); // если не найден, отправляем статусный код и сообщение об ошибке if (user == null) return Results.NotFound("Пользователь не найден"); // если пользователь найден, удаляем его users.Remove(user); return Results.Json(user); });
Здесь действует аналогичная логика - если объект по Id не найден, отправляет статусный код 404. Если же объект найден, то удаляем его из списка и посылаем клиенту.
При получении запроса с методом POST по адресу "/api/users" срабатывает следующая конечная точка:
app.MapPost("/api/users", (Person user)=>{ // устанавливаем id для нового пользователя user.Id = Guid.NewGuid().ToString(); // добавляем пользователя в список users.Add(user); return user; });
Запрос типа POST предполагает передачу приложению отправляемых данных. Причем мы ожидаем, что клиент отправит данные, которые соответствуют определению типа Person. И поэтому инфраструктура ASP.NET Core сможет автоматически собрать из них объект Person. И этот объект мы сможем получить в качестве параметра в обработчике конечной точки.
После получения данных устанавливаем у нового объекта свойство Id, добавляем его в список users и отправляем обратно клиенту.
Если приложению приходит PUT-запрос по адресу "/api/users", то аналогичным образом получаем отправленные клиентом данные в виде объекта Person и пытаемся найти подобный объект в списке users. Если объект не найден, отправляем статусный код 404. Если объект найден, то изменяем его данные и отправляем обратно клиенту:
app.MapPut("/api/users", (Person userData) => { // получаем пользователя по id var user = users.FirstOrDefault(u => u.Id == userData.Id); // если не найден, отправляем статусный код и сообщение об ошибке if (user == null) return Results.NotFound("Пользователь не найден"); // если пользователь найден, изменяем его данные и отправляем обратно клиенту user.Age = userData.Age; user.Name = userData.Name; return Results.Json(user); });
Таким образом, мы определили простейший API. В качестве клиента в проекте в папке Components определен компонент App:
@page "/" <!DOCTYPE html> <html> <head> <title>METANIT.COM</title> <meta charset="utf-8" /> </head> <body> <Home /> <script src="_framework/blazor.web.js"></script> </body> </html>
Здесь просто загружается компонент Home, где и определена основная логика взаимодействия с веб-сервисом:
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Web @using System.Net.Http.Json @rendermode RenderMode.InteractiveServer @inject IHttpClientFactory ClientFactory <h2>Список пользователей</h2> <div> <p> Имя:<br /> <input @bind-value="person.Name" /> </p> <p> Возраст:<br /> <input type="number" @bind-value="person.Age" /> </p> <p> <button @onclick="Submit">Сохранить</button> </p> </div> <table> <thead><tr><th>Имя</th><th>Возраст</th><th></th></tr></thead> <tbody> @foreach (var person in people) { <tr> <td>@person.Name</td> <td>@person.Age</td> <td> <button @onclick="()=>Edit(person)">Изменить</button> <button @onclick="@(async ()=> await Delete(person.Id))">Удалить</button> </td> </tr> } </tbody> </table> @code { List<Person> people = []; Person person = new(); HttpClient httpClient = null!; protected override async Task OnInitializedAsync() { httpClient = ClientFactory.CreateClient(); httpClient.BaseAddress = new Uri("https://localhost:7134/api/users"); await LoadData(); } async Task LoadData() { people = await httpClient.GetFromJsonAsync<List<Person>>(httpClient.BaseAddress) ?? people; } // устанавливаем редактируемый объект void Edit(Person p) { person.Id = p.Id; person.Name = p.Name; person.Age = p.Age; } async Task Submit() { // если id не установлен, то метод добавляем объект if (string.IsNullOrWhiteSpace(person.Id)) await httpClient.PostAsJsonAsync(httpClient.BaseAddress, person); else // иначе обновляем объект await httpClient.PutAsJsonAsync(httpClient.BaseAddress, person); // сбрасываем значения person.Id = ""; person.Name = ""; person.Age = 0; await LoadData(); } // удаление объекта async Task Delete(string id) { await httpClient.DeleteAsync($"{httpClient.BaseAddress}/{id}"); await LoadData(); } class Person { public string Id { get; set; } = ""; public string Name { get; set; } = ""; public int Age { get; set; } } }
В коде компонента определяем три переменных:
List<Person> people = []; Person person = new(); HttpClient httpClient = null!;
Список people представляет список загруженных с сервера данные, а переменная person представляет добавляемый или обновляемый объект.
В методе OnInitializedAsync()
при инициализации компонента создаем объект HttpClient, устанавливаем базовый адрес и с помощью метода LoadData загружаем данные с сервера:
protected override async Task OnInitializedAsync() { httpClient = ClientFactory.CreateClient(); httpClient.BaseAddress = new Uri("https://localhost:7134/api/users"); await LoadData(); } async Task LoadData() { people = await httpClient.GetFromJsonAsync<List<Person>>(httpClient.BaseAddress) ?? people; }
Чтобы после запуска приложения данные уже были загружены, переопределяем метод OnInitializedAsync()
и в нем вызываем метод LoadData.
Загруженный список выводится в компоненте в таблицу:
@foreach (var person in people) { <tr> <td>@person.Name</td> <td>@person.Age</td> <td> <button @onclick="()=>Edit(person)">Изменить</button> <button @onclick="@(async ()=> await Delete(person.Id))">Удалить</button> </td> </tr> }
В таблице для каждого объекта определяются две кнопки. При нажатии на первую кнопку - кнопку изменения срабатывает метод Edit
, который просто изменяет
значения свойств объекта person:
void Edit(Person p) { person.Id = p.Id; person.Name = p.Name; person.Age = p.Age; }
При нажатии на вторую кнопку - кнопку удаления срабатывает метод Delete, который отправляет запрос DELETE на сервер, а потом перезагружает данные:
async Task Delete(string id) { await httpClient.DeleteAsync($"api/users/{id}"); await LoadData(); }
Компонент определяет форму, на которой элементы ввода привязаны к переменной person - это будет либо добавляемые, либо обновляемые данные.
<div> <p> Имя:<br /> <input @bind-value="person.Name" /> </p> <p> Возраст:<br /> <input type="number" @bind-value="person.Age" /> </p> <p> <button @onclick="Submit">Сохранить</button> </p> </div>
По нажатию на кнопку срабатывает метод Submit:
async Task Submit() { if (string.IsNullOrWhiteSpace(person.Id)) await httpClient.PostAsJsonAsync("api/users", person); else await httpClient.PutAsJsonAsync($"api/users", person); person.Id = ""; person.Name = ""; person.Age = 0; await LoadData(); }
Если ранее была нажата кнопка изменения объекта, и сработал метод Edit, то у объекта person свойство Id будет представлять непустую строку. При добавлении это свойство равнои пустой строки. Соответственно по свойству Id мы можем узнать, идет речь об измении или добавлении объекта, и в зависимости от этого выполнить определенный запрос на сервер. После выполнения запроса сбрасываем значения объекта person и перезагружаем данные.