如何在 Web API 控制器中使用分层架构处理服务器错误

Posted

技术标签:

【中文标题】如何在 Web API 控制器中使用分层架构处理服务器错误【英文标题】:How to handle server errors with layered architecture in web API controller 【发布时间】:2018-10-26 09:29:35 【问题描述】:

编辑:

我刚刚意识到 SaveChangesAsync 返回 0 并不意味着它失败了,实体框架在失败时总是会抛出异常,因此检查 SaveChanges == 0 是否是多余的!在下面的示例中,保存更改应始终返回 1,如果失败则抛出异常。

但是在某些情况下,使用了其他东西并且它不是实体框架,所以这个问题也是如此。


服务器可能会出现故障,当我将所有数据访问代码放入控制器时,我可以这样处理:

[HttpPost]
public async Task<ActionResult<Item>> CreateAsync([FromBody] Item item)

    await _dbContext.AddAsync(item);
    if (await _dbContext.SaveChangesAsync() == 0)
    
        return StatusCode(StatusCodes.Status500InternalServerError);
    
    return CreatedAtAction(nameof(GetAsync), new  id = item.Id , item);

当我的数据访问被封装在服务层时,我应该如何处理?

public class ItemsService

    public async Task<Item> CreateAsync(Item item)
    
        await _dbContext.AddAsync(item);
        if (await _dbContext.SaveChangesAsync() == 0)
        
            return null;
        
        return item;
    

那么它会这样使用:

[HttpPost]
public async Task<ActionResult<Item>> CreateAsync([FromBody] Item item)

   // model state validation skipped for the sake of simplicity,
   // that would return BadRequest or some more valuable information
    var item = await _itemsService.CreateAsync(item);
    if (item == null)
    
        return StatusCode(StatusCodes.Status500InternalServerError);
    
    return CreatedAtAction(nameof(GetAsync), new  id = item.Id , item);

也许这适用于精细创建,因为只有 2 个状态代码,但让我们考虑更新可能有超过 2 个可能错误的地方,例如:

未找到 (404) 内部服务器错误(500) 好的 (200)

没有服务的代码:

[HttpPut("id")]
public async Task<ActionResult<Item>> UpdateAsync(int id, [FromBody] Item itemToUpdate)

    var item = await _dbContext.Items.FindAsync(id);
    if (item == null)
    
        return NotFound();
    

    // update item with itemToUpdate
    //...
    await _dbContext.Update(item);
    if (await _dbContext.SaveChangesAsync() == 0)
    
        return StatusCode(StatusCodes.Status500InternalServerError);
    
    return item;

现在服务无法正确处理:

public class ItemsService

    public async Task<Item> UpdateAsync(Item updateItem)
    
        var item = await _dbContext.Items.FindAsync(id);
        if (item == null)
        
            return null;
        
        //change some properties and update
        //...
        _dbContext.Items.Update(item);
        if (await _dbContext.SaveChangesAsync() == 0)
        
            // what now?
        
        return item;
    

因为它总是返回 null 并且无法判断该项目是否未找到或保存失败。

如何正确处理?

注意:我没有添加 DTO 或类似的东西来保持这个例子的简单。

【问题讨论】:

你有没有想过会冒泡层而不是返回值的异常来解决硬件/io/传输特定问题,例如“互联网/服务器不可用”、“磁盘已满” @k3b 你的意思是在 try-catch 中包装代码吗?或者你到底是什么意思? 【参考方案1】:

您的服务负责捕获它知道如何处理的所有异常并处理这些异常。所有其他异常都应通过某种IServiceResult&lt;T&gt; 报告,同时提供bool IsSuccessfulAggregatedException Exceptions get; 。所以与其抛出你让最高层来决定如何反应。从许多角度来看,这实际上是最好的方法,包括:功能的纯度(这是易于并行化的关键概念,对服务器来说不重要吗?),干净且可维护的代码(服务的消费者不应该知道/非常关心:他们只是验证结果是否成功并使用结果;否则挖掘聚合异常;实现您自己类型的异常提供适合您项目需求的方法可能是有意义的),能够继续进行更高阶-负责实际错误处理的函数;以rX 为例:.OnError(Action&lt;Exception&gt; handler)。远不止这些,但我不想让答案变得更长。

作为旁注。确保您阅读了来自Haskell 编程语言的关于MaybeEither monads 的介绍性杠杆文章;如果您更喜欢F#,可以尝试分别寻找SomeEither。在那里您可以找到有用的方法,例如 .Map(...),并查看以有效方式组合/聚合这些方法的好方法。

祝你好运。

【讨论】:

AggregatedException 应该是什么样子,应该如何填充? 不幸的是,我对 Haskell 和 F# 了解不多,因为我从未使用过它们,但我知道响应式编程,但如果这就是您所说的 rX @Konrad AggException 是标准的 .NET 类。由于并发和并行计算的本质,它在多线程编程中无处不在。如果您(还)不熟悉,那么花点时间尝试一下绝对是有意义的。但是,现在您可能会跳过那些“高级”方法;我上面推荐的解决方案将与这些想法完全兼容,您可以尽快或永远不要回复它们。【参考方案2】:

这实际上是一个相当大的话题,有很多选择。

我更喜欢抛出异常。您可以为此创建自己的类或使用 .net 类。你可以有 NotFoundException、InternalServerError 等。每个类都可以有 StatusCode 和 Message 字段或任何你想要的。接下来,您需要为 asp.net 实现异常过滤器,它将处理所有异常并返回带有可以从异常类中检索的状态码的响应。

另一种选择可能更简单。您可以创建一个包含 StatusCode 和对象作为结果的类,以便从您的方法中返回它。这是一种从您的方法中返回更多内容的方法,而不仅仅是 null 或对象。

几篇好文章: Exceptions or error codes

https://www.hanselman.com/blog/GoodExceptionManagementRulesOfThumb.aspx

http://codebetter.com/karlseguin/2006/04/05/understanding-and-using-exceptions/

【讨论】:

【参考方案3】:

服务的代码要好得多。控制器将业务逻辑委托给服务层。

为了更好地实现服务层,您应该:

当您的服务发生错误时抛出自定义异常。示例:抛出ItemNotFoundExcption 而不是返回nullItemUpdateException 更新失败时。 在您的控制器中,您需要捕获所有可能发生的自定义异常并执行需要完成的操作,例如为ItemNotFoundException 返回NotFound(),为ItemUpdateException 返回BadRequest()。 后一点可以通过自定义操作过滤器来实现,它可以帮助您使控制器的操作更精简。

【讨论】:

服务甚至不应该知道.BadRequest() 的存在。服务现在应该很热......服务;)让服务知道响应肯定是错误的职责分离,导致整体架构不佳。如果由于任何因素无法提供服务,则必须将此特定因素报告给呼叫者。而已;然后假定调用者而不是服务决定下一步该做什么。 @SerejaBogolubov 我在哪一行说 service do BadRequest() ?老兄重读了我的答案。服务仅抛出控制器操作捕获的自定义异常。然后操作可以解释自定义异常以返回 BadRequest、NotFound 或属于该层的任何内容。 确实如此。然后纯度是唯一的问题,但是我回滚我的 -1 使其变为 +1 ))))

以上是关于如何在 Web API 控制器中使用分层架构处理服务器错误的主要内容,如果未能解决你的问题,请参考以下文章

2设计Web Api分层架构

Web App 分层架构(基于 Vue+Router+Vuex)

(系统架构)标准Web系统的架构分层

关于分层架构

关于分层架构

标准Web系统的架构分层[转]