.NET 如何透传服务层异常至控制器返回带错误信息的400状态码
核心问题与改造方案
你现在这套代码哪怕逻辑思路是对的,也跑不通预期效果,先把几个硬伤挨个排掉,再说正确的实现逻辑。
你当前代码的几个致命bug
- 自定义异常的构造逻辑写反了:
PaginationTokenException接收字符串消息的构造函数里,本来应该把消息传给基类,结果你在构造函数内部主动throw new Exception(message),相当于只要走到这个重载,你自定义的异常类型直接被普通异常替代,后面所有按自定义异常做的判断全部失效。 - Service1把异常全吞了:你在Service1里catch了Service2抛出来的异常,只打了日志就往下走,根本没把异常往上抛,控制器永远收不到错误信息,甚至可能因为空对象触发后续的映射空引用异常。
- 异常捕获粒度太粗:Service2把所有DynamoDB调用抛的异常全包装成分页token错误,不管是用户传参错了、网络超时、权限不足,全归成一类,根本区分不出来什么场景该返回400,什么场景该返回500。
先回答你最关心的问题:逐层抛自定义异常是不是正确的实现方式?
这就是.NET分层架构的标准错误传递方案,完全正确。
这套模式的核心逻辑就是各层管各的事,靠自定义异常做层与层之间的错误契约:
- 最底层(基础设施层,也就是你的DynamoDB查询逻辑):只负责把第三方SDK抛的原始异常,翻译成带业务语义的自定义异常,别把AWS SDK的原生异常直接往上层丢。
- 中间业务层(你的Service1):只负责打日志、做业务逻辑校验,别随便吞异常,该往上抛就往上抛,不用关心上层要怎么转成HTTP响应。
- 最上层(控制器):作为请求管线的最后一站,负责根据异常类型映射成对应的HTTP状态码(参数错了返400、权限不够返403、服务端内部错了返500),把结构化的错误信息返回给前端。
这么做的好处是解耦:底层换数据库不用改上层逻辑,上层改返回格式不用动底层查询逻辑,比靠层层传错误码的方式可维护性高太多。
改造后的可运行代码
1. 先修自定义异常的bug
把之前错误的构造函数删掉,按场景定义通用的参数错误异常就行,不用硬绑分页token的场景:
[Serializable] public class InvalidRequestParameterException : Exception { public InvalidRequestParameterException() { } public InvalidRequestParameterException(string message) : base(message) { } public InvalidRequestParameterException(string message, Exception inner) : base(message, inner) { } }
2. 改Service2的异常捕获逻辑
只捕获明确是参数不合法的DynamoDB异常,其他类型的异常直接抛,别全包成自定义异常:
private async Task<DynamoResult<TResponse>> GetPagedQueryResults(DynamoQuery query) { var results = new List<Document>(); try { results = await search.GetNextSetAsync(); } // 只过滤DynamoDB返回的参数类错误,其他异常不处理 catch (AmazonDynamoDBException ex) when (ex.StatusCode == System.Net.HttpStatusCode.BadRequest) { throw new InvalidRequestParameterException($"查询参数不合法:{ex.Message}", ex); } return results; }
3. 改Service1的异常处理
别吞异常,只在打日志之后把异常往上抛:
public async Task<DynamoResult<EntityResponse>> ListByParams(DynamoQuery entityQuery) { var results = new DynamoResult<Entity>(); try { results = await GetPagedQueryResults(entityQuery); } catch (InvalidRequestParameterException ex) { // 埋日志,记录用户输入的参数和错误详情 Console.WriteLine($"参数校验失败,入参:{JsonSerializer.Serialize(entityQuery)}, 错误栈:{ex}"); // 直接抛,别吞 throw; } return new DynamoResult<EntityResponse> { Data = results.Data.Select(_mapper.Map<EntityResponse>).ToList() }; }
4. 改控制器的状态码映射
别逮着什么异常都返回400,按异常类型判断:
[HttpGet] [Authorize(Policy = "Read-Entity")] [Route("byParams/{param1}/{param2}")] [Produces(typeof(DynamoResult<EntityResponse>))] public async Task<IActionResult> ListByParams([FromQuery] DynamoQuery entityQuery) { try { return await HandleRequest(async () => { return Ok((await _entityStore.ListByParams(entityQuery))); }); } catch (InvalidRequestParameterException e) { // 明确是参数错误才返回400,带具体错误消息 return BadRequest(e.Message); } // 其他未捕获的异常交给框架默认处理,返回500,别硬转成400 }
几个容易踩的坑
- 别在中间层catch了异常就不抛:这是分层架构最常见的坏味道,等于直接把错误链路断了,后面排查问题根本找不到根源。
- 别把所有异常都映射成400:不然真的服务端出问题的时候,前端会以为是自己传参错了,排查效率极低。
- 尽量用
catch (xxxException) when (condition)的语法缩小捕获范围,别无差别catch所有异常,不然很容易把本来该是服务端内部错误的问题,误判成用户参数错误。
内容的提问来源于stack exchange,提问作者user9460641
相关产品推荐
相关产品推荐

