如何隐藏 .NET Core 上的 System.Exception 错误?

use*_*966 4 swagger .net-core asp.net-core-webapi

我现在尝试使用 .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;
        }
Run Code Online (Sandbox Code Playgroud)

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

Dai*_*Dai 7

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

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

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

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

  • 我添加了[ProducesResponseType]属性。
    • BookCreateDTO我明白两次指定默认响应类型是多余的 ([ProducesResponseType]以及ActionResult<BookCreateDTO>- 您应该能够删除其中任何一个而不影响 Swagger 输出。
  • [FromBody]为了安全起见,我添加了一个显式的。
  • 如果“book-name isused”检查失败,它会在 ASP.NET 的库存BadRequest响应中返回模型验证消息,该消息将呈现为 IETF RFC 7807 响应, ProblemDetails 就是抛出异常,然后希望您配置了 ASP。 NET Core 管道(在Configure())中将其作为处理ProblemDetails,而不是调用调试器或使用DeveloperExceptionPage.
    • 请注意,在名称冲突的情况下,我们希望返回 HTTP 409 Conflict 而不是 HTTP 400 Bad Request,因此被conflictResult.StatusCode = 409;覆盖。
  • BookCreateDTO最终响应是通过 AutoMapper从新实例生成的,而Ok()不是序列化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 );
}
Run Code Online (Sandbox Code Playgroud)