Работа с данными в стиле CRUD, в частности, добавление, получение, обновление и удаление данных, является достаточно распростренной задачей веб-приложений. И приложении на grpc также можно определить функционал для работы с данными. Рассмотрим, как это сделать.
Вначале создадим проект сервиса по типу ASP.NET Core gRPC Service. В папке Protos определим файл crud.proto со следующим содержимым:
syntax = "proto3"; package metanit; import "google/protobuf/empty.proto"; service UserService{ rpc ListUsers(google.protobuf.Empty) returns (ListReply); rpc GetUser(GetUserRequest) returns (UserReply); rpc CreateUser(CreateUserRequest) returns (UserReply); rpc UpdateUser(UpdateUserRequest) returns (UserReply); rpc DeleteUser(DeleteUserRequest) returns (UserReply); } message CreateUserRequest{ string name=1; int32 age = 2; } message GetUserRequest{ int32 id =1; } message UpdateUserRequest{ int32 id=1; string name=2; int32 age = 3; } message DeleteUserRequest{ int32 id =1; } message ListReply{ repeated UserReply Users = 1; } message UserReply{ int32 id = 1; string name=2; int32 age = 3; }
Здесь определен сервис UserService
, который будет получать данные от клиента. В сервисе определен ряд методов:
ListUsers
: предназначен для отправки списка объектов из условной бд. Он получает в качестве параметра пустой ответ. Для этого используем специально определенный тип
google.protobuf.Empty
из пакета google/protobuf/empty.proto
, который в начале файла импортируется.
GetUser
: предназначен для отправки одного объекта. В качестве параметра он получает сообщение GetUserRequest
, которое содержит id запрошенного объекта.
CreateUser
: для добавления объекта. В качестве параметра он получает сообщение CreateUserRequest
, которое содержит добавляемые данные
в виде строки name
и числа age
. И возвращает созданный объект.
UpdateUser
: предназначен для обновления объекта. В качестве параметра он получает сообщение UpdateUserRequest
с новыми данными. В качестве результата возвращает
новое состояние объекта
DeleteUser
: предназначен для удаления объекта. В качестве параметра он получает сообщение DeleteUserRequest
, которое содержит id удаляемого объекта.
В качестве результата возвращает удаленный объект.
В качестве результата почти все методы возвращают объект UserReply
, которое представляет сами данные:
message UserReply{ int32 id = 1; string name=2; int32 age = 3; }
И метод ListUsers
объект, которое по сути представляет набор объектов UserReply:
message ListReply{ repeated UserReply Users = 1; }
Оператор repeated указывает, что это будет набор объектов типа UserReply.
Далее в проекте в папке Services определим класс сервиса, который назовем UserApiService и который будет иметь следующий код:
using Google.Protobuf.WellKnownTypes; using Grpc.Core; using Metanit; // пространство имен сервиса UserService.UserServiceBase namespace CrudGrpcApp.Services; public class UserApiService : UserService.UserServiceBase { static int id = 0; // счетчик для генерации id // условная база данных static List<User> users = new() { new User(++id, "Tom", 38), new User(++id, "Bob", 42) }; // отправляем список пользователей public override Task<ListReply> ListUsers(Empty request, ServerCallContext context) { var listReply = new ListReply(); // определяем список // преобразуем каждый объект из списка users в объект UserReply var userList = users.Select(item => new UserReply { Id = item.Id, Name = item.Name, Age = item.Age }).ToList(); listReply.Users.AddRange(userList); return Task.FromResult(listReply); } // отправляем одного пользователя по id public override Task<UserReply> GetUser(GetUserRequest request, ServerCallContext context) { var user = users.Find(u => u.Id == request.Id); // если пользователь не найден, генерируем исключение if (user == null) { throw new RpcException(new Status(StatusCode.NotFound, "User not found")); } UserReply userReply = new UserReply() { Id = user.Id, Name = user.Name, Age = user.Age }; return Task.FromResult(userReply); } // добавление пользователя public override Task<UserReply> CreateUser(CreateUserRequest request, ServerCallContext context) { // формируем из данных объект User и добавляем его в список users var user = new User(++id, request.Name, request.Age); users.Add(user); var reply = new UserReply() { Id = user.Id, Name = user.Name, Age = user.Age }; return Task.FromResult(reply); } // обновление пользователя public override Task<UserReply> UpdateUser(UpdateUserRequest request, ServerCallContext context) { var user = users.Find(u => u.Id == request.Id); if (user == null) { throw new RpcException(new Status(StatusCode.NotFound, "User not found")); } // обновляем даннные user.Name = request.Name; user.Age = request.Age; var reply = new UserReply() { Id = user.Id, Name = user.Name, Age = user.Age }; return Task.FromResult(reply); } // удаление пользователя public override Task<UserReply> DeleteUser(DeleteUserRequest request, ServerCallContext context) { var user = users.Find(u => u.Id == request.Id); if (user == null) { throw new RpcException(new Status(StatusCode.NotFound, "User not found")); } users.Remove(user); var reply = new UserReply() { Id = user.Id, Name = user.Name, Age = user.Age }; return Task.FromResult(reply); } } // модель пользователя - класс User class User { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } public User(int id, string name, int age) { Id = id; Name = name; Age = age; } }
В качестве модели данных здесь выступает класс User
с тремя свойствами, который представляет условного пользователя. В классе UserApiService
для простоты имитируем базу данных
в виде статического списка users
с двумя начальными объектами.
static int id = 0; // счетчик для генерации id static List<User> users = new() { new User(++id, "Tom", 38), new User(++id, "Bob", 42) };
Для генерации идентификаторов определена вспомогательная статическая переменная id.
В методе ListUsers
возвращаем список объектов:
public override Task<ListReply> ListUsers(Empty request, ServerCallContext context) { var listReply = new ListReply(); var userList = users.Select(item => new UserReply { Id = item.Id, Name = item.Name, Age = item.Age }).ToList(); listReply.Users.AddRange(userList); return Task.FromResult(listReply); }
В качестве параметра получаем пустой ответ, для которого используем специальный тип Google.Protobuf.WellKnownTypes.Empty
. В самом методе определяем объект ListReply,
в его свойство Users
передаем все объекты из списка users, преобразуя их в тип UserReply. И отправляем сформированный объект ListReply клиенту.
В методе GetUser
клиенту отправляем одного пользователя:
public override Task<UserReply> GetUser(GetUserRequest request, ServerCallContext context) { var user = users.Find(u => u.Id == request.Id); if (user == null) { throw new RpcException(new Status(StatusCode.NotFound, "User not found")); } UserReply userReply = new UserReply() { Id = user.Id, Name = user.Name, Age = user.Age }; return Task.FromResult(userReply); }
Из параметра request получаем id запрошенного пользователя и ищем соответствующий объект в списке users. Вполне возможно, что пользователь с указанным id не будет найден. В этом случае генерируем исключение RpcException. В конструктор исключения мы можем передать объект Status, в котором с помощью свойства StatusCode можно указать статусный код, аналогично тому, как это делается для установки статусных кодов в http. Кроме того, можно указать строку статуса с указанием причины ошибки. И если будет сгенерировано исключение, то клиент получит эти данные.
Если же пользователь найден в списке users, то преобразуем его в тип UserReply и отправляем клиенту.
Остальные методы однотипны. Метод CreateUser
добавляет одного пользователя в список. Метод UpdateUser
по полученным данным обновляет определенного пользователя.
И метод DeleteUser
удаляет по полученному id пользователя из списка users. Во всех случаях результат метода отправляется пользователю.
В конце включим сервис UserApiService в конвейер обработки запроса в файле Program.cs:
using CrudGrpcApp.Services; // пространство имен сервиса UserApiService var builder = WebApplication.CreateBuilder(args); builder.Services.AddGrpc(); var app = builder.Build(); app.MapGrpcService<UserApiService>(); app.MapGet("/", () => "Hello METANIT.COM"); app.Run();
То есть получится следующий проект:
Для тестирования сервиса определим проект консольного приложения. Добавим в него через Nuget все нужные пакеты:
Grpc.Net.Client
Google.Protobuf
Grpc.Tools
Далее создадим в проекте консольного приложения новую папку Protos и в нее скопируем из проекта сервиса файл crud.proto
Далее нам надо отредактировать файл проекта с расширением csproj, который называется по имени проекта - узел <Project>
добавим следующий элемент:
<ItemGroup> <Protobuf Include="Protos\crud.proto" GrpcServices="Client" /> </ItemGroup>
Для получения списка пользователей в файле Program.cs определим следующий код:
using Grpc.Net.Client; using Metanit; // создаем канал для обмена сообщениями с сервером // параметр - адрес сервера gRPC using var channel = GrpcChannel.ForAddress("https://localhost:7138"); // создаем клиент var client = new UserService.UserServiceClient(channel); // получение списка ListReply users = await client.ListUsersAsync(new Google.Protobuf.WellKnownTypes.Empty()); foreach (var user in users.Users) { Console.WriteLine($"{user.Id}. {user.Name} - {user.Age}"); }
Поскольку сервер возвращает объект ListReply, то cоответственно на клиенте мы получаем этот объект и с помощью его свойства Users можем получить все данные пользователя в виде объекта UserReply.
Для тестирования получения одного объекта по id определим следующий код:
using Grpc.Net.Client; using Metanit; using var channel = GrpcChannel.ForAddress("https://localhost:7138"); var client = new UserService.UserServiceClient(channel); // получение одного объекта по id = 1 UserReply user = await client.GetUserAsync(new GetUserRequest { Id = 1 }); Console.WriteLine($"{user.Id}. {user.Name} - {user.Age}");
Однако, если пользователя по определенному id не окажется, то сервер генерирует исключение RpcException, отправляет клиенту соответствующий статус. Если на стороне сервера генерируется исключение, то на клиенте тоже автоматически генерируется исключение. И в этом случае, чтобы программа не завершилась аварийно, мы можем обработь исключение, получив статус ответа:
using Grpc.Core; using Grpc.Net.Client; using Metanit; using var channel = GrpcChannel.ForAddress("https://localhost:7138"); var client = new UserService.UserServiceClient(channel); try { // получение одного объекта по id = 4 UserReply user = await client.GetUserAsync(new GetUserRequest { Id = 4 }); Console.WriteLine($"{user.Id}. {user.Name} - {user.Age}"); } catch (RpcException ex) { Console.WriteLine(ex.Status.Detail); // получаем статус ответа }
Создание нового объекта:
using Grpc.Core; using Grpc.Net.Client; using Metanit; using var channel = GrpcChannel.ForAddress("https://localhost:7138"); var client = new UserService.UserServiceClient(channel); // добавление одного объекта UserReply user = await client.CreateUserAsync(new CreateUserRequest { Name = "Sam", Age = 28 }); Console.WriteLine($"{user.Id}. {user.Name} - {user.Age}");
Обновление объекта
using Grpc.Core; using Grpc.Net.Client; using Metanit; using var channel = GrpcChannel.ForAddress("https://localhost:7138"); var client = new UserService.UserServiceClient(channel); try { //обновление одного объекта - изменим имя у объекта с id = 1 на Tomas UserReply user = await client.UpdateUserAsync(new UpdateUserRequest { Id = 1, Name = "Tomas", Age = 38 }); Console.WriteLine($"{user.Id}. {user.Name} - {user.Age}"); } catch (RpcException ex) { Console.WriteLine(ex.Status.Detail); }
И тестирование удаления объекта:
using Grpc.Core; using Grpc.Net.Client; using Metanit; using var channel = GrpcChannel.ForAddress("https://localhost:7138"); var client = new UserService.UserServiceClient(channel); try { // удаление объекта с id = 2 UserReply user = await client.DeleteUserAsync(new DeleteUserRequest { Id= 2 }); Console.WriteLine($"{user.Id}. {user.Name} - {user.Age}"); } catch (RpcException ex) { Console.WriteLine(ex.Status.Detail); }