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
目录
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
解决方法可以参考这个:
我是修改了 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 的端口,启动两个项目,可以使用