ASP.NET Core gRPC Unrecognized Guid format异常解决方案
问题描述
需要根据用户Id查询、删除对应用户,由于gRPC proto 原生不支持 Guid 类型,因此先在 proto 中做了如下定义:
service UserAuth { //... rpc Delete (UserFilter) returns (Empty); } //messages... message Empty{} message UserFilter{ string userID = 1; }
数据库存储的用户Id为 Guid 格式,需要将请求中 string 类型的UserFilter.userID与数据库 Guid 类型的 Id 匹配,实现按Id删除用户的逻辑。
最初的实现抛出System.FormatException: Unrecognized Guid format异常,或是查询返回不符合预期的null结果,初始gRPC Delete方法实现如下:
public override async Task<Empty> Delete(UserFilter requestData, ServerCallContext context) { var guid = new Guid(requestData.UserID); //var data = await _context.Users_5.Where(x => x.Id.ToString() == requestData.UserID); var data = _context.Users_5.Where(x => x.Id.Equals(guid)).FirstOrDefault(); if (data == null) { throw new Exception("User Not Found"); } _context.Users_5.Remove(data!); _context.SaveChanges(); return await Task.FromResult(new Empty()); }
补充说明:传入的Guid为标准格式,示例值为8D84C24F-C83B-4FE0-54B9-08DA43CF8A29,User实体类中Id定义如下:
[Key] public Guid Id { get; set; }
调整为Guid.Parse(requestData.UserID.ToString())的写法后可以正常运行,但实现不够合理,需要更规范的落地方案。
最优实现方案
1. 服务端逻辑重构(兼容现有proto定义)
public override async Task<Empty> Delete(UserFilter requestData, ServerCallContext context) { // 入口层做参数校验,从根源避免格式异常 if (string.IsNullOrWhiteSpace(requestData.UserID) || !Guid.TryParse(requestData.UserID, out var userId)) { throw new RpcException(new Status(StatusCode.InvalidArgument, "用户ID格式非法")); } // 用EF Core主键查询方法,性能优于手写Where条件 var data = await _context.Users_5.FindAsync(userId); if (data == null) { throw new RpcException(new Status(StatusCode.NotFound, "指定用户不存在")); } _context.Users_5.Remove(data); // 全程使用异步方法,提升服务吞吐能力 await _context.SaveChangesAsync(); return new Empty(); }
2. 核心优化点
- 用
Guid.TryParse替代直接构造new Guid()或无校验的Guid.Parse():从入口拦截非法参数,不会抛出未处理的格式异常,同时返回gRPC标准状态码,方便客户端做错误处理 - 移除冗余写法:
requestData.UserID本身就是string类型,额外调用.ToString()没有任何意义,属于无效代码 - 用
FindAsync(userId)做主键查询:相比手写Where+FirstOrDefault,该方法会优先读取EF上下文缓存,性能更高,语义也更贴合主键查询场景 - 全链路异步:原有代码混用同步查询、同步保存,最后用
Task.FromResult包装返回值的写法完全违背异步编程规范,改成原生异步调用可以大幅提升服务并发能力 - 禁止使用
x.Id.ToString() == requestData.UserID的查询写法:这种写法会导致EF Core无法命中数据库Id列的索引,必须逐行将Guid转成字符串做比对,数据量稍大就会出现严重性能问题,绝对不能在生产环境使用
3. 长期方案:使用gRPC原生Uuid类型
如果使用.NET 6+的gRPC工具链,不需要自己用string传递Guid,可以直接使用官方Protobuf定义的Uuid类型,从根源避免格式转换问题:
首先在proto中导入官方Uuid定义:
syntax = "proto3"; import "google/protobuf/uuid.proto"; service UserAuth { rpc Delete (UserFilter) returns (Empty); } message Empty{} message UserFilter{ google.protobuf.Uuid userID = 1; }
代码生成后,服务端可以直接调用requestData.UserID.ToGuid()获取.NET原生Guid类型,客户端调用guid.ToUuid()即可完成赋值,不需要手动做字符串解析,是跨语言gRPC服务传递Guid的标准实现。如果是从Cookie取值赋值请求的场景,在客户端层就做Guid格式校验,不要把非法字符串传到服务端处理,分层校验可以大幅降低异常排查成本。
内容的提问来源于stack exchange,提问作者José Leal

