XiHan Framework 处于预览阶段

查看获取方式
跳转到内容

结果与错误 ​

本章说明框架如何表达成功与失败:原语层的 Result、Error 与 Maybe,错误码文法与种类,错误为什么只携带消息键,以及宿主如何把错误翻译成 HTTP Problem Details 等协议形态。适用于写操作、中间件与 Provider 的开发者,也供对接 API 的客户端参考。

框架没有统一响应信封,成功直接返回响应对象,失败返回 RFC 9457 Problem Details。

完整 API 见 Primitives 包。

Result 与 Result<T> ​

可恢复的失败(找不到、冲突、校验不通过、外部设施不可用)用 Result 表达,不抛异常。框架的端口都按这个约定返回:仓储的 AddAsync、缓存的 GetAsync、消息的 PublishAsync、操作的 HandleAsync。

类型用途
Result没有返回值的成功或携带 Error 的失败
Result<T>成功时携带值(不能为 null),失败时携带 Error
Maybe<T>类型层面的「可能没有」,default 即无值;不表达失败

创建与转换:

csharp
using XiHan.Framework.Primitives;

namespace Shop.Orders;

public static class OrderErrors
{
    public const string NotFound = "SHOP_NOTFOUND_001";
}

public static class OrderMessages
{
    public const string NotFound = "shop.order.not_found";
}

public sealed record OrderView(Guid Id, string Name);

public sealed class OrderLookup
{
    private readonly Dictionary<Guid, OrderView> _orders = [];

    public Result<OrderView> Find(Guid id)
    {
        if (_orders.TryGetValue(id, out var order))
        {
            // 值与错误都可以隐式转换成 Result<T>
            return order;
        }

        return Error.NotFound(OrderErrors.NotFound, OrderMessages.NotFound, MessageArguments.Of(("id", id)));
    }
}

读取结果:

成员说明
IsSuccess / IsFailure是否成功
Error失败时的错误,成功时为 null
TryGetValue(out value)成功时取出值
ValueOr(fallback) / ValueOrThrow()取值或回退;ValueOrThrow 在失败时抛出 FrameworkException
Map / Bind / Match链式转换
ToResult()Result<T> 去掉值
ThrowIfFailure()失败时抛出,只用于启动期、配置期这类不可恢复的场合

default(Result) 与 default(Result<T>) 视为未初始化的失败(PRIMITIVES_VALIDATION_002),用来发现忘记赋值的分支。

错误码 ​

text
ErrorCode = Domain "_" Kind "_" Sequence
Domain    = 术语表的错误码域(KERNEL、PERSISTENCE、SECURITY …)或应用自定义的大写标识
Kind      = VALIDATION | NOTFOUND | CONFLICT | DENIED | UNAVAILABLE | TIMEOUT | CANCELLED | UNEXPECTED | 领域自定义
Sequence  = 三位数字,域内唯一,发布后不复用

示例:PERSISTENCE_CONFLICT_001、SECURITY_DENIED_003、SHOP_NOTFOUND_001。ErrorCode.IsValid、ErrorCode.KindOf、ErrorCode.DomainOf 解析与校验错误码。

错误码面向程序,文案面向人:客户端只应依赖错误码,不解析文案。框架已登记的错误码以 XiHan Spec 的 conventions/error-codes.md 为准,各包页列出本包会返回的错误码。

Error ​

Error 不可变,由工厂方法创建,工厂方法校验错误码的 KIND 段与方法名一致:

工厂方法KIND
Error.Validation(code, messageKey, details)VALIDATION
Error.NotFound(code, messageKey, details)NOTFOUND
Error.Conflict(code, messageKey, details)CONFLICT
Error.Denied(code, messageKey, details)DENIED
Error.Unavailable(code, messageKey, details)UNAVAILABLE
Error.Unexpected(code, messageKey, inner, details)UNEXPECTED
new Error(code, messageKey, details, inner)其他种类(TIMEOUT、CANCELLED 与领域自定义)
成员说明
Code / Kind错误码与它的 KIND 段
MessageKey消息键,文案在语言文件里
Details参数与细节,名字到值的字典;既是模板参数,也随 Problem Details 输出标量项
Inner内部错误
Message按进程默认区域渲染的文案,供日志使用
WithCode(code)换码,保留消息键与参数

错误只携带消息键 ​

Error 的第二个参数是消息键常量,不是文案。文案放在所属包的 Localization/<区域>.json 里,宿主在 Egress 阶段按调用方的区域翻译:同一个错误,Accept-Language: zh-CN 的调用方收到中文,未指定的收到默认的英文。参数以 MessageArguments.Of(("name", value)) 传入,模板里写 {name};按位置的参数用 MessageArguments.Positional(...),模板里写 {0}。消息键文法、语言文件格式与 10 种内置区域见国际化。

Error.Literal(code, text) 携带现成的文字而不是消息键,只用于两种场合:从线上还原对端返回的错误、把异常转换成错误。正式包里 Error.* 的第二个参数必须是常量,架构测试强制这一点。

异常 ​

异常只表达编程错误与配置错误:参数为 null、清单不合法、配置值非法、启动时连不上必需的外部设施。它们在启动期暴露,不翻译成调用方语言:

类型用途
ArgumentException 及其子类参数与配置值非法
FrameworkException(code, message, inner)带错误码的框架异常;ToError() 转成 Error(文本以 Literal 承载)

调用期间未处理的异常由 Observe 阶段捕获,记录日志并转成 KERNEL_UNEXPECTED_001 结果,调用方收到 500,不会看到异常细节。雪花标识生成器的 PERSISTENCE_UNAVAILABLE_004 / 005 例外:SnowflakeIdFeature 在 Resilience 阶段把它们转成同码的 UNAVAILABLE 结果,调用方收到 503,见标识生成。

翻译成协议 ​

宿主读取管道结束后的结果,按错误码的 KIND 映射协议状态。HTTP 的映射(Hosting.AspNetCore 的 ProblemResponse.StatusCodeFor):

KINDHTTP 状态
VALIDATION400
NOTFOUND404
CONFLICT409
DENIED未认证(SECURITY_DENIED_001)与凭据无效(SECURITY_DENIED_003)为 401,并带 WWW-Authenticate 质询;其余为 403
UNAVAILABLE503
TIMEOUT504
CANCELLED408
UNEXPECTED 与其他500

截止时间到了一律是 KERNEL_TIMEOUT_001(504),调用方同时取消、下游把取消写成 KERNEL_CANCELLED_001 结果也一样;408 只表示调用方在截止时间到达之前自己取消(见韧性)。

Problem Details 的字段:

json
{
  "type": "urn:xihan:error:KERNEL_VALIDATION_003",
  "title": "VALIDATION",
  "status": 400,
  "detail": "操作 'greetings.create' 的请求不符合契约:$.name: required property 'name' is missing",
  "instance": "/greetings",
  "code": "KERNEL_VALIDATION_003",
  "traceId": "5978653a746fa20f0dce9f4a2f954bac",
  "errors": { "name": ["缺少必填属性 'name'"] },
  "details": { "id": "greetings.create", "reason": "$.name: required property 'name' is missing" }
}
字段内容
typeurn:xihan:error:<错误码>
title错误码的 KIND
statusHTTP 状态码
detail按调用方区域翻译的文案;响应头 Content-Language 给出命中的区域
instance请求路径
code错误码
traceId本次调用的 W3C trace-id
errors只在校验错误时出现:字段名到文案列表
detailsError.Details 里的标量项

gRPC、MCP 与命令行的映射分别见 gRPC、MCP 与 CloudEvents 与命令行宿主:gRPC 以状态码加 xihan-error-code 尾部元数据表达,MCP 以 isError 结果表达,命令行以按 KIND 的退出码加 stderr 上的 Problem JSON 表达。

在端点里返回错误 ​

最小 API 端点在 Handle 阶段内执行,把失败写进调用上下文,由宿主统一翻译:

csharp
using XiHan.Framework.Hosting.AspNetCore;
using XiHan.Framework.Hosting.AspNetCore.Endpoints;
using XiHan.Framework.Kernel.Pipeline;
using XiHan.Framework.Primitives;
using XiHan.Framework.Profiles.Minimal;

var builder = WebApplication.CreateBuilder(args);
builder.AddXiHan(xihan => xihan.UseMinimalProfile());

var app = builder.Build();
app.UseXiHan();

app.MapGet("/reports/{id}", (string id, HttpContext http) =>
{
    // 结果写进上下文,Egress 由宿主翻译为 Problem Details
    http.GetXiHanContext().ValueOrThrow().SetOutcome(
        Error.NotFound("SHOP_NOTFOUND_002", "shop.report.not_found", MessageArguments.Of(("id", id))));
    return Results.Empty;
});

app.Run();

操作契约不需要这样做:HandleAsync 返回的 Result 就是调用结果。

相关 ​

Released under The MIT License