eShopOnContainer 中 Grpc 服务定义与实现
服务于前端的后端 (BFF) 模式是 API 网关模式的一种变形,针对外部使用者的不同需求,为每种不同的客户端使用者提供一种后端 API,形成服务统一优雅的服务边界。
边界层封装内部服务的复杂性,为外部调用提供统一的外观表示。不同的外部使用者有着不同的使用需求,所以边界层的封装也会由于外部使用者的多样化,而出现多种形式。
Web.Shopping.HttpAggregator 是对内部服务进行封装之后,提供给 Web 层使用。
而 Mobile.Shopping.HttpAggregator 对内部服务进行封装之后,提供给 SPA 单页应用使用。
在 BFF 与实际的后端服务之间,使用了 Grpc 通讯协议来提高通讯效率。通讯协议的实际定义位于微服务中,在 BFF 中,通过引用 Grpc 协议定义文件来生成访问服务的 Grpc 客户端。而在微服务中,也通过这个通讯协议定义文件来完成实际的服务实现。最终完成 BFF 与微服务之间的通讯实现。
在下面的项目文件中,引入了 Grpc 的定义文件:
- src\ApiGateways\Web.Bff.Shopping\aggregator\Web.Shopping.HttpAggregator.csproj
- src\ApiGateways\Mobile.Bff.Shopping\aggregator\Mobile.Shopping.HttpAggregator.csproj
<ItemGroup>
<Protobuf Include="..\..\..\Services\Basket\Basket.API\Proto\basket.proto" GrpcServices="Client" />
<Protobuf Include="..\..\..\Services\Catalog\Catalog.API\Proto\catalog.proto" GrpcServices="Client" />
<Protobuf Include="..\..\..\Services\Ordering\Ordering.API\Proto\ordering.proto" GrpcServices="Client" />
</ItemGroup>
为了支持 Grpc,项目文件中引用了 Grpc 相关的 NuGet 包:
<PackageReference Include="Google.Protobuf" Version="3.15.0" />
<PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" Version="2.34.0" />
<PackageReference Include="Grpc.Core" Version="2.34.0" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.34.0" />
<PackageReference Include="Grpc.Tools" Version="2.34.0" PrivateAssets="All" />
以 src\Services\Basket\Basket.API\Proto\basket.proto
为例,其定义如下:
syntax = "proto3";
option csharp_namespace = "GrpcBasket";
package BasketApi;
service Basket {
rpc GetBasketById(BasketRequest) returns (CustomerBasketResponse) {}
rpc UpdateBasket(CustomerBasketRequest) returns (CustomerBasketResponse) {}
}
message BasketRequest {
string id = 1;
}
message CustomerBasketRequest {
string buyerid = 1;
repeated BasketItemResponse items = 2;
}
message CustomerBasketResponse {
string buyerid = 1;
repeated BasketItemResponse items = 2;
}
message BasketItemResponse {
string id = 1;
int32 productid = 2;
string productname = 3;
double unitprice = 4;
double oldunitprice = 5;
int32 quantity = 6;
string pictureurl = 7;
}
在 .NET 中,会忽略 package 语句,而使用 csharp_namespace 中定义的命名空间。
实现 Basket 服务端 Grpc 服务
在 Basket.API 项目中,定义了如下内容:
<ItemGroup>
<Protobuf Include="Proto\basket.proto" GrpcServices="Server" Generator="MSBuild:Compile" />
<Content Include="@(Protobuf)" />
<None Remove="@(Protobuf)" />
</ItemGroup>
其中的 GrpcServices
特性用来限制 C# 代码生成。 有效 GrpcServices 选项如下:
- 同时生成服务端定义和客户端代码(如果不存在 GrpcServices 属性定义,则为默认值)
- Server,只生成服务端定义,服务器端抽象接口定义会自动添加
Base
后缀 - Client,只生成客户端代码,客户端定义会自动添加
Client
后缀 - None,不生成
由于这里定义了 Server
,所以,在服务器端仅仅生成了服务定义的抽象接口 Basket.BasketBase
。
在 src\Services\Basket\Basket.API\Grpc\BasketService.cs 中,实现了 Grpc 服务端。
namespace GrpcBasket;
public class BasketService : Basket.BasketBase
{
private readonly IBasketRepository _repository;
private readonly ILogger<BasketService> _logger;
public BasketService(IBasketRepository repository, ILogger<BasketService> logger)
{
_repository = repository;
_logger = logger;
}
[AllowAnonymous]
public override async Task<CustomerBasketResponse> GetBasketById(BasketRequest request, ServerCallContext context)
服务端作为 ASP.NET Core,配置 Grpc 支持。
public virtual IServiceProvider ConfigureServices(IServiceCollection services)
{
// ...
services.AddGrpc(options =>
{
options.EnableDetailedErrors = true;
});
// ...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
// ...
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<BasketService>();
// ...
}
使用 Basket 客户端
在 Web.Bff.Shopping/aggregator 项目中,也包含了 Grpc 定义的 protobuf 定义。
<ItemGroup>
<Protobuf Include="..\..\..\Services\Basket\Basket.API\Proto\basket.proto" GrpcServices="Client" />
<Protobuf Include="..\..\..\Services\Catalog\Catalog.API\Proto\catalog.proto" GrpcServices="Client" />
<Protobuf Include="..\..\..\Services\Ordering\Ordering.API\Proto\ordering.proto" GrpcServices="Client" />
</ItemGroup>
这里设置了 GrpcServices
为 Clinet
,所以,将会生成名称为 GrpcBasket.Basket.BasketClient
的类型。在 Web.Shopping.HttpAggregator.csproj
和 Mobile.Shopping.HttpAggregator.csproj
两个项目的 Startup.cs 中,可以看到对该类型的使用。
// Basket
public static IServiceCollection AddGrpcServices(this IServiceCollection services)
{
// grpc 的异常处理器
services.AddTransient<GrpcExceptionInterceptor>();
// basket
services.AddScoped<IBasketService, BasketService>();
services.AddGrpcClient<Basket.BasketClient>((services, options) =>
{
var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket;
options.Address = new Uri(basketApi);
}).AddInterceptor<GrpcExceptionInterceptor>();
// catalog
services.AddScoped<ICatalogService, CatalogService>();
services.AddGrpcClient<Catalog.CatalogClient>((services, options) =>
{
var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog;
options.Address = new Uri(catalogApi);
}).AddInterceptor<GrpcExceptionInterceptor>();
// ordering
services.AddScoped<IOrderingService, OrderingService>();
services.AddGrpcClient<OrderingGrpc.OrderingGrpcClient>((services, options) =>
{
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering;
options.Address = new Uri(orderingApi);
}).AddInterceptor<GrpcExceptionInterceptor>();
return services;
};
而自定义的 BasketService
则封装了生成的 BasketClient
实现,其中主要完成了客户端数据表示到服务端数据表示的转换工作。
public class BasketService : IBasketService
{
private readonly Basket.BasketClient _basketClient;
private readonly ILogger<BasketService> _logger;
public BasketService(Basket.BasketClient basketClient, ILogger<BasketService> logger)
{
_basketClient = basketClient;
_logger = logger;
}
public async Task<BasketData> GetByIdAsync(string id)
{
_logger.LogDebug("grpc client created, request = {@id}", id);
var response = await _basketClient.GetBasketByIdAsync(new BasketRequest { Id = id });
_logger.LogDebug("grpc response {@response}", response);
return MapToBasketData(response);
}
注:这里似乎可以考虑使用 AutoMapper 来优化数据传输对象的转换。