XiHan Framework 处于预览阶段

查看获取方式
跳转到内容

宿主与 Web 应用 ​

本页讲 ASP.NET Core 宿主怎样把 HTTP 请求接进内核管道:装配、请求到调用上下文的翻译、探针、区域协商、Problem Details、端点上的授权要求与 Native AOT,适用于用 WebApplication 承载 XiHan 应用的项目。完整 API 见 Hosting.AspNetCore 包。

宿主做什么 ​

Hosting.AspNetCore 是 Ring 4 的宿主适配,宿主本身不是特性(包里另有一个可选的预签名地址特性,见对象存储)。它负责三件事:在 WebApplicationBuilder 上装配 XiHan 应用(AddXiHan);把每个 HTTP 请求翻译成调用上下文,让其余 ASP.NET Core 管道在内核管道的 Handle 阶段内执行(UseXiHan);把失败结果写成 RFC 9457 Problem Details。认证、租户解析与授权判定不在宿主里:宿主只把凭据和授权要求放进上下文,由安全与多租户子系统在 Authenticate、Tenant、Authorize 阶段处理。

一个请求经过 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()。
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();

WebApplicationBuilder 上的 AddXiHan 先装载环境变量 XIHAN_CONFIG_PATH 指向的目录(Kubernetes 里是 ConfigMap 的挂载点):appsettings.json 最先,其余顶层 *.json 按文件名序数随后;这些文件紧接 appsettings.{环境}.json 插入(覆盖 appsettings,环境变量与命令行参数覆盖它们,顺序见配置与选项),并以 reloadOnChange 装载,IOptionsMonitor 随文件变化。然后交给内核的 AddXiHan:XiHan 应用与宿主共用服务集合和配置,随宿主启动与停止。配置的写法见配置与选项,挂载目录的部署方式见部署与打包。

和其他 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 不收,调用方的租户与主体只来自认证,ADR-0121),值按 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 名字(ADR-0043、ADR-0048)

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

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

探针 ​

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

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

下面的响应只截取了 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 探针的配置见部署与打包。

在端点里读调用上下文 ​

经过 UseXiHan() 的请求,端点里用 HttpContext.GetXiHanContext() 取当前调用上下文,返回 Maybe<InvocationContext>;请求没有经过 UseXiHan()(绕过管道的端点、或者没有调用 UseXiHan())时为无值。端点在 Handle 阶段执行,此时 Authenticate 与 Tenant 阶段已经写好安全上下文,GetSecurity() 取主体与租户;SetOutcome(...) 把失败结果写回上下文,由宿主写成 Problem Details。

下例接上一节的 Program.cs,写法来自 samples/SimpleApp:

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

app.MapGet("/whoami", (HttpContext http) =>
{
    var security = http.GetXiHanContext().ValueOrThrow().GetSecurity();
    return new WhoAmI(security.Principal.Id, security.Principal.IsAuthenticated, security.Tenant?.Id);
});

app.MapGet("/missing", (HttpContext http) =>
{
    // 不抛异常:失败结果留在上下文里,管道结束后宿主写成 Problem Details
    http.GetXiHanContext().ValueOrThrow().SetOutcome(Error.NotFound("SAMPLES_NOTFOUND_001", SampleMessages.ResourceMissing));
    return Results.Empty;
});

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

public static class SampleMessages
{
    public const string ResourceMissing = "samples.resource.missing";
}

调用链深处拿不到 HttpContext 的服务(领域服务、仓储装饰器)不要依赖 HTTP,改用作用域服务 IInvocationAccessor.Current 取当前调用的上下文,见执行管道。

端点上的授权要求 ​

普通的最小 API 端点用三个约定扩展挂授权要求,UseXiHan() 把它放进上下文,Authorize 阶段判定:

扩展要求
RequireXiHanAuthentication()已认证
RequireXiHanPermission(permissionCode, resource)持有权限码;resource 可选,限定到某个资源
RequireXiHanPolicy(policyName, resource)命名策略允许;resource 可选
csharp
using XiHan.Framework.Hosting.AspNetCore.Endpoints;

app.MapGet("/orders/stats", (OrderEventCounter counter) => new OrderStatistics(counter.Handled))
    .RequireXiHanAuthentication();

app.MapGet("/orders/export", () => Results.Text("id,name"))
    .RequireXiHanPermission("orders.read");

OrderEventCounter 与 OrderStatistics 是 samples/SimpleApp 里的类型。判定由安全子系统完成:没有装配 security.authorization@1 时要求只进入上下文,不被判定。权限码必须由某个特性用 FeatureContext.AddPermission 登记;装配了授权能力时,UseXiHan() 在宿主建请求管道时核对全部端点的要求,未登记的权限码与不存在的策略让宿主启动失败(SECURITY_VALIDATION_004,一次列出全部,ADR-0080)。端点上的要求优先于操作契约上的权限或策略。认证与授权的配置见认证与授权。

Problem Details ​

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

成员内容
typeurn:xihan:error:<错误码>
title错误码的 KIND 段,如 NOTFOUND
statusHTTP 状态码,按 KIND 映射,对照表见状态码与错误映射
detail按调用方区域翻译的文案;没有装配本地化时是 Error.Message
instance请求路径
code错误码,客户端只依赖它
traceId本次调用的 trace-id;应用未就绪的 503 还没有上下文,为空串
errors只在 VALIDATION 种类的错误上出现:字段键 → 消息数组
detailsError.Details 里的标量(字符串、布尔、数值、Guid、时间等,如 id、retryAfter);集合与对象不输出
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)返回的文案原样输出。samples/SimpleApp 的下单请求名称为空、金额为 0 时,响应里的这一段是 {"errors":{"name":["长度必须 >= 1"],"amount":["值必须 >= 1"]}}。校验的写法见数据校验。

401 响应另带 WWW-Authenticate 质询,规则见状态码与错误映射。

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

区域协商与本地化 ​

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>

消息键、区域配置与回退链见国际化。

Native AOT ​

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

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

操作契约的请求与响应类型也登记在同一个上下文里,操作的 JSON Schema 由 Kernel.Generators 在编译期生成。仓库 CI 以 dotnet publish framework/samples/SimpleApp/SimpleApp.csproj 做 Native AOT 冒烟(PublishAot 在项目文件里声明),示例装配了 HTTP、gRPC、MCP 与 CloudEvents 四个协议适配器。序列化与裁剪的完整要求见序列化与 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
调用时才看到的要求(RequireAuthorization 显式写入的、启动之后才加进来的端点)引用的策略不存在(端点元数据与操作契约的引用在启动时核对,不合格启动失败)404SECURITY_NOTFOUND_001

相关 ​

Released under The MIT License