You need to enable JavaScript to run this app. .NET 如何透传服务层异常至控制器返回带错误信息的400状态码
优惠活动
大模型
产品
解决方案
定价
更多

.NET 如何透传服务层异常至控制器返回带错误信息的400状态码

核心问题与改造方案

你现在这套代码哪怕逻辑思路是对的,也跑不通预期效果,先把几个硬伤挨个排掉,再说正确的实现逻辑。

你当前代码的几个致命bug

  • 自定义异常的构造逻辑写反了:PaginationTokenException 接收字符串消息的构造函数里,本来应该把消息传给基类,结果你在构造函数内部主动throw new Exception(message),相当于只要走到这个重载,你自定义的异常类型直接被普通异常替代,后面所有按自定义异常做的判断全部失效。
  • Service1把异常全吞了:你在Service1里catch了Service2抛出来的异常,只打了日志就往下走,根本没把异常往上抛,控制器永远收不到错误信息,甚至可能因为空对象触发后续的映射空引用异常。
  • 异常捕获粒度太粗:Service2把所有DynamoDB调用抛的异常全包装成分页token错误,不管是用户传参错了、网络超时、权限不足,全归成一类,根本区分不出来什么场景该返回400,什么场景该返回500。

先回答你最关心的问题:逐层抛自定义异常是不是正确的实现方式?

这就是.NET分层架构的标准错误传递方案,完全正确。
这套模式的核心逻辑就是各层管各的事,靠自定义异常做层与层之间的错误契约:

  1. 最底层(基础设施层,也就是你的DynamoDB查询逻辑):只负责把第三方SDK抛的原始异常,翻译成带业务语义的自定义异常,别把AWS SDK的原生异常直接往上层丢。
  2. 中间业务层(你的Service1):只负责打日志、做业务逻辑校验,别随便吞异常,该往上抛就往上抛,不用关心上层要怎么转成HTTP响应。
  3. 最上层(控制器):作为请求管线的最后一站,负责根据异常类型映射成对应的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.29 00:54:04