ASP.NET Core 3.x 入门(九)gRPC in ASP.NET Core 3.x

此入门教程是记录下方参考资料视频的学习过程
开发工具:Visual Studio 2019

参考资料:https://www.bilibili.com/video/BV1c441167KQ
API文档:https://docs.microsoft.com/zh-cn/dotnet/api/?view=aspnetcore-3.1

目录

ASP.NET Core 3.x 入门(一)创建项目 和 部署

ASP.NET Core 3.x 入门(二)建立 Controller ,使用 Tag Helper

ASP.NET Core 3.x 入门(三)View Component

ASP.NET Core 3.x 入门(四)Razor Page

ASP.NET Core 3.x 入门(五)SignalR

ASP.NET Core 3.x 入门(六)Blazor

ASP.NET Core 3.x 入门(七)Web API

ASP.NET Core 3.x 入门(八)gRPC - Protocol Buffer

ASP.NET Core 3.x 入门(九)gRPC in ASP.NET Core 3.x

gRPC

  • Remote Procedure Call 远程过程调用
  • 性能好
  • Protocol Buffer,二进制的串行化格式,使用 HTTP/2 传输
  • 支持全双工流
  • 与语言和平台无关
  • 约定为先的开发模式(.proto文件)

新建项目

Visual Studio 有自带的 ASP.NET Core 的 gRPC 模板,但是这里我们还是使用 ASP.NET Core 3.x 的空模板
我的项目命名是 My_gRPC_Api ,不勾选 Https 和 Docker
项目还缺一个库,通过 NuGet 安装,Grpc.AspNetCore
将之前 Web API 项目的 Models 、Services 和 Repositories 复制到当前项目下,注意替换命名空间

proto 生成 C# 代码

项目下新建一个 Protos 文件夹,在里面新建 proto 文件,为了防止名称冲突,所以我新建了多个 proto 文件
grpcapi.proto

syntax = "proto3";

option csharp_namespace = "My_gRPC_Api";

package API;

// IDepartmentRepository

//对应 Models 中的 Department 类
message Department {
  int32 id = 1;
  string name = 2;
  string location = 3;
  int32 employeeCount = 4;
}

//对应 Repositories 中的 IDepartmentRepository
service Departments {
  //对应 Task<IEnumerable<Department>> GetAll() 方法
  rpc GetAll(GetAllDepartmentsRequest) returns (GetAllDepartmentsResponse) {}

  //对应 Task<Department> Add(Department department) 方法
  rpc Add(AddDepartmentRequest) returns (AddDepartmentResponse) {}
}

//对应的方法需要的参数
message GetAllDepartmentsRequest {}

//对应返回值类型
message GetAllDepartmentsResponse {
  //因为是集合,所以用 repeated
  repeated Department Departments = 1;
}

message AddDepartmentRequest { Department department = 1; }

message AddDepartmentResponse { Department department = 1; }

//--------------------------------------------------------------------

// IEmployeeRepository

//对应 Models 中的 Employee 类
message Employee {
  int32 id = 1;
  int32 departmentId = 2;

  string firstName = 3;
  string lastName = 4;
  Gender gender = 5;
  bool fired = 6;
}

enum Gender {
  FEMALE = 0;
  MALE = 1;
}

//对应 Repositories 中的 IEmployeeRepository
service Employees {
  //对应 Task<Employee> Add(Employee employee) 方法
  rpc Add(AddEmployeeRequest) returns (AddEmployeeResponse) {}

  //对应 Task<IEnumerable<Employee>> GetByDepartmentId(int departmentId) 方法
  rpc GetByDepartmentId(GetByDepartmentIdRequest)
      returns (GetByDepartmentIdResponse) {}

  //对应 Task<Employee> Fire(int id) 方法
  rpc Fire(FireRequest) returns (FireResponse) {}
}

message AddEmployeeRequest { Employee employee = 1; }

message AddEmployeeResponse { Employee employee = 1; }

message GetByDepartmentIdRequest { int32 departmentId = 1; }

message GetByDepartmentIdResponse { repeated Employee Employees = 1; }

message FireRequest { int32 id = 1; }

message FireResponse { Employee employee = 1; }

//--------------------------------------------------------------------

// ISummaryRepository

//对应 Models 中的 CompanySummary 类
message CompanySummary {
  int32 employeeCount = 1;
  int32 averageDepartmentEmployeeCount = 2;
}

//对应 Repositories 中的 ISummaryRepository
service CompanySummarys {
  //对应 public Task<CompanySummary> GetCompanySummary() 方法
  rpc GetCompanySummary(GetCompanySummaryRequest)
      returns (GetCompanySummaryResponse) {}
}

message GetCompanySummaryRequest {}

message GetCompanySummaryResponse { CompanySummary companySummary = 1; }

GetById() 因为重复命名的问题,所以不写

写 proto 文件,右击文件,选择属性
Build Action:Protobuf compiler
gRPC Stub Classes:Server only
修改完属性之后,可以双击项目查看到变化,然后我们 Build 项目

当然,这里有一个坑,就是 C 盘下的当前用户中文路径问题,因为 NuGet 把库的下载放在用户文件夹下,而 proto 的插件不能有 ASCII 以外的字符,会报以下错误

protoc-gen-grpc: The system cannot find the path specified.
--grpc_out

解决方法可以参考这个:

https://blog.csdn.net/img_Guo/article/details/86096604

我是修改了 NuGet 的下载路径解决了不能生成的问题

还有,因为 proto 不支持中文变量,所以我们的 Models Employee 下的 Gender 枚举要修改,其实也无所谓,因为等会儿就要删了

生成代码在 debug 的目录下

使用生成的 C# 代码

接下来就可以删除 Models 文件夹了,命名空间也要删干净,因为 gRPC 已经生成好了

因为我们的项目目录下已经有一个 Services 文件夹,所以我们在项目下新建一个文件夹,命名 GRpcServices
在该目录下新建几个类,没见过的类就是 gRPC 生成的,可以选中没见过的类,用 F12 去看看生成的代码
DepartmentService.cs

public class DepartmentService : Departments.DepartmentsBase
{
    private readonly IDepartmentRepository _departmentRepository;

    public DepartmentService(IDepartmentRepository departmentRepository)
    {
        this._departmentRepository = departmentRepository;
    }

    public override async Task<GetAllDepartmentsResponse> GetAll(GetAllDepartmentsRequest request, ServerCallContext context)
    {
        var result = new GetAllDepartmentsResponse();

        var departments = await this._departmentRepository.GetAll();
        result.Departments.AddRange(departments);

        return result;
    }

    public override async Task<AddDepartmentResponse> Add(AddDepartmentRequest request, ServerCallContext context)
    {
        var department = await this._departmentRepository.Add(request.Department);

        var result = new AddDepartmentResponse
        {
            Department = department
        };

        return result;
    }
}

EmployeeService.cs

public class EmployeeService : Employees.EmployeesBase
{
    private readonly IEmployeeRepository _employeeRepository;

    public EmployeeService(IEmployeeRepository employeeRepository)
    {
        this._employeeRepository = employeeRepository;
    }

    public override async Task<GetByDepartmentIdResponse> GetByDepartmentId(GetByDepartmentIdRequest request, ServerCallContext context)
    {
        var result = new GetByDepartmentIdResponse();
        var employees = await this._employeeRepository.GetByDepartmentId(request.DepartmentId);

        result.Employees.AddRange(employees);
        return result;
    }

    public override async Task<AddEmployeeResponse> Add(AddEmployeeRequest request, ServerCallContext context)
    {
        var employee = await this._employeeRepository.Add(request.Employee);
        var result = new AddEmployeeResponse
        {
            Employee = employee
        };

        return result;
    }

    public override async Task<FireResponse> Fire(FireRequest request, ServerCallContext context)
    {
        var employee = await this._employeeRepository.Fire(request.Id);
        var result = new FireResponse
        {
            Employee = employee
        };

        return result;
    }
}

没什么可解释的,看看类型就知道了,主要就是 request 和 response

Startup.cs

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddGrpc();

        services.AddSingleton<IClock, UtcClock>();
        services.AddSingleton<IDepartmentRepository, DepartmentRepository>();
        services.AddSingleton<IEmployeeRepository, EmployeeRepository>();
        services.AddSingleton<ISummaryRepository, SummaryRepository>();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGrpcService<DepartmentService>();
            endpoints.MapGrpcService<EmployeeService>();
        });
    }
}

做一个客户端

这里为了方便,就直接用 Blazor 的例子 My_ASP_NET_Core_Blazor_Program,直接把整个项目复制到解决方案里
Blzaor 的项目要用 NuGet 安装 Grpc.AspNetCore
并且在 Blazor 项目的配置里添加,以下代码,然后重新 Build

<ItemGroup>
    <Protobuf Include="..\My_gRPC_Api\Protos\grpcapi.proto" GrpcServices="Client" />
</ItemGroup>

Blazor 项目的 Startup.cs

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
        services.AddServerSideBlazor();

        services.AddSingleton<IClock, ChinaClock>();
        services.AddSingleton<IDepartmentService, DepartmentService>();
        services.AddSingleton<IEmployeeService, EmployeeService>();

        //这个 uri 对应 gRPC 那个项目的端口
        services.AddGrpcClient<Departments.DepartmentsClient>(o => o.Address = new Uri("https://localhost:5001"));
        services.AddGrpcClient<Employees.EmployeesClient>(o => o.Address = new Uri("https://localhost:5001"));

        //让 Http/2 不需要证书
        AppContext.SetSwitch("System.Net.Http.ScoketsHttpHandler.Http2UnencryptedSupport", true);
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        //需要使用 https
        app.UseHttpsRedirection();

        app.UseStaticFiles();

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapBlazorHub();
            
            //初始页面 Pages/_Host.cshtml
            endpoints.MapFallbackToPage("/_Host");
        });
    }
}

删除 Blazor 项目中的 Models ,以及引用它的命名空间,Pages 里也要改
Services 里的类也对应修改
IDepartmentService.cs

public interface IDepartmentService
{
    //获取所有部门
    Task<IEnumerable<Department>> GetAll();
    //添加部门
    Task Add(Department department);
}

IEmployeeService.cs

public interface IEmployeeService
{
    //添加员工
    Task Add(Employee employee);
    //根据部门Id获取该部门所有员工集合
    Task<IEnumerable<Employee>> GetByDepartmentId(int departmentId);
    //解雇员工
    Task<Employee> Fire(int id);
}

DepartmentService.cs

public class DepartmentService : IDepartmentService
{
    private readonly Departments.DepartmentsClient _client;

    public DepartmentService(Departments.DepartmentsClient client)
    {
        this._client = client;
    }

    public async Task<IEnumerable<Department>> GetAll()
    {
        var response = await this._client.GetAllAsync(new GetAllDepartmentsRequest());
        return response.Departments;
    }

    public async Task Add(Department department)
    {
        await this._client.AddAsync(new AddDepartmentRequest
        {
            Department = department
        });
    }

}

EmployeeService.cs

public class EmployeeService : IEmployeeService
{
    private readonly Employees.EmployeesClient _client;

    public EmployeeService(Employees.EmployeesClient client)
    {
        this._client = client;
    }
    public async Task Add(Employee employee)
    {
        await this._client.AddAsync(new AddEmployeeRequest
        {
            Employee = employee
        });
    }

    public async Task<IEnumerable<Employee>> GetByDepartmentId(int departmentId)
    {
        var response = await this._client.GetByDepartmentIdAsync(new GetByDepartmentIdRequest
        {
            DepartmentId = departmentId
        });

        return response.Employees;
    }

    public async Task<Employee> Fire(int id)
    {
        var response = await this._client.FireAsync(new FireRequest
        {
            Id = id
        });

        return response.Employee;
    }
}

gRPC 项目的 Startup.cs

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddGrpc();

        services.AddSingleton<IClock, UtcClock>();
        services.AddSingleton<IDepartmentRepository, DepartmentRepository>();
        services.AddSingleton<IEmployeeRepository, EmployeeRepository>();
        services.AddSingleton<ISummaryRepository, SummaryRepository>();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGrpcService<DepartmentService>();
            endpoints.MapGrpcService<EmployeeService>();
        });
    }
}

注意:两个项目端口不能冲突,在 launchSetting.json 里改

Blazor Startup.cs 里的端口对应 gRPC 的 launchSetting.json ,gRPC 是 https 的端口,启动两个项目,可以使用

ASP.NET Core 3.x 入门(九)gRPC in ASP.NET Core 3.x 结束

posted @ 2021-05-13 20:18  .NET好耶  阅读(178)  评论(0编辑  收藏  举报