如何在 .NET Core 上隐藏 System.Exception 错误?
How can I hide System.Exception errors on .NET Core?
我现在尝试使用 .NET Web API 提高自己,并且我正在尝试 return Swagger 中的自定义错误。但是当 returning 这个自定义错误时,我可以看到错误在哪一行。我该怎么做才能防止这种情况发生?
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 响应中只看到这条异常消息?
感谢您的帮助。
异常应该是异常的:不要为 non-exceptional 错误抛出异常。
我不建议在 C# 操作方法 return 类型中指定您的 web-service 的响应 DTO 类型,因为它会限制您的表达能力(正如您所发现的那样)。
- 而是使用
IActionResult
或 ActionResult<T>
来记录默认(即 HTTP 2xx)响应类型,然后在 [ProducesResponseType]
属性中列出错误 DTO 类型及其相应的 HTTP 状态代码。
- 这也意味着每个响应状态代码应该只与一个单个 DTO 类型相关联。
- 虽然 Swagger 的表现力不足以让您说“如果响应状态为 HTTP 200,则响应 body/DTO 为 one-of
DtoFoo
, DtoBar
, DtoQux
", in-practice一个well-designed web-service API 不应该表现出那种响应 DTO 多态性。
- 如果没有,客户端还应该如何从 HTTP headers 中知道类型是什么? (好吧,您可以将完整的 DTO type-name 放在自定义 HTTP 响应 header 中,但这会引入其他问题...)
- 对于错误情况,将错误添加到
ModelState
(如果可能,使用 Key
)和 let ASP.NET Core handle the rest for you with ProblemDetails
。
- 如果您确实抛出异常,那么 ASP.NET 核心 can be configured to automatically render it as a
ProblemDetails
- 或者它可以显示 DeveloperExceptionPage
- 或完全不同的东西。
- 我注意到 不 在控制器中为 non-exceptional 异常抛出异常的一个很好的理由是,您的日志记录框架可能会选择记录有关未处理异常的更多详细信息在 ASP.NET Core 的管道中,这将导致您的日志中出现无用的无关条目,从而更难找到您需要修复的“真实”异常。
- 使用
[ProducesResponseType]
记录使用的 DTO 及其相应的 HTTP 状态代码:这在使用 Swagger/NSwag 生成在线文档和客户端库时非常有用。
- 另外:不要将 EF 实体类型用作 DTO 或 ViewModels。
- 原因1:当响应(带有EF实体objects)被序列化时,具有lazy-loaded属性的实体将导致你的整个数据库object-graph 被序列化(因为 JSON 序列化器将遍历每个 object 的每个 属性)。
- 原因 2:安全!如果你直接接受一个 EF 实体作为输入请求 body DTO 或 HTML 表单模型那么 users/visitors 可以任意设置属性,例如例如
POST /users
和 { accessLevel: 'superAdmin' }
。虽然您可以排除或限制 object 的哪些属性可以通过请求设置,但它只会增加您项目的维护工作量(因为它是您的另一个 non-local、manually-written、列表或定义您需要确保的程序与 其他所有内容 . 一起保存 in-sync
- 原因 3:Self-documenting 意图:entity-type 用于 in-proc 状态,而不是通信合同。
- 原因 4:entity-type 的成员永远不会完全 您想要在 DTO 中公开的内容.
- 例如,您的
User
实体将具有 Byte[] PasswordHash
和 Byte[] PasswordSalt
属性 (我希望...),显然这两个属性必须 永远不会 暴露;但是在用于编辑用户的用户 DTO 中,您可能需要 不同的 成员,例如 NewPassword
和 ConfirmPassword
- 它们根本不映射到数据库列。
- 原因 5:在与原因 4 相关的说明中,使用实体 classes 作为 DTO 会自动绑定您的 web-service API 到你的数据库模型。
- 假设有一天您绝对需要更改您的数据库设计:也许有人告诉您业务需求已更改;这很正常,而且 一直都在发生。
- 假设数据库设计更改是从只允许每个客户 1 个地址(因为街道地址存储在与客户相同的 table 中)到允许客户有多个地址(即 street-address 列移动到不同的 table)...
- ...因此您更改了数据库,运行 迁移脚本,并部署到生产环境 - 但突然间您的所有 web-service 客户端都停止工作,因为他们都假定您的
Customer
object 有内联街道地址字段,但现在它们丢失了(因为您的 Customer
EF 实体类型不再有 street-address 列,这在 CustomerAddress
实体class).
- 如果您一直在使用专门用于
Customer
objects 的专用 DTO 类型,那么在更新应用程序设计的过程中,您会注意到构建会更快中断(而不是不可避免地稍后中断!) 由于您的 DTO-to-Entity(和 Entity-to-DTO)映射代码中的 C# compile-time type-checking - 这就是一个好处。
- 但主要的好处是它允许您完全 abstract-away 您的底层数据库设计 - 因此,在我们的示例中,如果您有依赖于 Custome 的远程客户端地址信息是内联的,那么您的客户 DTO 仍然可以通过在向远程客户端呈现其 JSON/XML/Protobuf 响应时将 第一个 客户地址内联到原始客户 DTO 中来模拟旧设计。这可以节省时间、麻烦、精力、金钱、压力、投诉、解雇、不必要的殴打、严重的身体伤害和预约牙科保健员的预约。
无论如何,我已按照上述指导修改了您发布的代码:
- 我添加了
[ProducesResponseType]
属性。
- 我明白指定默认响应类型
BookCreateDTO
两次是多余的(在 [ProducesResponseType]
和 ActionResult<BookCreateDTO>
中 - 您应该能够删除其中任何一个而不影响大摇大摆的输出。
- 为了安全起见,我添加了一个明确的
[FromBody]
。
- 如果“book-name 未使用”检查失败,它 return 是 ASP.NET 的库存
BadRequest
响应中的模型验证消息,呈现为IETF RFC 7807 响应,aka ProblemDetails
而不是 抛出异常然后希望您配置了 ASP.NET 核心管道(在 Configure()
中)将其作为 ProblemDetails
处理,而不是调用调试器或使用 DeveloperExceptionPage
。
- 请注意,在名称冲突的情况下,我们希望 return HTTP 409 冲突而不是 HTTP 400 错误请求,因此
conflictResult.StatusCode = 409;
被覆盖。
- 最终响应是通过 AutoMapper 和
Ok()
从新的 BookCreateDTO
实例生成的,而不是序列化您的 Book
实体 object。
[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 );
}
我现在尝试使用 .NET Web API 提高自己,并且我正在尝试 return Swagger 中的自定义错误。但是当 returning 这个自定义错误时,我可以看到错误在哪一行。我该怎么做才能防止这种情况发生?
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 响应中只看到这条异常消息? 感谢您的帮助。
异常应该是异常的:不要为 non-exceptional 错误抛出异常。
我不建议在 C# 操作方法 return 类型中指定您的 web-service 的响应 DTO 类型,因为它会限制您的表达能力(正如您所发现的那样)。
- 而是使用
IActionResult
或ActionResult<T>
来记录默认(即 HTTP 2xx)响应类型,然后在[ProducesResponseType]
属性中列出错误 DTO 类型及其相应的 HTTP 状态代码。- 这也意味着每个响应状态代码应该只与一个单个 DTO 类型相关联。
- 虽然 Swagger 的表现力不足以让您说“如果响应状态为 HTTP 200,则响应 body/DTO 为 one-of
DtoFoo
,DtoBar
,DtoQux
", in-practice一个well-designed web-service API 不应该表现出那种响应 DTO 多态性。- 如果没有,客户端还应该如何从 HTTP headers 中知道类型是什么? (好吧,您可以将完整的 DTO type-name 放在自定义 HTTP 响应 header 中,但这会引入其他问题...)
- 对于错误情况,将错误添加到
ModelState
(如果可能,使用Key
)和 let ASP.NET Core handle the rest for you withProblemDetails
。 - 如果您确实抛出异常,那么 ASP.NET 核心 can be configured to automatically render it as a
ProblemDetails
- 或者它可以显示DeveloperExceptionPage
- 或完全不同的东西。- 我注意到 不 在控制器中为 non-exceptional 异常抛出异常的一个很好的理由是,您的日志记录框架可能会选择记录有关未处理异常的更多详细信息在 ASP.NET Core 的管道中,这将导致您的日志中出现无用的无关条目,从而更难找到您需要修复的“真实”异常。
- 使用
[ProducesResponseType]
记录使用的 DTO 及其相应的 HTTP 状态代码:这在使用 Swagger/NSwag 生成在线文档和客户端库时非常有用。 - 另外:不要将 EF 实体类型用作 DTO 或 ViewModels。
- 原因1:当响应(带有EF实体objects)被序列化时,具有lazy-loaded属性的实体将导致你的整个数据库object-graph 被序列化(因为 JSON 序列化器将遍历每个 object 的每个 属性)。
- 原因 2:安全!如果你直接接受一个 EF 实体作为输入请求 body DTO 或 HTML 表单模型那么 users/visitors 可以任意设置属性,例如例如
POST /users
和{ accessLevel: 'superAdmin' }
。虽然您可以排除或限制 object 的哪些属性可以通过请求设置,但它只会增加您项目的维护工作量(因为它是您的另一个 non-local、manually-written、列表或定义您需要确保的程序与 其他所有内容 . 一起保存 in-sync
- 原因 3:Self-documenting 意图:entity-type 用于 in-proc 状态,而不是通信合同。
- 原因 4:entity-type 的成员永远不会完全 您想要在 DTO 中公开的内容.
- 例如,您的
User
实体将具有Byte[] PasswordHash
和Byte[] PasswordSalt
属性 (我希望...),显然这两个属性必须 永远不会 暴露;但是在用于编辑用户的用户 DTO 中,您可能需要 不同的 成员,例如NewPassword
和ConfirmPassword
- 它们根本不映射到数据库列。
- 例如,您的
- 原因 5:在与原因 4 相关的说明中,使用实体 classes 作为 DTO 会自动绑定您的 web-service API 到你的数据库模型。
- 假设有一天您绝对需要更改您的数据库设计:也许有人告诉您业务需求已更改;这很正常,而且 一直都在发生。
- 假设数据库设计更改是从只允许每个客户 1 个地址(因为街道地址存储在与客户相同的 table 中)到允许客户有多个地址(即 street-address 列移动到不同的 table)...
- ...因此您更改了数据库,运行 迁移脚本,并部署到生产环境 - 但突然间您的所有 web-service 客户端都停止工作,因为他们都假定您的
Customer
object 有内联街道地址字段,但现在它们丢失了(因为您的Customer
EF 实体类型不再有 street-address 列,这在CustomerAddress
实体class). - 如果您一直在使用专门用于
Customer
objects 的专用 DTO 类型,那么在更新应用程序设计的过程中,您会注意到构建会更快中断(而不是不可避免地稍后中断!) 由于您的 DTO-to-Entity(和 Entity-to-DTO)映射代码中的 C# compile-time type-checking - 这就是一个好处。 - 但主要的好处是它允许您完全 abstract-away 您的底层数据库设计 - 因此,在我们的示例中,如果您有依赖于 Custome 的远程客户端地址信息是内联的,那么您的客户 DTO 仍然可以通过在向远程客户端呈现其 JSON/XML/Protobuf 响应时将 第一个 客户地址内联到原始客户 DTO 中来模拟旧设计。这可以节省时间、麻烦、精力、金钱、压力、投诉、解雇、不必要的殴打、严重的身体伤害和预约牙科保健员的预约。
无论如何,我已按照上述指导修改了您发布的代码:
- 我添加了
[ProducesResponseType]
属性。- 我明白指定默认响应类型
BookCreateDTO
两次是多余的(在[ProducesResponseType]
和ActionResult<BookCreateDTO>
中 - 您应该能够删除其中任何一个而不影响大摇大摆的输出。
- 我明白指定默认响应类型
- 为了安全起见,我添加了一个明确的
[FromBody]
。 - 如果“book-name 未使用”检查失败,它 return 是 ASP.NET 的库存
BadRequest
响应中的模型验证消息,呈现为IETF RFC 7807 响应,akaProblemDetails
而不是 抛出异常然后希望您配置了 ASP.NET 核心管道(在Configure()
中)将其作为ProblemDetails
处理,而不是调用调试器或使用DeveloperExceptionPage
。- 请注意,在名称冲突的情况下,我们希望 return HTTP 409 冲突而不是 HTTP 400 错误请求,因此
conflictResult.StatusCode = 409;
被覆盖。
- 请注意,在名称冲突的情况下,我们希望 return HTTP 409 冲突而不是 HTTP 400 错误请求,因此
- 最终响应是通过 AutoMapper 和
Ok()
从新的BookCreateDTO
实例生成的,而不是序列化您的Book
实体 object。
[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 );
}