Рассмотрим взаимодействие HttpClient с веб-приложением, которое реализует Web API на примере простейшего веб-приложения ASP.NET Core
Сначала определим проект веб-приложения. Для этого создадим проект по типу ASP.NET Core Empty:
К примеру, в моем случае проект называется HttpClientTestApp. Откроем в этом проекте в файл Program.cs
и определим в нем следующий код:
int id = 1; // для генерации id объектов // начальные данные List<Person> users = new List<Person> { new() { Id = id++, Name = "Tom", Age = 37 }, new() { Id = id++, Name = "Bob", Age = 41 }, new() { Id = id++, Name = "Sam", Age = 24 } }; var builder = WebApplication.CreateBuilder(); var app = builder.Build(); app.MapGet("/api/users", () => users); app.MapGet("/api/users/{id}", (int id) => { // получаем пользователя по id Person? user = users.FirstOrDefault(u => u.Id == id); // если не найден, отправляем статусный код и сообщение об ошибке if (user == null) return Results.NotFound(new { message = "Пользователь не найден" }); // если пользователь найден, отправляем его return Results.Json(user); }); app.MapDelete("/api/users/{id}", (int id) => { // получаем пользователя по id Person? user = users.FirstOrDefault(u => u.Id == id); // если не найден, отправляем статусный код и сообщение об ошибке if (user == null) return Results.NotFound(new { message = "Пользователь не найден" }); // если пользователь найден, удаляем его users.Remove(user); return Results.Json(user); }); app.MapPost("/api/users", (Person user) => { // устанавливаем id для нового пользователя user.Id = id++; // добавляем пользователя в список 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(new { message = "Пользователь не найден" }); // если пользователь найден, изменяем его данные и отправляем обратно клиенту user.Age = userData.Age; user.Name = userData.Name; return Results.Json(user); }); app.Run(); public class Person { public int Id { get; set; } public string Name { get; set; } = ""; public int Age { get; set; } }
Более подробно создание приложения Web API на ASP.NET рассмотрено в статье Пример приложения Web API на ASP.NET. Сейчас лишь вкратце пройдемся по основным момента данного кода. Вначале определяется переменная для генерации id объектов и создается список объектов Person - те данные, с которыми будет работать пользователь::
int id = 1; var users = new List<Person> { new() { Id = id++, Name = "Tom", Age = 37 }, new() { Id = id++, Name = "Bob", Age = 41 }, new() { Id = id++, Name = "Sam", Age = 24 } };
При создании нового объекта значение переменной id увеличивается на единицу, поэтому каждый новый объект получает id на единицу больше, чем предыдущий. Для упрошения данные определены в виде обычного списка объектов, но в реальной ситуации обычно подобные данные извлекаются из какой-нибудь базы данных.
Затем с помощью методов MapGet/MapPost/MapPut/MapDelete определяется набор конечных точек, которые будут обрабатывать разные типы запросов.
Вначале добавляется конечная точка, которая обрабатывает запрос типа GET по маршруту "api/users":
app.MapGet("/api/users", () => users);
Запрос GET предполагает получение объектов, и в данном случае отправляем выше определенный список объектов Person.
Когда клиент обращается к приложению для получения одного объекта по id в запрос типа GET по адресу "api/users/{id}", то срабатывает другая конечная точка:
app.MapGet("/api/users/{id}", (int id) => { // получаем пользователя по id Person? user = users.FirstOrDefault(u => u.Id == id); // если не найден, отправляем статусный код и сообщение об ошибке if (user == null) return Results.NotFound(new { message = "Пользователь не найден" }); // если пользователь найден, отправляем его return Results.Json(user); });
Здесь через параметр id получаем из пути запроса идентификатор объекта Person и по этому идентификатору ищем нужный объект в списке users.
Если объект по Id не был найден, то возвращаем с помощью метода Results.NotFound()
статусный код 404 с некоторым сообщением в формате JSON.
Если объект найден, то с помощью метода Results.Json()
отправляет найденный объект клиенту.
При получении запроса типа DELETE по маршруту "/api/users/{id}" срабатывает другая конечная точка:
app.MapDelete("/api/users/{id}", (int id) => { // получаем пользователя по id Person? user = users.FirstOrDefault(u => u.Id == id); // если не найден, отправляем статусный код и сообщение об ошибке if (user == null) return Results.NotFound(new { message = "Пользователь не найден" }); // если пользователь найден, удаляем его users.Remove(user); return Results.Json(user); });
Здесь действует аналогичная логика - если объект по Id не найден, отправляет статусный код 404. Если же объект найден, то удаляем его из списка и посылаем клиенту.
При получении запроса с методом POST по адресу "/api/users" срабатывает следующая конечная точка:
app.MapPost("/api/users", (Person user)=>{ // устанавливаем id для нового пользователя user.Id = id++; // добавляем пользователя в список 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(new { message = "Пользователь не найден" }); // если пользователь найден, изменяем его данные и отправляем обратно клиенту user.Age = userData.Age; user.Name = userData.Name; return Results.Json(user); });
Для теста запустим этот проект и обратимся в браузере по адресу "/api/users" для получения списка данных:
Таким образом, мы определили простейший API. Теперь посмотрим, как мы можем взаимодействовать с этим кодом с помощью класса HttpClient.
Поскольку конечная точка, которая обрабатывает GET-запрос, возвращает список данных в формате JSON, то для его получения у HttpClient удобнее использовать метод GetFromJsonAsync():
using System.Net.Http.Json; class Program { static HttpClient httpClient = new HttpClient(); static async Task Main() { List<Person>? people = await httpClient.GetFromJsonAsync<List<Person>> ("https://localhost:7094/api/users"); if (people != null) { foreach (var person in people) { Console.WriteLine(person.Name); } } } } public class Person { public int Id { get; set; } public string Name { get; set; } = ""; public int Age { get; set; } }
Как видно из предыдущего скриншота, у меня приложение ASP.NET запущено по адресу "https://localhost:7094/", а адресом ресурса является "api/users", соответственно в моем случае для получения данных я обращаюсь по адресу "https://localhost:7094/api/users". Полученные данные автоматически десериализуются из JSON в список объектов Person, свойство Name которых затем выводится на консоль:
Tom Bob Sam
В ранее определенном сервере для отправки одного объекта применяется конечная точка
app.MapGet("/api/users/{id}", (int id) => { ....
То есть также надо выполнить get-запрос, только в адресную строку нам надо передать id объекта. И в данном случае мы могли бы использовать тот же метод GetFromJsonAsync()
int id = 1; // получаем объект с id=1 Person? person = await httpClient.GetFromJsonAsync<Person>($"https://localhost:7094/api/users/{id}");
Однако, что если мы отправим id несуществующего объекта. В этом случае объект person будет равен null. Однако сервер в этом случае посылает нам соответствующий статусный код и
сообщение об ошибке. И было бы неплохо иметь возможность в случае проблемы также использовать эти данные. В этом случае мы можем отправить запрос с помощью метода GetAsync()
и
получить ответ в виде HttpResponseMessage. И в зависимости от статусного кода десериализовать ответ в один из типов данных:
using System.Net; using System.Net.Http.Json; class Program { static HttpClient httpClient = new HttpClient(); static async Task Main() { // id первого объекта int id = 1; using var response = await httpClient.GetAsync($"https://localhost:7094/api/users/{id}"); // если объект на сервере найден, то есть статусный код равен 404 if (response.StatusCode == HttpStatusCode.NotFound) { Error? error = await response.Content.ReadFromJsonAsync<Error>(); Console.WriteLine(error?.Message); } else if (response.StatusCode == HttpStatusCode.OK) { // считываем ответ Person? person = await response.Content.ReadFromJsonAsync<Person>(); Console.WriteLine($"{person?.Id} - {person?.Name}"); } } } record Error(string Message); class Person { public int Id { get; set;} public string Name { get; set; } = ""; public int Age { get; set; } }
Добавление данных на сервере обрабатывается конечной точкой
app.MapPost("/api/users", (Person user) =>
То есть нам надо в запросе POST послать данные, которые соответствуют объекту Person. Для отправки данных на HttpClient можно использовать метод PostAsync(), либо PostAsJsonAsync(), в который передается адрес ресурса и сами добавляемые данные. Поскольку в данном случае веб-приложение ожидает данные в формате JSON, то проще воспользоваться методом PostAsJsonAsync():
using System.Net.Http.Json; class Program { static HttpClient httpClient = new HttpClient(); static async Task Main() { // отправляемый объект var mike = new Person { Name = "Mike", Age = 31 }; using var response = await httpClient.PostAsJsonAsync("https://localhost:7094/api/users/", mike); // считываем ответ и десериализуем данные в объект Person Person? person = await response.Content.ReadFromJsonAsync<Person>(); Console.WriteLine($"{person?.Id} - {person?.Name}"); } } class Person { public int Id { get; set;} public string Name { get; set; } = ""; public int Age { get; set; } }
Для изменения данных мы должны отправить данные следующей конечной точке:
app.MapPut("/api/users", (Person userData) => { ...
Для отправки измененных данных в запросе типа PUT можно использовать метод PutAsync(), либо PutAsJsonAsync(), в которые передаются адрес ресурса и сами добавляемые данные. Поскольку в данном случае веб-приложение ожидает данные в формате JSON, то проще воспользоваться методом PutAsJsonAsync():
using System.Net.Http.Json; class Program { static HttpClient httpClient = new HttpClient(); static async Task Main() { // id изменяемого объекта int id = 1; // отправляемый объект var tom = new Person { Id = id, Name = "Tomas", Age = 38 }; using var response = await httpClient.PutAsJsonAsync("https://localhost:7094/api/users/", tom); if (response.StatusCode == System.Net.HttpStatusCode.NotFound) { // если возникла ошибка, считываем сообщение об ошибке Error? error = await response.Content.ReadFromJsonAsync<Error>(); Console.WriteLine(error?.Message); } else if (response.StatusCode == System.Net.HttpStatusCode.OK) { // десериализуем ответ в объект Person Person? person = await response.Content.ReadFromJsonAsync<Person>(); Console.WriteLine($"{person?.Id} - {person?.Name} ({person?.Age})"); } } } record Error(string Message); class Person { public int Id { get; set;} public string Name { get; set; } = ""; public int Age { get; set; } }
За удаление данных на сервере отвечает конечная точка
app.MapDelete("/api/users/{id}", (string id) => ..
То есть в Delete-запросе нам надо передать id удаляемого объекта. Для этого у HttpClient можно использовать метод DeleteFromJsonAsync(), который тиизируется типом удаляемого объекта, получает адрес удаляемого ресурса и возвращает удаленный объект:
using System.Net.Http.Json; class Program { static HttpClient httpClient = new HttpClient(); static async Task Main() { // id удаляемого объекта int id = 1; Person? person = await httpClient.DeleteFromJsonAsync<Person>($"https://localhost:7094/api/users/{id}"); Console.WriteLine($"{person?.Id} - {person?.Name} ({person?.Age})"); } } class Person { public int Id { get; set;} public string Name { get; set; } = ""; public int Age { get; set; } }
Однако если объекта с переданным id не окажется на сервере, веб-приложение возвратит статусный код 404 и сообщение об ошибке. Чтобы отловить эту ситуацию, можно использовать метод DeleteAsync(), который возвращает объект HttpResponseMessage:
using System.Net.Http.Json; class Program { static HttpClient httpClient = new HttpClient(); static async Task Main() { // id удаляемого объекта int id = 1; using var response = await httpClient.DeleteAsync($"https://localhost:7094/api/users/{id}"); if (response.StatusCode == System.Net.HttpStatusCode.NotFound) { // если возникла ошибка, считываем сообщение об ошибке Error? error = await response.Content.ReadFromJsonAsync<Error>(); Console.WriteLine(error?.Message); } else if (response.StatusCode == System.Net.HttpStatusCode.OK) { // десериализуем ответ в объект Person Person? person = await response.Content.ReadFromJsonAsync<Person>(); Console.WriteLine($"{person?.Id} - {person?.Name} ({person?.Age})"); } } } record Error(string Message); class Person { public int Id { get; set;} public string Name { get; set; } = ""; public int Age { get; set; } }