Пишем сервис gRPC¶
Настало время написать сервис gRPC. Для этого возьмём наш файл country.proto и адаптируем его под реалии этой главы: добавим поля Anthem и CapitalCity, изменим поле для флага на FlagUri, уберем поля с датами и поменяем значения package и csharp_namespace.
Примечание
В этом разделе мы не будем возвращаться к тому, как добавить файл .proto к проекту. Прочитать об этом можно в главе 5.
Вот код файла protobuf.
syntax = "proto3";
option csharp_namespace = "CountryService.gRPC.v1";
package CountryService.v1;
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
service CountryService {
rpc GetAll(google.protobuf.Empty) returns (stream CountryReply) {}
rpc Get(CountryIdRequest) returns (CountryReply) {}
rpc Update(CountryUpdateRequest) returns (google.protobuf.Empty) {}
rpc Delete(CountryIdRequest) returns (google.protobuf.Empty) {}
rpc Create(stream CountryCreationRequest) returns (stream CountryCreationReply) {}
}
message CountryReply {
int32 Id = 1;
string Name = 2;
string Description = 3;
string FlagUri = 4;
string Anthem = 5;
string CapitalCity = 6;
repeated string Languages = 7;
}
message CountryIdRequest {
int32 Id = 1;
}
message CountryUpdateRequest {
int32 Id = 1;
string Description = 2;
}
message CountryCreationRequest {
string Name = 1;
string Description = 2;
string FlagUri = 3;
string Anthem = 4;
string CapitalCity = 5;
repeated int32 Languages = 7;
}
message CountryCreationReply {
int32 Id = 1;
string Name = 2;
}
А вот реализация сервиса:
namespace CountryService.gRPC.Services;
public class CountryGrpcService : CountryServiceBase
{
private readonly ICountryServices _countryService;
public CountryGrpcService(ICountryServices countryService)
{
_countryService = countryService;
}
public override async Task GetAll(Empty request, IServerStreamWriter<CountryReply> responseStream, ServerCallContext context)
{
var lst = await _countryService.GetAllAsync();
foreach (var country in lst)
{
await responseStream.WriteAsync(country.ToReply());
}
await Task.CompletedTask;
}
public override async Task<CountryReply> Get(CountryIdRequest request, ServerCallContext context)
{
var country = await _countryService.GetAsync(request.Id);
if (country == null)
throw new RpcException(new Status(StatusCode.NotFound, $"Country with Id {request.Id} hasn't been found"));
return country.ToReply();
}
public override async Task<Empty> Update(CountryUpdateRequest request, ServerCallContext context)
{
var updateSucceed = await _countryService.UpdateAsync(new UpdateCountryModel
{
Id = request.Id,
Description = request.Description,
UpdateDate = DateTime.UtcNow
});
if (!updateSucceed)
throw new RpcException(new Status(StatusCode.NotFound,
$"Country with Id {request.Id} hasn't been updated, it have probably been deleted"));
return new Empty();
}
public override async Task<Empty> Delete(CountryIdRequest request, ServerCallContext context)
{
var deleteSucceed = await _countryService.DeleteAsync(request.Id);
if (!deleteSucceed)
throw new RpcException(new Status(StatusCode.NotFound,
$"Country with Id {request.Id} hasn't been deleted, it have probably been deleted"));
return new Empty();
}
public override async Task Create(IAsyncStreamReader<CountryCreationRequest> requestStream,
IServerStreamWriter<CountryCreationReply> responseStream, ServerCallContext context)
{
await foreach (var countryToCreate in requestStream.ReadAllAsync())
{
var createdCountryId = await _countryService.CreateAsync(new CreateCountryModel
{
Name = countryToCreate.Name,
Description = countryToCreate.Description,
Anthem = countryToCreate.Anthem,
CapitalCity = countryToCreate.CapitalCity,
FlagUri = countryToCreate.FlagUri,
Languages = countryToCreate.Languages,
CreateDate = DateTime.UtcNow
});
await responseStream.WriteAsync(new CountryCreationReply
{
Id = createdCountryId,
Name = countryToCreate.Name
});
}
}
}
Метод
ToReply() — это метод расширения, реализованный в отдельном файле:namespace CountryService.gRPC.Mappers;
public static class CountryReplyMapper
{
public static CountryReply ToReply(this CountryModel country)
{
var countryReply = new CountryReply
{
Id = country.Id,
Name = country.Name,
Description = country.Description,
Anthem = country.Anthem,
CapitalCity = country.CapitalCity,
FlagUri = country.FlagUri
};
countryReply.Languages.AddRange(country.Languages);
return countryReply;
}
}
Далее, вынесем строку подключения к БД в файл appsettings.json:
"ConnectionStrings": {
"CountryService": "Server=(LocalDB)\\MSSQLLocalDB;Database=CountryService;Integrated Security=True;MultipleActiveResultSets=True"
}
Внимание!
В реальных приложениях строки подключения содержат чувствительную информацию, необходимую для подключения. Поэтому хранить её лучше в хранилище секретов. Об этом подробнее рассказано здесь.
Теперь настроим приложение, добавив провайдер сжатия, перехватчики, рефлексию gRPC, а также эндпоинты для предоставления версий Protobuf, получим следующий файл Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc(options =>
{
options.EnableDetailedErrors = true;
options.IgnoreUnknownServices = true;
options.MaxReceiveMessageSize = 6291456; // 6 Mb
options.MaxSendMessageSize = 6291456; // 6 Mb
options.CompressionProviders = new List<ICompressionProvider>
{
new BrotliCompressionProvider() // br
};
options.ResponseCompressionAlgorithm = "br";
options.ResponseCompressionLevel = CompressionLevel.Optimal;
options.Interceptors.Add<ExceptionInterceptor>();
});
builder.Services.AddGrpcReflection();
builder.Services.AddScoped<ICountryRepository, CountryRepository>();
builder.Services.AddScoped<ICountryServices, CountryServices>();
builder.Services.AddDbContext<CountryContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("CountryService")));
builder.Services.AddSingleton<ProtoService>();
var app = builder.Build();
app.MapGrpcReflectionService();
app.MapGrpcService<CountryGrpcService>();
app.MapGet("/protos", (ProtoService protoService) => Results.Ok(protoService.GetAll()));
app.MapGet("/protos/v{version:int}/{protoName}", (ProtoService protoService, int version, string protoName) =>
{
var filePath = protoService.Get(version, protoName);
return filePath != null ? Results.File(filePath) : Results.NotFound();
});
app.MapGet("/protos/v{version:int}/{protoName}/view",
async (ProtoService protoService, int version, string protoName) =>
{
var text = await protoService.ViewAsync(version, protoName);
return !string.IsNullOrEmpty(text) ? Results.Text(text) : Results.NotFound();
});
app.Run();
Дата создания : 17 июня 2023 г.