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

时间:2018-05-16 10:52:16

标签: c# asp.net-core domain-driven-design entity-framework-core asp.net-core-webapi

修改

我刚刚意识到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个状态代码,但让我们考虑更新可能存在两个以上可能错误的位置:

  • 未找到(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或类似的东西来保持这个例子的简单。

3 个答案:

答案 0 :(得分:1)

您的服务负责捕获它知道如何处理和处理这些异常的所有异常。所有其他例外情况都应通过提供IServiceResult<T>bool IsSuccessful的某种AggregatedException Exceptions { get; }进行报告。所以,而不是扔你让最高级别来决定如何反应。从许多角度来看,它实际上是最好的方法,包括:功能的纯度(这是简单并行的关键概念,对服务器来说不重要吗?),清洁和可维护的代码(消费者的服务不应该知道/关心那么多:它们只是验证结果是否成功并消耗结果;否则挖掘聚合异常;可能有意义实现自己的异常类型,提供适合您项目需求的方法),能力在高阶函数上进行,这些函数负责实际的错误处理;以rX作为一个受欢迎的例子:.OnError(Action<Exception> handler)。除此之外还有很多,但是我不想让答案更长。

作为旁注。请务必阅读Maybe编程语言EitherHaskell monad的介绍性杠杆文章;如果您更喜欢F#,则可以尝试分别寻找SomeEither。在那里,您可以找到有用的方法,如.Map(...),并查看以有效方式组合/汇总这些方法的好方法。

祝你好运。

答案 1 :(得分:1)

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

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

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

答案 2 :(得分:0)

它实际上是一个非常大的主题,并且有很多选项可以做到这一点。

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

另一种选择可能更简单。您可以创建一个包含StatusCode和object的类,这样您就可以从方法中返回它。它是一种从方法返回更多然后只是null或object的方法。

很少有好文章: Exceptions or error codes

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

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