XiHan Framework 处于预览阶段

查看获取方式
跳转到内容

ASP.NET Core 宿主 ​

这页讲 Hosting.AspNetCore 怎样把一个 ASP.NET Core 应用变成 XiHan 应用:在 WebApplicationBuilder 上装配特性,把每个 HTTP 请求翻译成调用上下文并送进内核管道,映射探针,把失败写成 Problem Details,按 Accept-Language 协商区域,以及 Native AOT 发布要做的两件事。读完这页,我们能用 WebApplication 承载一个 XiHan 应用,并且说得出一个请求从进入 Kestrel 到写出响应中间经过了什么。

前置知识:第一个应用、装配入口与执行管道。在端点里读调用上下文、给端点挂授权要求这两件事放在端点里使用框架,这页只做提及。

为什么需要 ​

ASP.NET Core 自己就能承载 Web 应用,但它不知道什么是调用上下文、特性与管道阶段:请求头里的 traceparent、baggage、截止时间与凭据需要有人翻译成内核能理解的上下文,失败结果需要有人统一写成客户端能解析的错误信封,Kubernetes 的探针需要有人把应用状态机暴露出去。没有宿主适配,每个应用都得自己写这一层,而且写法互不相同,客户端也无法用同一套规则解析错误。

Hosting.AspNetCore 把这层翻译收敛成两个调用:AddXiHan 负责装配,UseXiHan 负责把请求接进管道。宿主只做翻译,不做判定:认证、租户解析与授权都在管道的 Authenticate、Tenant、Authorize 阶段由对应子系统完成,宿主只把凭据和授权要求放进上下文。

宿主做什么 ​

Hosting.AspNetCore 是宿主适配,不是特性,所以它没有清单,也不提供能力 ID(包里另有一个可选的预签名地址特性,见分片、预签名与命名实例)。它负责三件事:

调用职责
builder.AddXiHan(...)在 WebApplicationBuilder 上装配 XiHan 应用:先装载挂载的配置目录,再把特性交给内核的 AddXiHan,应用与宿主共用服务集合与配置,随宿主一起启动与停止
app.UseXiHan()把每个 HTTP 请求翻译成调用上下文,让它之后的 ASP.NET Core 中间件与端点在内核管道的 Handle 阶段内执行;调用失败时写 RFC 9457 Problem Details
app.MapXiHanHealth()映射 startup / ready / live 三个探针,直接读应用状态机,不经管道

一个请求经过 UseXiHan() 时按下面的顺序处理:

  1. 匹配到的端点带 PipelineBypassMetadata(探针、OpenAPI 文档、.proto、gRPC 方法,以及应用自己标注的端点)时直接放行,不建调用上下文。
  2. 应用不在服务状态(Ready 或 Degraded)时直接回 503,错误码 KERNEL_UNAVAILABLE_001。
  3. 从请求头建调用上下文。端点元数据上的授权要求(RequireXiHan*)与操作描述(MapXiHanOperations 映射的端点)进入上下文;操作端点先把请求绑定成载荷,绑定失败直接写 Problem Details,不进管道。
  4. 经内核管道 Ingress → Observe → Resilience → Authenticate → Tenant → Authorize → Validate → Idempotency → Transaction → Handle → Egress 执行,ASP.NET Core 剩下的中间件与端点委托就是 Handle 阶段。端点抛出的异常由内核翻译成 KERNEL_UNEXPECTED_001。
  5. 调用结果是失败、且响应还没有开始写出时,宿主写 Problem Details;端点已经写出正文时保留端点的内容。

成功的调用宿主不改动响应,端点返回什么就写出什么。管道阶段的含义见执行管道。

最小示例 ​

  1. 引用 XiHan.Framework.Hosting.AspNetCore。Profiles.Minimal、Profiles.Standard、Profiles.Full 都已包含它,选组合包的办法见组合包与选包。
  2. 在 WebApplicationBuilder 上调用 AddXiHan(...) 装配特性。
  3. 调用 app.UseXiHan()。它必须在路由之后、端点之前:WebApplication 会在管道开头自动加上路由,按下例的位置调用即可。
  4. 需要探针时调用 app.MapXiHanHealth()。
bash
dotnet add package XiHan.Framework.Profiles.Standard --prerelease
csharp
using XiHan.Framework.Hosting.AspNetCore;
using XiHan.Framework.Profiles.Standard;

var builder = WebApplication.CreateBuilder(args);

// Standard 组合:内核、宿主与五个子系统的内存默认实现
builder.AddXiHan(xihan => xihan.UseStandardProfile());

var app = builder.Build();

app.UseXiHan();          // 之后映射的端点都在内核管道的 Handle 阶段内执行
app.MapXiHanHealth();    // /health/startup、/health/ready、/health/live,不经内核管道

app.MapGet("/", () => "ok");

app.Run();

下面的命令假设应用监听 http://localhost:5000(dotnet run -- --urls http://localhost:5000,或者在 appsettings.json 的 Kestrel:Endpoints 里配置):

bash
curl -s localhost:5000/
curl -s localhost:5000/health/ready

第一条得到 ok,第二条得到一段 JSON,status 是 pass,state 是 Ready,features 里列出每个已装配特性的状态。这个请求经过了完整的内核管道,只是没有任何阶段拒绝它;/health/ready 则不经管道,应用还没启动完也能访问。

装配时发生了什么 ​

WebApplicationBuilder 上的 AddXiHan 先装载环境变量 XIHAN_CONFIG_PATH 指向的目录(Kubernetes 里是 ConfigMap 的挂载点):appsettings.json 最先,其余顶层 *.json 按文件名序数随后,紧接 appsettings.{环境}.json 插入,并以 reloadOnChange 装载,IOptionsMonitor 随文件变化。然后交给内核的 AddXiHan,应用与宿主共用服务集合和配置。配置源的顺序见配置目录与热更新,挂载目录的部署方式见 Kubernetes。

和其他 ASP.NET Core 中间件一起用时有两条顺序约定:

  • 经反向代理部署时,在 UseXiHan() 之前调用 UseForwardedHeaders(),调用来源里的远端地址才是客户端地址。框架不自己解析 X-Forwarded-For,信任哪些代理由 ForwardedHeadersOptions 决定。
  • 给 SignalR Hub 这类绕过内核管道的端点做认证时,UseAuthentication()、UseAuthorization() 放在 UseXiHan() 之前,写法见端点里使用框架与实时通信。

请求如何变成调用上下文 ​

UseXiHan() 用 HttpInvocation.CreateContext(httpContext) 建上下文,映射规则来自 HTTP 载体规范:

请求调用上下文规则
traceparentTraceId、SpanId、TraceFlagsW3C 版本 00,采样标志原样保留(上游 -00 时内核 Span 不采样);缺失或格式不对时并入 ASP.NET Core 当前的请求 Activity,也没有时生成新的 trace-id
baggageBaggageW3C 格式;只保留已登记的键(xihan.correlation、xihan.idempotency-key、xihan.locale;xihan.tenant 与 xihan.principal 不收,调用方的租户与主体只来自认证),值按 URL 解码,未登记的 xihan. 键与其他键丢弃
xihan-deadlineDeadlineRFC 3339,按 UTC 解析;解析失败视为没有截止时间
grpc-timeoutDeadline只在没有 xihan-deadline 时生效;数值最多 8 位,单位 H、M、S、m、u、n,按宿主时钟换算成绝对时间
Accept-LanguageBaggage xihan.locale只在 Baggage 里没有 xihan.locale 时写入,见下文「区域协商与本地化」
Authorization凭据只拆成方案与值原样放入,由 Authenticate 阶段解释;宿主不验证凭据
方法、路径、连接调用来源 InvocationOrigin协议 http;目标是方法加路径(含 PathBase,不含查询串);远端地址、User-Agent、Referer;匹配到的端点的路由模板与 WithName 名字

截止时间在进入管道时已过的请求得到 504 KERNEL_TIMEOUT_001。调用来源只在本进程内可读,不随下游调用传播;访问日志与审计据此取客户端信息,与协议无关。上下文的字段含义与传播规则见调用上下文。

Authorization 头缺失时,只有登记在认证方案 XiHan 里的路径前缀下,查询串 access_token 才作为 Bearer 凭据,其他路径的查询串令牌一律忽略。这条规则只为浏览器的 WebSocket 与 EventSource 准备,见实时通信。

探针 ​

MapXiHanHealth(prefix) 映射三个探针端点,prefix 缺省为 /health。探针直接读应用状态机,带 PipelineBypassMetadata,应用未就绪时也能访问;通过时返回 200、status 为 pass,否则 503、status 为 fail。

端点通过条件
/health/startup全部特性已启动
/health/ready应用处于服务状态(Ready 或 Degraded)
/health/live应用状态不是 Failed

下面的响应只截取了 features 里的一条:

json
{
  "status": "pass",
  "state": "Ready",
  "features": [
    { "id": "xihan.samples.orders", "state": "Ready" }
  ],
  "contributions": []
}

响应体是 HealthResponse:state 是应用的聚合状态;features 每个特性一条(id、state,失败时有 error 错误码);contributions 是 IHealthContributor 上报的条目(如每个进程外插件),另带 description、checkedAt、durationMs 与 data,有才写。某个条目 Degraded 或 Failed 时应用聚合为 Degraded,仍在服务,/health/ready 仍通过。状态机与健康贡献见生命周期与健康,Kubernetes 探针的配置见 Kubernetes。

Problem Details ​

失败结果按 RFC 9457 写成 application/problem+json,标准成员之外有四个扩展成员(code、traceId、errors、details):

成员内容
typeurn:xihan:error:<错误码>
title错误码的 KIND 段,如 NOTFOUND
statusHTTP 状态码,按 KIND 映射,对照表见 HTTP 路由与载荷
detail按调用方区域翻译的文案;没有装配本地化时是 Error.Message
instance请求路径
code错误码,客户端只依赖它
traceId本次调用的 trace-id;应用未就绪的 503 还没有上下文,为空串
errors只在 VALIDATION 种类的错误上出现:字段键 → 消息数组
detailsError.Details 里的标量(字符串、布尔、数值、Guid、时间等,如 id、retryAfter);集合与对象不输出

以示例应用的 /missing 端点为例(它把一个 NotFound 错误写进上下文,写法见端点里使用框架):

bash
curl -s localhost:5000/missing -H "Accept-Language: zh-CN"
json
{
  "type": "urn:xihan:error:SAMPLES_NOTFOUND_001",
  "title": "NOTFOUND",
  "status": 404,
  "detail": "示例资源不存在。",
  "instance": "/missing",
  "code": "SAMPLES_NOTFOUND_001",
  "traceId": "4bf92f3577b34da6a3ce929d0e0e4736"
}

errors 的键是字段键:请求属性的序列化名,嵌套用点分路径与下标(lines[0].sku),根级为空串。值有两种来源:内核校验器按 Schema 产生的违规带 kernel.validation.* 消息键,与 detail 走同一条翻译通道;应用自校验(IValidatableObject)返回的文案原样输出。示例应用的下单请求名称为空、金额为 0 时,响应里的这一段是 {"errors":{"name":["长度必须 >= 1"],"amount":["值必须 >= 1"]}}。校验的写法见数据校验。

401 响应另带 WWW-Authenticate 质询,规则见 HTTP 路由与载荷。

ProblemResponse 公开了这套规则:FromError 构造响应体,StatusCodeFor 给出状态码,WriteTo 写出 JSON。绕过管道的端点要自己写错误、或访问日志要记状态码时用它们,结果与宿主写出的一致。Result 与 Error 本身的用法见结果与错误处理。

区域协商与本地化 ​

Accept-Language 的候选按权重降序排列(同权重保持原顺序),跳过 *、权重为 0 与格式不合法的项。调用方在 Baggage 里显式给了 xihan.locale 时以它为准,不看 Accept-Language。

装配了 localization.resources@1(Profiles.Minimal 起已包含 Localization.InMemory)时,候选与启用的区域协商:先找精确命中,再找同语言的第一个启用区域,都没有时取默认区域 en-US。没有装配本地化时取第一个合法候选,规范化后写进 Baggage,下游服务仍能拿到调用方语言。

Problem Details 的 detail 用消息目录翻译成协商出的区域,Content-Language 标出文案实际命中的区域:目录里没有这种语言时沿回退链取到默认区域,Content-Language 就是 en-US。code 不随区域变化。端点里直接调用 key.L() 也得到当前请求语言的文案。

应用自己的文案按消息键放进每种语言一个文件的消息目录,登记方式与框架包相同:

csharp
using XiHan.Framework.Localization.Abstractions;
using XiHan.Framework.Localization.Abstractions.Messages;

builder.Services.AddMessageCatalog(MessageCatalog.FromEmbeddedResources("samples", typeof(SampleMessages)));
xml
<ItemGroup>
  <EmbeddedResource Include="Localization/*.json" LogicalName="Localization/%(Filename)%(Extension)" />
</ItemGroup>

SampleMessages 是放消息键常量的静态类,Localization/zh-CN.json 这类文件里是键到模板的映射。消息键、区域配置与回退链见全球化和本地化。

Native AOT ​

Hosting.AspNetCore 的响应体都不经反射序列化:探针用源生成的序列化上下文,Problem Details 由 ProblemResponse.WriteTo 手写。应用要 Native AOT 发布时,自己的端点也照这两点做:

  1. 在项目里打开最小 API 的请求委托生成器,端点委托在编译期生成,不走反射绑定。
  2. 端点用到的请求与响应类型登记进源生成的 JsonSerializerContext,插到 HTTP JSON 选项解析链的最前面。
xml
<PropertyGroup>
  <EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
  <PublishAot>true</PublishAot>
</PropertyGroup>
csharp
using System.Text.Json.Serialization;

// 放在 builder.Build() 之前
builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default));

// camelCase 与不区分大小写:HTTP 适配器把路由值、查询串与请求体合并成 JSON 对象,按 JSON 约定匹配属性
// 允许从字符串读数字:路由值与查询串都是字符串
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString)]
[JsonSerializable(typeof(WhoAmI))]
internal sealed partial class AppJsonContext : JsonSerializerContext;

public sealed record WhoAmI(string Principal, bool IsAuthenticated, string? Tenant);

操作契约的请求与响应类型也登记在同一个上下文里,操作的 JSON Schema 由 Kernel.Generators 在编译期生成。示例应用就是这样以 Native AOT 发布的,它同时装配了 HTTP、gRPC、MCP 与 CloudEvents 四个协议适配器。序列化与裁剪的完整要求见 JSON 序列化与 Native AOT。

行为与错误码 ​

情形状态码错误码
应用未就绪或正在停止503KERNEL_UNAVAILABLE_001
进入管道时截止时间已过504KERNEL_TIMEOUT_001
端点抛出未处理异常500KERNEL_UNEXPECTED_001
操作端点的请求体不是 JSON 对象400KERNEL_VALIDATION_001
未认证 / 凭据无效、过期或签名不符401SECURITY_DENIED_001 / SECURITY_DENIED_003
策略拒绝 / 缺少权限403SECURITY_DENIED_002 / SECURITY_DENIED_004
授予存储、授予来源或收窄过滤器读不到数据(授权无法判定)503SECURITY_UNAVAILABLE_001
调用时才看到的要求引用的策略不存在(端点与契约上的引用在启动时核对)404SECURITY_NOTFOUND_001

常见问题 ​

UseXiHan() 到底放在哪里 ​

放在路由之后、端点映射之前。WebApplication 会自动在管道开头加路由,所以 builder.Build() 之后第一行写 app.UseXiHan() 就是对的。需要放在它前面的只有两类中间件:UseForwardedHeaders()(代理场景下取真实客户端地址),以及给绕过管道的端点服务的 UseAuthentication() / UseAuthorization()。

我已有的最小 API 端点要改写吗 ​

不用。UseXiHan() 之后映射的端点照常工作,只是多了一层:它们在内核管道的 Handle 阶段内执行,请求先经过认证、租户、授权与事务阶段,失败结果由宿主统一写成 Problem Details。要在端点里读主体、租户或返回框架错误时,再按端点里使用框架的写法取调用上下文。

应用还在启动时就有请求进来会怎样 ​

探针照常响应(/health/startup 与 /health/ready 返回 503、status 为 fail),其他请求直接得到 503 KERNEL_UNAVAILABLE_001,不进管道。应用正在停止时同样如此。Kubernetes 用 startup 与 readiness 探针配合,流量不会在这段时间进来,见 Kubernetes。

为什么 Problem Details 的 detail 是英文 ​

两种可能。一是没有装配本地化(localization.resources@1),detail 直接是 Error.Message。二是消息目录里没有请求语言的译文,沿回退链取到了默认区域 en-US,这时响应头 Content-Language 是 en-US。客户端判断错误类型只看 code,不要依赖 detail 的文字。

相关包与下一步 ​

Released under The MIT License