NET5 GRPC 全局异常处理,参数校验,swagger文档生成,以及 GRPC 提供 web api 接口
实现得很傻逼....别问为什么...问就是新手瞎几把写
先上效果图
怎么创建 grpc server 我就不说了...
先安装一下需要的玩意
Calzolari.Grpc.Net.Client.Validation
Microsoft.AspNetCore.Grpc.Swagger (这是预览版的记得勾选上)
/// <summary> /// 业务异常类 /// </summary> public class BusinessException : Exception { public BusinessException(int code, string msg) { Code = code; Msg = msg; } /// <summary> /// 错误码 /// </summary> public int Code { get; set; } /// <summary> /// 错误信息 /// </summary> public string Msg { get; set; } = string.Empty; /// <summary> /// 服务器未知错误 /// </summary> /// <returns></returns> public static BusinessException UnknownError() => new(-1, "未知错误"); }
GRPC 中的异常用拦截器来处理
/// <summary> /// grpc 全局异常处理拦截器 /// </summary> public class ExceptionInterceptor : Interceptor { private readonly ILogger<ExceptionInterceptor> logger; public ExceptionInterceptor(ILogger<ExceptionInterceptor> logger) { this.logger = logger; } public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation) { try { return await continuation(request, context); } catch (Exception e) { if (e is RpcException re) { if (re.StatusCode == StatusCode.InvalidArgument) { string base64ErrStr = re.Trailers.GetValue("validation-errors-text"); byte[] bytes = Convert.FromBase64String(base64ErrStr); string jsonErrorStr = Encoding.UTF8.GetString(bytes); List<ValidationFailure> validationFailures = JsonConvert.DeserializeObject<List<ValidationFailure>>(jsonErrorStr); re.Trailers.Clear(); if (validationFailures.Count > 0) { string message = string.Empty; validationFailures.ForEach(err => { message += $"{err.ErrorMessage},"; }); message = message[0..^1]; throw new RpcException(new Status(StatusCode.InvalidArgument, message)); } throw; } } if (e is BusinessException be) { throw new RpcException(new Status(StatusCode.FailedPrecondition, be.Msg)); } logger.LogError(e.ToString()); throw new RpcException(new Status(StatusCode.Internal, "Server internal error, contact administrator!")); } } }
因为要把 grpc 转换成 web api ,为了返回友好的格式要自己处理一下(实现得很傻逼...主要是不知道怎么把 RpcException 第二个参数 Metadata 元数据信息在 HttpContext 中读取出来...不然可以通过元数据传递错误信息,有知道的老哥可以回复一下,谢谢)
用中间件来处理 web api 的请求
/// <summary> /// web api 全局异常处理中间件 /// </summary> public class ExceptionMiddleware { private readonly RequestDelegate next; private readonly ILogger<ExceptionInterceptor> logger; public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionInterceptor> logger) { this.next = next; this.logger = logger; } public async Task Invoke(HttpContext context) { try { StringValues stringValues = context.Request.Headers["accept"]; if (stringValues.Equals("application/json")) { // TODO 实现的有问题,性能不会太好,目前不知道怎么从 HttpContext 读取 GRPC 的元数据,不然可以通过他获取错误消息 var responseOriginalBody = context.Response.Body; using var ms = new MemoryStream(); context.Response.Body = ms; await next.Invoke(context); if (context.Response.StatusCode != 200) { ms.Seek(0, SeekOrigin.Begin); using var responseReader = new StreamReader(ms); var responseContent = await responseReader.ReadToEndAsync(); ms.Seek(0, SeekOrigin.Begin); Dictionary<object, object> dictionary = JsonConvert.DeserializeObject<Dictionary<object, object>>(responseContent); string error = (string)dictionary["error"]; string indexStr = "Detail=\""; int index = error.LastIndexOf(indexStr); string detail = error.Substring(index + indexStr.Length, error.LastIndexOf("\")") - index - indexStr.Length); ResponseData rd = new(null, false, detail); string rdJson = JsonConvert.SerializeObject(rd); byte[] rdJsonBytes = Encoding.UTF8.GetBytes(rdJson); using var ms2 = new MemoryStream(); await ms2.WriteAsync(rdJsonBytes.AsMemory(0, rdJsonBytes.Length)); ms2.Seek(0, SeekOrigin.Begin); await ms2.CopyToAsync(responseOriginalBody); } else { ms.Seek(0, SeekOrigin.Begin); using var responseReader = new StreamReader(ms); var responseContent = await responseReader.ReadToEndAsync(); ms.Seek(0, SeekOrigin.Begin); ResponseData rd = new(JsonConvert.DeserializeObject(responseContent), true, ""); string rdJson = JsonConvert.SerializeObject(rd); byte[] rdJsonBytes = Encoding.UTF8.GetBytes(rdJson); using var ms2 = new MemoryStream(); await ms2.WriteAsync(rdJsonBytes.AsMemory(0, rdJsonBytes.Length)); ms2.Seek(0, SeekOrigin.Begin); await ms2.CopyToAsync(responseOriginalBody); } } else { await next.Invoke(context); } } catch (Exception ex) { logger.LogError(ex.ToString()); throw; } } public class ResponseData { public ResponseData(object data, bool success, string errorMsg) { Data = data; Success = success; ErrorMsg = errorMsg; } [JsonProperty("data")] public object Data { get; set; } [JsonProperty("sucess")] public bool Success { get; set; } [JsonProperty("error_msg")] public string ErrorMsg { get; set; } } }
接下来就是 grpc 怎么做参数校验
随便来一个 proto 服务
syntax = "proto3"; import "google/api/annotations.proto"; // 用户服务 service UserGrpcService { // 用户登录 rpc Login (UserLoginRequest) returns (UserLoginReply){ option (google.api.http) = { post: "/v1/user/login", body: "*" }; } } message UserLoginRequest { string user_name = 1; // 用户名 string password = 2; // 密码 }
定义参数校验, 看到下面的 UserLoginRequest 这玩意了吗...这是 根据 proto 文件自动生成的类型 和 proto中的 参数一样的,这样定义了就可以校验了, 记得添加 services.AddValidator<Login>();
public class Login : AbstractValidator<UserLoginRequest> { public Login() { RuleFor(r => r.UserName).MaximumLength(12); RuleFor(r => r.Password).MaximumLength(12); } }
public void ConfigureServices(IServiceCollection services) { services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.All)); _ = services.AddGrpc(options => { options.Interceptors.Add<ExceptionInterceptor>(); options.EnableMessageValidation(); }); services.AddValidator<Login>(); services.AddGrpcValidation(); services.AddGrpcHttpApi(); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "My Api", Version = "v1" }); }); services.AddGrpcSwagger(); }
public void Configure(IApplicationBuilder app) { app.UseMiddleware<ExceptionMiddleware>(); app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); }); _ = app.UseRouting(); _ = app.UseEndpoints(endpoints => { 添加自己的服务 }); }
proto文件怎么编写直接看微软的吧 从 gRPC 创建 JSON Web API | Microsoft Docs
写得很乱...当个笔记了,实现的效果就是 业务中抛出 BusinessException 异常,或者参数校验抛出的异常,grpc拦截器中捕获然后修改一下格式再次抛出友好的异常给客户端,然后 http 的中间件中判断一下请求来自grpc 客户端还是通过 http api 接口访问过来的,如果是http api 就封装统一格式.