# FriendlyException 友好异常
FriendlyException 用于在业务代码中创建稳定、可识别、不会暴露内部细节的友好异常。日常开发里主要通过 Oops 使用,配合错误码、错误消息和 HTTP 状态码让前端获得一致的异常语义。
如果要了解全局异常过滤器、异常消息映射、统一返回结构和异常处理器,请看 异常处理。
# 能力边界
FriendlyException 负责:
- 创建业务异常和普通友好异常。
- 支持错误码、错误消息和 HTTP 状态码。
- 支持错误码枚举和错误码消息配置。
- 为统一返回和全局异常处理提供异常元数据。
FriendlyException 不负责:
- 直接定义最终 JSON 响应结构。
- 替代业务层的规则校验。
- 替代
IUnifyResultProvider的响应包装职责。 - 替代
IGlobalExceptionHandler的日志、监控和审计职责。
# 注册方式
通常不需要单独注册,AddWebAppCore() 和 AddWebApp() 会启用友好异常能力。
using Air.Cloud.WebApp.Extensions;
builder.Services
.AddControllers()
.AddWebApp();
如需单独启用:
using Air.Cloud.WebApp.FriendlyException.Extensions;
builder.Services
.AddControllers()
.AddFriendlyException();
# FriendlyExceptionSettings
配置节点为 FriendlyExceptionSettings。
{
"FriendlyExceptionSettings": {
"HideErrorCode": false,
"DefaultErrorCode": "",
"DefaultErrorMessage": "Internal Server Error",
"ThrowBah": false
}
}
| 配置项 | 默认值 | 说明 |
|---|---|---|
HideErrorCode | false | 拼装错误消息时是否隐藏错误码前缀 |
DefaultErrorCode | 空字符串 | 默认错误码 |
DefaultErrorMessage | Internal Server Error | 默认错误消息 |
ThrowBah | false | 是否把 Oops.Oh(...) 也按业务验证异常处理 |
# Oops 使用方式
Oops 是业务代码中最常用的友好异常入口。推荐按语义选择:
| 方法 | 适用场景 | 默认状态码 | 是否验证异常 |
|---|---|---|---|
Oops.Bah(...) | 业务规则失败、参数或状态不满足要求 | 400 | 是 |
Oops.Oh(...) | 系统级友好异常、流程异常、不可预期异常 | 500 | 否 |
如果 FriendlyExceptionSettings.ThrowBah 配置为 true,Oops.Oh(...) 也会按业务验证异常处理。
# 抛出业务验证异常
业务规则失败时使用 Oops.Bah(...)。
using Air.Cloud.WebApp.FriendlyException;
public object Create(CreateUserRequest request)
{
if (request.Name == "admin")
{
throw Oops.Bah("用户名 admin 已被保留,不能使用");
}
return new { Id = 1 };
}
Oops.Bah(...) 会:
- 设置 HTTP 状态码为
400。 - 标记为验证异常。
- 使用
ValidationException作为内部异常。 - 在统一返回启用时走
OnValidateFailed()语义。
也可以使用格式化参数:
throw Oops.Bah("用户 {0} 不允许登录", userName);
# 抛出普通友好异常
普通系统级异常使用 Oops.Oh(...)。
throw Oops.Oh("导入任务执行失败:{0}", taskId);
默认行为:
- HTTP 状态码为
500。 - 不标记为验证异常。
- 由
IUnifyResultProvider.OnException()包装最终响应。
如果需要指定内部异常类型:
throw Oops.Oh<InvalidOperationException>("订单状态不允许执行该操作");
或:
throw Oops.Oh("订单状态不允许执行该操作", typeof(InvalidOperationException));
指定内部异常类型后,AppFriendlyException.InnerException 会使用这个类型。全局异常映射会优先用内部异常类型参与 IfExceptionAttribute 匹配。
# 使用错误码
可以直接传入字符串错误码:
throw Oops.Bah("USER_NAME_RESERVED");
throw Oops.Oh("ORDER_IMPORT_FAILED");
默认消息会按 FriendlyExceptionSettings.DefaultErrorMessage 拼装,并带上错误码前缀。
也可以通过 ErrorCodeMessageSettings 配置错误码消息:
{
"ErrorCodeMessageSettings": {
"Definitions": [
[ "USER_NAME_RESERVED", "用户名已被保留" ],
[ "ORDER_STATUS_INVALID", "订单状态不允许执行该操作" ]
]
}
}
之后可以直接:
throw Oops.Bah("USER_NAME_RESERVED");
错误码消息解析顺序:
- 当前方法或类上的
[IfException("CODE", ErrorMessage = "...")]。 ErrorCodeMessageSettings配置。[ErrorCodeType]枚举上的[ErrorCodeItemMetadata]。FriendlyExceptionSettings.DefaultErrorMessage。
# 自定义错误码枚举
定义错误码枚举:
using Air.Cloud.WebApp.FriendlyException.Attributes;
[ErrorCodeType]
public enum UserErrorCodes
{
[ErrorCodeItemMetadata("用户名已被保留", ErrorCode = "USER_NAME_RESERVED")]
UserNameReserved
}
使用:
throw Oops.Bah(UserErrorCodes.UserNameReserved);
如果错误码枚举不在默认扫描范围内,可以实现 IErrorCodeTypeProvider 并注册:
using Air.Cloud.WebApp.FriendlyException.Providers;
public sealed class MyErrorCodeTypeProvider : IErrorCodeTypeProvider
{
public IEnumerable<Type> Definitions => new[]
{
typeof(UserErrorCodes)
};
}
builder.Services
.AddControllers()
.AddFriendlyException<MyErrorCodeTypeProvider>();
# 自定义状态码
AppFriendlyException 可以通过扩展方法设置状态码:
using Air.Cloud.WebApp.FriendlyException;
using Air.Cloud.WebApp.FriendlyException.Extensions;
using Microsoft.AspNetCore.Http;
throw Oops.Oh("资源不存在")
.StatusCode(StatusCodes.Status404NotFound);
StatusCode(...) 会修改当前 AppFriendlyException.StatusCode 并返回同一个异常实例,适合链式调用。
# 推荐实践
- 业务规则失败优先使用
Oops.Bah(...)。 - 系统异常和不可预期异常使用
Oops.Oh(...)或直接抛原始异常。 - 前端需要稳定识别业务错误时,使用
errorCode。 - 不要把内部异常堆栈、数据库错误、第三方响应明细直接暴露给前端。
- 不要在业务代码里手工拼最终 JSON,最终结构交给
IUnifyResultProvider。