NET5 GRPC 全局异常处理,参数校验,swagger文档生成,以及 GRPC 提供 web api 接口

实现得很傻逼....别问为什么...问就是新手瞎几把写

先上效果图

NET5 GRPC 全局异常处理,参数校验,swagger文档生成,以及 GRPC 提供 web api 接口

 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 就封装统一格式.