【问题标题】:How can I hide System.Exception errors on .NET Core?如何在 .NET Core 上隐藏 System.Exception 错误?
【发布时间】:2022-01-03 06:48:43
【问题描述】:

我现在尝试使用 .NET Web API 改进自己,并尝试在 Swagger 中返回自定义错误。但是当返回这个自定义错误时,我可以看到错误在哪一行。我该怎么做才能防止这种情况发生?

public async Task<BookCreateDTO> CreateBook(BookCreateDTO bookCreateDto)
        {
            if (await _context.Books.AnyAsync(x => x.Name == bookCreateDto.Name))
            {
                throw new BookExistException("Book already exist");
            }

            var book= _mapper.Map<Book>(bookCreateDto);
            _context.Books.Add(book);
            await _context.SaveChangesAsync();
            return book;
        }

我应该怎么做才能在 Swagger 响应中仅看到此异常消息? 感谢您的帮助。

【问题讨论】:

  • 异常应该是异常的:不要为非异常错误抛出异常。

标签: .net-core swagger asp.net-core-webapi


【解决方案1】:

异常应该是异常的:不要为非异常错误抛出异常。

我不建议在 C# 操作方法返回类型中指定 Web 服务的响应 DTO 类型,因为它会限制您的表现力(正如您所发现的那样)。

  • 改为使用IActionResultActionResult&lt;T&gt; 记录默认(即HTTP 2xx)响应类型,然后在[ProducesResponseType] 属性中列出错误DTO 类型及其相应的HTTP 状态代码。
    • 这也意味着每个响应状态代码应该只与一个单个 DTO 类型相关联。
    • 虽然 Swagger 的表达力不足以让您说“如果响应状态是 HTTP 200,那么响应正文/DTO 是 one-of DtoFoo, DtoBar, DtoQux ",在实践中,精心设计的 Web 服务 API不应该表现出这种响应 DTO 多态性。
      • 如果不知道,那么客户端应该如何仅从 HTTP 标头中知道类型是什么? (好吧,您可以将完整的 DTO 类型名称放在自定义 HTTP 响应标头中,但这会带来其他问题...)
  • 对于错误情况,将错误添加到ModelState(如果可能,使用Key)和let ASP.NET Core handle the rest for youProblemDetails
  • 如果您确实抛出了异常,那么 ASP.NET Core can be configured to automatically render it as a ProblemDetails - 或者它可以显示 DeveloperExceptionPage - 或完全其他的东西。
    • 我注意到在控制器内为非异常异常抛出异常的一个很好的理由是,您的日志框架可能会选择在 ASP.NET Core 的管道中记录有关未处理异常的更多详细信息,这将导致您的日志中出现无用的无关条目,从而使您更难找到需要修复的“真实”异常。
  • 使用 [ProducesResponseType] 记录使用的 DTO 及其相应的 HTTP 状态代码:这在使用 Swagger/NSwag 生成在线文档和客户端库时非常有用。
  • 另外:不要将 EF 实体类型用作 DTO 或 ViewModel
    • 原因 1:当响应(带有 EF 实体对象)被序列化时,具有延迟加载属性的实体将导致您的整个数据库对象图被序列化(因为 JSON 序列化程序将遍历每个对象)。
    • 原因 2:安全!如果您直接接受 EF 实体作为输入请求正文 DTO 或 HTML 表单模型,则用户/访问者可以任意设置属性,例如例如,POST /users{ accessLevel: 'superAdmin' }。虽然您可以排除或限制可以通过请求设置对象的哪些属性,但它只会增加项目的维护工作量(因为它是程序中的另一个非本地、手动编写的列表或定义,您需要确保保留在- 与其他一切同步。
    • 原因 3:自我记录意图:实体类型用于进程内状态,而不是作为通信合同。
    • 原因 4:实体类型的成员永远不会完全在 DTO 中公开。
      • 例如,您的User 实体将具有Byte[] PasswordHashByte[] PasswordSalt 属性(我希望...),显然这两个属性必须从不 暴露;但在用于编辑用户的用户 DTO 中,您可能需要不同的 成员,例如 NewPasswordConfirmPassword - 它们根本不映射到 DB 列。
    • 原因 5:在原因 4 的相关说明中,使用实体类作为 DTO 会自动将 Web 服务 API 的精确设计绑定到数据库模型。
      • 假设有一天您绝对需要对数据库设计进行更改:也许有人告诉您业务需求发生了变化;这是正常的,并且一直在发生
      • 假设 DB 设计更改是从每个客户只允许 1 个地址(因为街道地址与客户存储在同一个表中)到允许客户有多个地址(即街道地址列移动到不同的表)...
      • ...所以您更改数据库、运行迁移脚本并部署到生产环境 - 但突然间,您的所有 Web 服务客户端都停止工作,因为他们都认为您的 Customer 对象具有内联街道地址字段,但现在它们不见了(因为您的 Customer EF 实体类型不再有街道地址列,这在 CustomerAddress 实体类中结束了)。
      • 如果您一直使用专门用于Customer 对象的专用 DTO 类型,那么在更新应用程序设计的过程中,您会注意到由于 C# 编译时类型,构建会更快地中断(而不是不可避免地更晚!) - 签入您的 DTO 到实体(以及实体到 DTO)的映射代码 - 这是一个好处。
      • 但是主要的好处是它允许您完全抽象出您的底层数据库设计 - 因此,在我们的示例中,如果您的远程客户端依赖于内联的客户地址信息,那么您的客户 DTO 仍然可以模拟通过将 first 客户地址内联到原始客户 DTO 中,当它向远程客户端呈现其 JSON/XML/Protobuf 响应时,旧设计。这样可以节省时间、麻烦、精力、金钱、压力、投诉、解雇、不必要的殴打、严重的身体伤害和预约牙科保健员。

无论如何,我已经修改了您发布的代码以遵循上述指导:

  • 我添加了[ProducesResponseType] 属性。
    • 我认为指定默认响应类型 BookCreateDTO 两次是多余的(在 [ProducesResponseType]ActionResult&lt;BookCreateDTO&gt; 中 - 您应该能够删除其中任何一个而不影响 Swagger 输出。
  • 为了安全起见,我添加了明确的[FromBody]
  • 如果“未使用书名”检查失败,它会在 ASP.NET 的 stock BadRequest 响应中返回模型验证消息,该响应呈现为 IETF RFC 7807 响应,aka @ 987654353@ 而不是抛出异常,然后希望您将 ASP.NET Core 管道(在 Configure() 中)配置为将其作为 ProblemDetails 处理,而不是调用调试器或使用DeveloperExceptionPage
    • 请注意,在名称冲突的情况下,我们希望返回 HTTP 409 Conflict 而不是 HTTP 400 Bad Request,因此 conflictResult.StatusCode = 409; 将被覆盖。
  • 最终响应是通过 AutoMapper 和 Ok() 从新的 BookCreateDTO 实例生成的,而不是序列化您的 Book 实体对象。
[ProducesResponseType(typeof(BookCreateDTO), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
public async Task< ActionResult<BookCreateDTO> > CreateBook( [FromBody] BookCreateDTO bookCreateDto )
{
    // Does a book with the same name exist? If so, then return HTTP 409 Conflict.
    if( await _context.Books.AnyAsync(x => x.Name == bookCreateDto.Name) )
    {
        this.ModelState.Add( nameof(BookCreateDTO.Name), "Book already exists" );
        BadRequestObjectResult conflictResult = this.BadRequest( this.ModelState );
        // `BadRequestObjectResult` is HTTP 400 by default, change it to HTTP 409:
        conflictResult.StatusCode = 409;
        return conflictResult;
    }

    Book addedBook;
    {
        addedBook = this.mapper.Map<Book>( bookCreateDto );
        _ = this.context.Books.Add( book );
        _ = await this.context.SaveChangesAsync();
    }

    BookCreateDTO responseDto = this.mapper.Map<BookCreateDTO >( addedBook );

    return this.Ok( responseDto );
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-05-05
    • 1970-01-01
    • 1970-01-01
    • 2022-11-25
    • 2017-09-24
    • 2017-10-17
    • 1970-01-01
    相关资源
    最近更新 更多