冠军

导航

eShopOnContainer 中 Grpc 服务定义与实现

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>

这里设置了 GrpcServicesClinet,所以,将会生成名称为 GrpcBasket.Basket.BasketClient 的类型。在 Web.Shopping.HttpAggregator.csprojMobile.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 来优化数据传输对象的转换。

posted on 2023-03-17 20:51  冠军  阅读(62)  评论(0编辑  收藏  举报