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


posted @ 2021-08-26 19:41  四月是你的谎言  阅读(649)  评论(0编辑  收藏  举报