# Mud.HttpUtils
**Repository Path**: mudtools/MudHttpUtils
## Basic Information
- **Project Name**: Mud.HttpUtils
- **Description**: Mud.HttpUtils 是一个基于 Roslyn 源代码生成器的声明式 HTTP 客户端框架, Native AOT 兼容,通过特性标注的方式自动生成类型安全的 HTTP API 客户端代码。无需手写 HttpClient 调用代码,只需定义接口并添加特性标注,编译器会自动生成完整的实现代码。
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: master
- **Homepage**: https://www.mudtools.cn/
- **GVP Project**: No
## Statistics
- **Stars**: 2
- **Forks**: 0
- **Created**: 2026-04-11
- **Last Updated**: 2026-10-01
## Categories & Tags
**Categories**: Uncategorized
**Tags**: HttpClient, WebApi, roslyn, dotNET, aot
## README
# Mud.HttpUtils
[](https://www.nuget.org/packages/Mud.HttpUtils/ "Mud.HttpUtils") [](https://www.nuget.org/packages/Mud.HttpUtils/ "downloads")
[](https://www.nuget.org/packages/Mud.HttpUtils.Abstractions/ "Mud.HttpUtils.Abstractions") [](https://www.nuget.org/packages/Mud.HttpUtils.Abstractions/ "downloads")
[](https://www.nuget.org/packages/Mud.HttpUtils.Attributes/ "Mud.HttpUtils.Attributes") [](https://www.nuget.org/packages/Mud.HttpUtils.Attributes/ "downloads")
[](https://www.nuget.org/packages/Mud.HttpUtils.Client/ "Mud.HttpUtils.Client") [](https://www.nuget.org/packages/Mud.HttpUtils.Client/ "downloads")
[](https://www.nuget.org/packages/Mud.HttpUtils.Resilience/ "Mud.HttpUtils.Resilience") [](https://www.nuget.org/packages/Mud.HttpUtils.Resilience/ "downloads")
[](https://www.nuget.org/packages/Mud.HttpUtils.Generator/ "Mud.HttpUtils.Generator") [](https://www.nuget.org/packages/Mud.HttpUtils.Generator/ "downloads")
[](https://www.nuget.org/packages/Mud.HttpUtils.OpenTelemetry/ "Mud.HttpUtils.OpenTelemetry") [](https://www.nuget.org/packages/Mud.HttpUtils.OpenTelemetry/ "downloads")
[](https://www.nuget.org/packages/Mud.HttpUtils.Newtonsoft.Json/ "Mud.HttpUtils.Newtonsoft.Json") [](https://www.nuget.org/packages/Mud.HttpUtils.Newtonsoft.Json/ "downloads")
[](https://www.nuget.org/packages/Mud.HttpUtils.Xml/ "Mud.HttpUtils.Xml") [](https://www.nuget.org/packages/Mud.HttpUtils.Xml/ "downloads")
[](https://www.nuget.org/packages/Mud.HttpUtils.JsonContextScaffolder/ "Mud.HttpUtils.JsonContextScaffolder") [](https://www.nuget.org/packages/Mud.HttpUtils.JsonContextScaffolder/ "downloads")
[](LICENSE)
**基于 Roslyn 的声明式 HTTP 客户端源代码生成器**
### 📖 项目简介
Mud.HttpUtils 是一个基于 Roslyn 源代码生成器的声明式 HTTP 客户端框架,Native AOT 兼容,通过特性标注的方式自动生成类型安全的 HTTP API 客户端代码。无需手写 HttpClient 调用代码,只需定义接口并添加特性标注,编译器会自动生成完整的实现代码。
### ✨ 核心特性
- 🚀 **编译时生成,最小化运行时开销**:编译时生成代码,核心路径零反射,性能优异。QueryMap 嵌套复杂类型已支持递归编译期展平(AOT 安全),仅极端深度回退到反射
- 🎯 **类型安全**:强类型 API 调用,编译时检查错误
- 🚀 **Native AOT 兼容**:统一序列化抽象 `IHttpContentSerializer` + `JsonContextScaffolder` 脚手架自动生成 `JsonSerializerContext` + `AOT001`–`AOT007` 编译期诊断(CI 严格模式可升级为 Error),实现零反射 Native AOT 构建
- 🔗 **无 DI 入口**:通过 `RestService.ForGenerated(HttpClient, GeneratedClientOptions?)` 在不依赖 DI 容器的场景下创建 AOT 安全的客户端实例,配合 `[ModuleInitializer]` 自动注册工厂
- 📝 **声明式编程**:通过特性标注定义 HTTP API,简洁直观
- 🔧 **功能丰富**:支持多种 HTTP 方法、参数类型、内容格式、Token 认证、加密传输等
- 🛡️ **弹性策略**:内置重试、超时、熔断策略,基于 Polly 实现
- 🔐 **加密支持**:可插拔的加密提供程序,内置 AES 加密实现
- 🔄 **令牌管理**:并发安全的令牌刷新,支持持久化存储契约,内置内存存储默认实现
- 🌐 **多客户端**:支持多命名客户端场景,通过 `IHttpClientResolver` 动态解析
- 🎨 **灵活配置**:支持接口级、方法级、参数级的配置优先级
- 🏗️ **接口级动态属性**:在接口上定义 `[Query]`/`[Path]` 属性,实现全局参数
- 🗺️ **QueryMap 参数映射**:将对象/字典展开为查询参数,支持序列化控制
- 🔗 **Base Path 支持**:在接口级别定义统一路径前缀
- 📦 **Response\ 包装类型**:同时返回响应内容和元数据
- 🤖 **默认参数推断**:未标注特性的参数根据类型自动推断为 `[Query]`(简单类型)或 `[Body]`(复杂类型)
- 🔀 **头部合并控制**:通过 `[HeaderMerge]` 控制接口级与方法级同名头部的合并策略
- 📐 **序列化方法控制**:通过 `[SerializationMethod]` 指定接口或方法级别的请求体序列化方式(JSON/XML/FormUrlEncoded)
- 📊 **OpenTelemetry 可观测性**:内置分布式追踪与指标采集,一键开启
- 📦 **多框架支持**:支持 .NET Standard 2.0、.NET 6.0、.NET 8.0、.NET 10.0
### 📦 NuGet 包
| 组件 | 描述 | NuGet | 下载 |
| --------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| **Mud.HttpUtils** | 元包:Abstractions + Attributes + Client + Resilience | [](https://www.nuget.org/packages/Mud.HttpUtils/) |  |
| **Mud.HttpUtils.Abstractions** | 纯接口定义,最小依赖 | [](https://www.nuget.org/packages/Mud.HttpUtils.Abstractions/) |  |
| **Mud.HttpUtils.Attributes** | 特性定义 | [](https://www.nuget.org/packages/Mud.HttpUtils.Attributes/) |  |
| **Mud.HttpUtils.Client** | 客户端实现 + DI 注册 | [](https://www.nuget.org/packages/Mud.HttpUtils.Client/) |  |
| **Mud.HttpUtils.Resilience** | 弹性策略(Polly) | [](https://www.nuget.org/packages/Mud.HttpUtils.Resilience/) |  |
| **Mud.HttpUtils.Generator** | 源代码生成器(已内置接口规范分析器与代码修复) | [](https://www.nuget.org/packages/Mud.HttpUtils.Generator/) |  |
| **Mud.HttpUtils.OpenTelemetry** | OpenTelemetry 可观测性适配 | [](https://www.nuget.org/packages/Mud.HttpUtils.OpenTelemetry/) |  |
| **Mud.HttpUtils.Newtonsoft.Json** | Newtonsoft.Json 序列化器适配 | [](https://www.nuget.org/packages/Mud.HttpUtils.Newtonsoft.Json/) |  |
| **Mud.HttpUtils.Xml** | XML 序列化器适配 | [](https://www.nuget.org/packages/Mud.HttpUtils.Xml/) |  |
| **Mud.HttpUtils.JsonContextScaffolder** | JsonSerializerContext 脚手架工具 | [](https://www.nuget.org/packages/Mud.HttpUtils.JsonContextScaffolder/) |  |
> 说明:上表仅列出已发布到 NuGet 的包。`Mud.HttpUtils.Testing` 为测试辅助库,**不随版本发布**;`Mud.HttpUtils.Analyzers` 与 `Mud.HttpUtils.CodeFixes` **已合并进 `Mud.HttpUtils.Generator`**,随源生成器包一并发布,无需单独安装。
### 🏛️ 系统架构
Mud.HttpUtils 采用**编译时生成 + 运行时装饰**的分层架构:开发者仅声明带特性的接口,源代码生成器在编译期为每个接口生成强类型实现;运行时通过装饰器链叠加弹性策略、令牌管理、加解密与可观测性。
#### 分层架构
```mermaid
graph TB
subgraph Consumer["① 使用者代码"]
IFace["声明式接口
IUserApi(特性标注)"]
Svc["业务服务注入 IUserApi"]
end
subgraph Compile["② 编译时(Mud.HttpUtils.Generator)"]
SG["HttpInvokeClassSourceGenerator
+ RegistrationGenerator"]
end
subgraph Runtime["③ 运行时核心"]
Abs["Mud.HttpUtils.Abstractions
接口 / 契约 / 可观测性静态源"]
Attr["Mud.HttpUtils.Attributes
特性定义"]
Client["Mud.HttpUtils.Client
EnhancedHttpClient · 令牌管理
加解密 · 命名客户端解析器"]
Res["Mud.HttpUtils.Resilience
ResilientHttpClient(Polly 装饰器)"]
OTel["Mud.HttpUtils.OpenTelemetry
Tracing + Metrics 导出"]
end
Meta["Mud.HttpUtils(元包)
AddMudHttpUtils 一站式注册"]
IFace -->|"编译时扫描特性"| SG
SG -->|"生成实现类 UserApi(.Internal 命名空间)"| Svc
Svc --> IFace
Meta -. "聚合引用" .- Abs
Meta -.- Attr
Meta -.- Client
Meta -.- Res
Attr -. "依赖" .- Abs
Client -. "实现接口" .- Abs
Res -. "装饰 IEnhancedHttpClient" .- Client
Res -.- Abs
OTel -. "采集可观测性源" .- Abs
SG -. "引用" .- Abs
SG -.- Attr
```
#### 一次 HTTP 请求的调用链路
```mermaid
sequenceDiagram
participant B as 业务服务
participant G as 生成代码 UserApi(.Internal)
participant X as IHttpRequestExecutor
participant T as Token / 加解密 / 拦截器
participant R as IResiliencePolicyResolver
participant RC as ResilientHttpClient(装饰器)
participant H as EnhancedHttpClient
participant N as HttpClient(System.Net.Http)
B->>G: 调用 GetUserAsync(id)
G->>X: 构建请求并执行
X->>T: 注入 Token、头部合并、加解密
X->>R: 解析方法级 / 全局弹性策略
R-->>X: ResiliencePipeline
X->>RC: 经弹性装饰执行
RC->>H: 重试 / 超时 / 熔断外壳
H->>N: 发送 HttpRequestMessage
N-->>H: HttpResponseMessage
H-->>X: 响应拦截 / 反序列化
X-->>G: 返回 T / Response
G-->>B: 结果
```
> **关键设计**:
>
> - **零运行时反射**:生成代码直接调用 `IHttpRequestExecutor` 与 `IEnhancedHttpClient`,核心路径无反射(仅 `FormUrlEncoded` Body、`QueryMap` 复杂类型、XML 序列化等少数场景保留反射)。
> - **装饰器叠加**:`ResilientHttpClient` 实现 `IEnhancedHttpClient` 并包装内层客户端,因此弹性策略、令牌恢复、追踪等能力可逐层叠加而不侵入业务接口。
> - **多租户隔离**:`IAppContextHolder` / `IAppManager` / `AppResiliencePolicyResolver` 为不同 App 维护独立的上下文与弹性策略。多租户场景**必须**调用 `AddMudHttpAppContextHolder()`(或使用配置入口 `AddMudHttpClientsFromConfiguration` 自动补齐)+ `AddMudHttpAppResilience(...)` 才能获得 per-app 弹性隔离;多租户场景**必须**注册 `IAppAccessAuthorizer` 防止跨租户越权。
### 🚀 快速开始
#### 1. 安装 NuGet 包
```bash
# 安装元包(包含 Abstractions + Attributes + Client + Resilience)
dotnet add package Mud.HttpUtils
# 安装源代码生成器
dotnet add package Mud.HttpUtils.Generator
```
#### 2. 定义 API 接口
```csharp
using Mud.HttpUtils.Attributes;
[HttpClientApi(HttpClient = "IEnhancedHttpClient")]
public interface IUserApi
{
[Get("/users/{id}")]
Task GetUserAsync([Path] int id);
[Post("/users")]
Task CreateUserAsync([Body] CreateUserRequest request);
[Get("/users")]
Task> GetUsersAsync(
[Query] string? name = null,
[Query] int page = 1,
[Query] int pageSize = 20
);
[Put("/users/{id}")]
Task UpdateUserAsync([Path] int id, [Body] UpdateUserRequest request);
[Delete("/users/{id}")]
Task DeleteUserAsync([Path] int id);
[Post("/upload")]
Task UploadAsync([Upload] IFormFile file);
[Post("/login")]
Task LoginAsync([Form(FieldName = "username")] string user, [Form(FieldName = "password")] string pass);
}
```
#### 3. 注册服务
```csharp
// 一站式注册:Client + 弹性策略
services.AddMudHttpUtils("userApi", "https://api.example.com", options =>
{
options.Retry.MaxRetryAttempts = 3;
options.Timeout.TimeoutSeconds = 30;
});
// 注册生成器生成的 API 接口
services.AddWebApiHttpClient();
```
#### 4. 使用 API
```csharp
public class UserService
{
private readonly IUserApi _userApi;
public UserService(IUserApi userApi)
{
_userApi = userApi;
}
public async Task GetUserByIdAsync(int id)
{
return await _userApi.GetUserAsync(id);
}
}
```
### 🎯 功能特性
#### HTTP 方法支持
- `[Get]` - GET 请求
- `[Post]` - POST 请求
- `[Put]` - PUT 请求
- `[Delete]` - DELETE 请求(支持带请求体)
- `[Patch]` - PATCH 请求
- `[Head]` - HEAD 请求
- `[Options]` - OPTIONS 请求
#### 参数类型
| 特性 | 说明 | 示例 |
| --------------------------------- | ------------------------------------------------ | -------------------------------------------------- |
| `[Path]` | URL 路径参数 | `[Get("/users/{id}")]` + `[Path] int id` |
| `[Query]` | URL 查询参数 | `[Query] string? name` |
| `[QueryMap]` | 查询参数映射(对象/字典展开为查询参数) | `[QueryMap] SearchCriteria criteria` |
| `[ArrayQuery]` | 数组查询参数 | `[ArrayQuery] int[] ids` |
| `[RawQueryString]` | 原始查询字符串 | `[RawQueryString] string queryString` |
| `[Header]` | HTTP 请求头(支持参数/方法/接口级别) | `[Header("X-API-Key")] string apiKey` |
| `[Body]` | 请求体 | `[Body] UserRequest request` |
| `[Body(RawString = true)]` | 原始字符串请求体 | `[Body(RawString = true)] string content` |
| `[Body(UseStringContent = true)]` | 字符串内容请求体 | `[Body(UseStringContent = true)] string content` |
| `[FormContent]` | 表单数据 | `[FormContent] IFormContent formData` |
| `[Form]` | 表单字段(`application/x-www-form-urlencoded`) | `[Form(FieldName = "username")] string user` |
| `[MultipartForm]` | 多部分表单字段(`multipart/form-data`) | `[MultipartForm] IFormFile file` |
| `[Upload]` | 文件上传参数(支持自定义字段名/文件名/内容类型) | `[Upload(FieldName = "doc")] IFormFile file` |
| `[FilePath]` | 文件下载路径 | `[FilePath] string savePath` |
| `[Token]` | Token 认证(支持参数/接口/方法级别) | `[Token("UserAccessToken")] string token` |
| `[Retry]` | 方法级重试策略标注 | `[Retry(MaxRetries = 3)]` |
| `[Timeout]` | 方法级超时策略标注 | `[Timeout(30000)]` |
| `[CircuitBreaker]` | 方法级熔断策略标注 | `[CircuitBreaker(FailureThreshold = 5)]` |
| `[HeaderMerge]` | 头部合并模式控制(接口/方法级别) | `[HeaderMerge(HeaderMergeMode.Replace)]` |
| `[SerializationMethod]` | 请求体序列化方法控制(接口/方法级别) | `[SerializationMethod(SerializationMethod.Xml)]` |
| `[InterfacePath]` | 接口级固定路径参数 | `[InterfacePath("tenantId", "default")]` |
| `[InterfaceQuery]` | 接口级固定查询参数 | `[InterfaceQuery("version", "2.0")]` |
| `[AllowAnyStatusCode]` | 允许任意 HTTP 状态码(不抛异常) | `[AllowAnyStatusCode]` |
#### 内容类型管理
支持三级配置,优先级从高到低:
```
Body 参数级 > 方法级 > 接口级 > 默认值 (application/json)
```
#### 请求头(Header)
`[Header]` 特性支持应用到参数、方法或接口级别:
```csharp
// 参数级别
[Get("/users")]
Task> GetUsersAsync([Header("X-API-Key")] string apiKey);
// 方法级别(添加固定请求头)
[Get("/users")]
[Header("Accept", "application/json")]
[Header("X-Request-Source", "Web")]
Task> GetUsersAsync();
// 接口级别(所有方法自动携带)
[HttpClientApi]
[Header("X-API-Version", "v2")]
public interface IUserApi { }
```
`HeaderAttribute` 支持 `AliasAs`(别名映射)和 `Replace`(替换模式)属性。
#### 弹性策略
基于 Polly 的弹性策略,通过装饰器模式包装 HTTP 客户端:
| 策略 | 默认状态 | 说明 |
| ---- | -------- | ------------------------------ |
| 重试 | 启用 | 默认 3 次重试,支持指数退避 |
| 超时 | 启用 | 默认 30 秒,悲观超时策略 |
| 熔断 | 关闭 | 连续失败阈值触发,支持半开状态 |
策略组合顺序:**重试(外层) → 熔断 → 超时(内层)**
```csharp
services.AddMudHttpUtils("myApi", "https://api.example.com", options =>
{
options.Retry.MaxRetryAttempts = 3;
options.Retry.UseExponentialBackoff = true;
options.Timeout.TimeoutSeconds = 30;
options.CircuitBreaker.Enabled = true;
options.CircuitBreaker.FailureThreshold = 5;
options.CircuitBreaker.BreakDurationSeconds = 30;
});
```
**非幂等方法防护**:默认仅对幂等方法(GET/HEAD/OPTIONS/PUT/DELETE/TRACE)重试,POST/PATCH 等非幂等方法退化为超时+熔断(防重复提交)。超时与熔断对所有方法始终生效。如需对非幂等方法重试:
```csharp
// 全局开关
options.Retry.AllowNonIdempotentRetry = true;
// 或方法级标注
[Post("/orders")]
[Retry(AllowNonIdempotent = true)]
Task CreateOrderAsync([Body] CreateOrderRequest request);
```
也支持从 `appsettings.json` 绑定:
```json
{
"MudHttpResilience": {
"Retry": {
"Enabled": true,
"MaxRetryAttempts": 3,
"UseExponentialBackoff": true
},
"Timeout": { "Enabled": true, "TimeoutSeconds": 30 },
"CircuitBreaker": {
"Enabled": true,
"FailureThreshold": 5,
"BreakDurationSeconds": 30
}
}
}
```
#### 三种运行模式
| 模式 | 配置 | 构造函数依赖 | 适用场景 |
| ---------------------- | ------------------------------------ | -------------------------------------------------------- | -------------------------------- |
| **HttpClient(推荐)** | `HttpClient = "IEnhancedHttpClient"` | `IOptions`, `IEnhancedHttpClient` | 通用场景,配合 `AddMudHttpUtils` |
| **TokenManager** | `TokenManage = "IFeishuAppManager"` | `IOptions`, Token 管理器 | 飞书/钉钉等需要 Token 管理 |
| **默认** | 无 | `IOptions`, `IMudAppContext` | 遗留场景 |
> `HttpClient` 与 `TokenManage` 互斥,同时定义时 `HttpClient` 优先。
#### Token 认证
```csharp
// 接口级 Token(通用类型用 TokenTypes 常量,平台自定义类型用字符串字面量或自定义常量类)
[Token("TenantAccessToken")]
public interface IApi { }
// 参数级 Token
[Get("/users/{id}")]
Task GetUserAsync([Path] int id, [Token("UserAccessToken")] string? token = null);
// Token 注入模式:Header(默认)、Query、Path、ApiKey、HmacSignature、BasicAuth、Cookie
[Token("AppAccessToken", InjectionMode = TokenInjectionMode.Header, Name = "Authorization")]
// 使用 RequiresUserId 自动获取用户级令牌
[Token("UserAccessToken", RequiresUserId = true)]
public interface IUserApi { }
// 使用 TokenManagerKey 解耦业务概念和技术查找键
[Token(TokenType = "UserAccessToken", TokenManagerKey = "FeishuUser")]
public interface IFeishuUserApi { }
```
#### 加密支持
```csharp
// 使用默认 AES 加密注册
services.AddMudHttpClient("myApi", encryption =>
{
encryption.Key = Convert.FromBase64String("your-base64-key");
// 注意:从 v1.8.0 起 IV 自动随机生成,无需手动设置
}, client =>
{
client.BaseAddress = new Uri("https://api.example.com");
});
// 或注册自定义加密提供程序
services.AddSingleton();
// 请求体加密
[Post("/api/secure")]
Task PostSecureAsync(
[Body(EnableEncrypt = true, EncryptSerializeType = SerializeType.Json, EncryptPropertyName = "data")] Request request
);
// 响应解密
[Post("/api/secure-data", ResponseEnableDecrypt = true)]
Task GetSecureDataAsync([Body] Request request);
```
> 默认 AES 实现为认证加密(AES-GCM / AES-CBC+HMAC),密文带版本前缀,无需额外 MAC 配置。
#### SSRF 防护(.NET 6+)
URL 来自用户输入时,建议启用连接期 IP 准入校验,根治 DNS rebinding(URL 校验期与实际建连期解析结果可能不一致):
```csharp
// 1. 注册 IP 准入策略(默认实现拒绝私网/回环/链路本地地址,fail-closed)
services.AddMudHttpClientSsrfProtection();
// 2. 为命中的 HttpClient 启用连接期校验(建连时对实际连接的 IP 执行准入校验)
services.AddMudHttpClient("myApi", "https://api.example.com")
.AddMudHttpClientSsrfProtection();
// 自定义准入策略:注册自己的 IIpAddressPolicy 替换默认实现(如本地调试放行 localhost)
services.AddSingleton();
```
- 被策略拒绝的连接抛出 `InvalidOperationException`。
- 默认 `AllowCustomBaseUrls = false` 时强制 HTTPS + 白名单(fail-closed);`AllowCustomBaseUrls = true` 放行自定义 URL 时,必须自行校验 URL 来源。
- **信任边界声明**:白名单域名由配置方保证可信(含其 DNS 解析结果)。未启用连接期校验时,白名单域名一旦被 DNS rebinding 解析到内网 IP,将不受连接期防护。生产环境推荐:`AddMudHttpClientSsrfProtection(services)` 注册策略 + 在每个命名客户端的 `IHttpClientBuilder` 上 `AddMudHttpClientSsrfProtection(builder)` 启用连接建立时的 IP 准入校验(net6.0+)。
- DNS 解析结果带 TTL 缓存(默认 5 分钟),并发场景下同域名解析受锁保护(单飞)。
#### 遥测脱敏(默认开启)
Span tag、日志与诊断事件中的 URL 默认脱敏(掩码 `access_token` / `refresh_token` / `api_key` 等敏感 query 值),防止令牌随遥测泄漏:
```csharp
// 全局开关(静态属性,需在进程启动时设置)
MudHttpObservabilityOptions.RedactUrlInTelemetry = true; // 默认 true:URL 脱敏
MudHttpObservabilityOptions.RecordFullUrlOnSuccess = false; // 默认 false:成功请求仅记录 scheme://host/path(不含 query)
MudHttpObservabilityOptions.EmitDiagnosticEvents = true; // 默认 true:诊断事件(ActivityEvent / DiagnosticSource)
```
- 脱敏只掩码敏感 query 键的值,保留键名与 URL 结构,兼顾排障;未命中敏感词表的 query 原样保留。
- `RecordFullUrlOnSuccess = true` 时成功请求也记录完整 URL,但仍受 `RedactUrlInTelemetry` 约束;错误路径(`ApiException.RequestUri`)始终保留完整 URI,由 `IExceptionRedactor` 兜底擦除。
- 指标 tag 白名单:`MudHttpObservabilityOptions.MetricTagAllowlist` 控制所有指标维度(默认包含 `client_name`/`method`/`host`/`outcome`/`status_code`/`policy_key`/`token_manager_key`/`retry_count`),白名单之外的维度被丢弃,从机制上杜绝高基数 tag(如 `cache_key`)打爆时序后端。
#### 错误内容上限(默认 10240)
错误响应体(`ApiException.Content`)与捕获的请求体(`ApiException.RequestContent`)默认在**读取阶段**截断为 10240 字符,防止恶意/超大响应导致 OOM:
```csharp
o.MaxExceptionContentLength = 10240; // 默认 10240;设为 0 或负数 = 不限制
```
- 内置方法路径(`EnhancedHttpClient`)与生成代码路径(`DefaultHttpRequestExecutor`)默认值一致(10240),截断内容带 `...[已截断]` 后缀。
- `MaxSuccessResponseBytes`(默认 `0` = 不限制)可为成功响应体设置字节级守卫:已知长度(Content-Length)预判超限即抛 `ApiRequestException`,chunked 无长度场景由守卫流在读取阶段拦截。流式下载(`DownloadLargeAsync`/流式枚举)不受此限。
#### 令牌管理
```csharp
// 核心接口
ITokenManager // 通用令牌管理
IUserTokenManager // 用户令牌管理
ITokenProvider // Token 提供器(统一封装 Token 获取逻辑)
ICurrentUserContext // 当前用户上下文(线程安全的用户 ID 传播,替代 CurrentUserId 属性)
TokenRequest // Token 请求参数(TokenManagerKey, UserId, Scopes)
ITokenStore // 令牌持久化存储契约(持久化 SPI,与缓存契约互补,见下方分层说明)
IUserTokenStore // 用户级令牌持久化存储契约(持久化 SPI,支持按用户隔离)
TokenManagerBase // 令牌管理器抽象基类(并发安全刷新,支持 MetricsKey 覆写)
UserTokenManagerBase // 用户令牌管理器抽象基类(并发安全刷新)
TokenTypes // 令牌类型常量(TenantAccessToken、UserAccessToken 等)
MemoryTokenStore // 内存令牌存储默认实现(ITokenStore)
MemoryUserTokenStore // 内存用户令牌存储默认实现(IUserTokenStore)
MemoryEncryptedTokenStore // 内存加密令牌存储默认实现(IEncryptedTokenStore)
DefaultFormContent // 默认表单内容实现(IFormContent)
```
> **持久化 vs 缓存(分层定位)**:`ITokenStore` 家族是**持久化 SPI**(跨进程/跨实例,全异步),`ITokenCache` 是**进程内缓存契约**(全同步,管理器直接消费)—— 两者互补不互替。**仅注册 `ITokenStore` 不会自动让令牌获得持久化能力**:持久化须经 `TokenStoreBackedTokenCache`(内存镜像 + 异步写穿 + `HydrateAsync` 水合)桥接进入管理器管线;进程内缓存场景请实现 `ITokenCache` 并注入管理器。已注册但未接入管理器的存储会在启动期收到 EventId 191 提示。加密只在同一读写链路做一层(store 层加密装饰或缓存层 `EncryptedTokenCache` 二选一)。选型决策表见「令牌存储与缓存选型指南」。
##### 令牌存储与缓存选型指南
| 需求场景 | 选型 | 说明 |
|---|---|---|
| 纯进程内缓存(单实例、重启即失) | `ConcurrentDictionaryTokenCache`(默认,租户) / `MemoryCacheTokenCache`(需要绝对+滑动过期与驱逐回调,用户级默认) | 两者能力差异见下表;过期判定口径统一归管理器管线 |
| 跨进程 / 跨实例持久化(Redis / DB) | 自定义 `ITokenStore` / `IUserTokenStore`(全异步 SPI)+ `TokenStoreBackedTokenCache` 桥接 | 仅注册 store 无效(启动期 EventId 191 提示);桥接器负责镜像 / 写穿 / 水合 |
| store 已加密 | store 层实现 `IEncryptedTokenStore`(如下游自建加密装饰器) | 桥接器构造期检测并告警;**不得**再套 `EncryptedTokenCache`(密文套密文) |
| store 未加密但需要静态加密 | 缓存层套 `EncryptedTokenCache`(`ITokenCache` 为内层) | 仅适用于"无持久化"的纯内存链路 |
| 多实例强一致(2.2+) | 实现桥接器的 `IAsyncTokenCache` 真穿透路径 | 方案 B 的镜像在多实例下有滞后,异步契约是根治路径 |
两个进程内缓存的差异(防 C9 静默失效):
| 能力 | `ConcurrentDictionaryTokenCache` | `MemoryCacheTokenCache` |
|---|---|---|
| 绝对 / 滑动过期 | ❌ no-op(S1-5 起带一次性 Debug 诊断) | ✅ |
| 驱逐回调 | ❌ no-op | ✅ |
| Compact | ✅ LRU(按最后访问) | ✅(BCL Compact + 影子索引对账) |
| 过期判定 | 归管理器管线(`TokenExpiryPolicy`) | 缓存自身 + 管线双重 |
// 实现自定义令牌管理器
public class MyTokenManager : TokenManagerBase
{
protected override Task GetCachedTokenAsync(string tokenType, CancellationToken ct) { }
protected override Task RefreshTokenCoreAsync(string tokenType, CancellationToken ct) { }
}
// 使用 RequiresUserId 自动获取用户级令牌
[HttpClientApi(TokenManage = "IFeishuAppManager")]
[Token(TokenType = "UserAccessToken", RequiresUserId = true)]
public interface IFeishuUserApi { }
// 生成的构造函数自动注入 ICurrentUserContext,CurrentUserId 属性委托给 _currentUserContext.UserId
// 使用 TokenManagerKey 解耦业务概念和技术查找键
[Token(TokenType = "UserAccessToken", TokenManagerKey = "FeishuUser")]
public interface IFeishuContactApi { }
// 覆写 MetricsKey 使指标维度可区分(多实例场景下避免监控数据混叠)
public class MyNamedTokenManager : TokenManagerBase
{
public MyNamedTokenManager(string instanceName) => _instanceName = instanceName;
protected override string MetricsKey => _instanceName;
private readonly string _instanceName;
// 必须实现:GetCachedTokenAsync / RefreshTokenCoreAsync
}
```
#### 多命名客户端
```csharp
// 注册多个客户端
services.AddMudHttpClient("userApi", "https://user-api.example.com");
services.AddMudHttpClient("orderApi", "https://order-api.example.com");
// 通过 IHttpClientResolver 动态获取
public class MultiApiService
{
private readonly IHttpClientResolver _resolver;
public MultiApiService(IHttpClientResolver resolver) => _resolver = resolver;
public async Task CallUserApiAsync()
{
var client = _resolver.GetClient("userApi");
await client.GetAsync("/users/1");
}
}
```
#### 流式响应(.NET 6+)
```csharp
// IAsyncEnumerable 流式处理
await foreach (var message in _httpClient.SendAsAsyncEnumerable(request, cancellationToken: ct))
{
yield return message;
}
// 原始 HttpResponseMessage
var response = await _httpClient.SendRawAsync(request);
// 响应流
var stream = await _httpClient.SendStreamAsync(request);
```
> `SendStreamAsync` 返回的流**所有权归调用方**(由调用方负责 `Dispose`,释放即同时释放底层 `HttpResponseMessage`)。
> 接口方法也可直接声明 `Task` 返回 —— 生成器会发射 `SendStreamAsync` 直达调用;
> 该路径不参与 `[Cache]`/`[Retry]`/`[CircuitBreaker]`/`[Timeout]` 编排与 `Response` 包装
> (生成期以 `HTTPCLIENT025` 提示)。详见生成器包 README 的「直达返回」一节。
#### 文件上传与下载
```csharp
// 文件上传(支持 JsonPropertyName 属性名映射)
[Post("/upload")]
Task UploadAsync([FormContent] IFormContent formData);
// 文件下载
[Get("/files/{fileId}")]
Task DownloadFileAsync([Path] string fileId, [FilePath(BufferSize = 81920)] string savePath);
// 二进制数据下载
[Get("/files/{fileId}/content")]
Task DownloadFileContentAsync([Path] string fileId);
```
**成功响应体守卫(可选)**:限制反序列化路径的成功响应体大小,超限抛 `ApiRequestException`;`0`(默认)= 不限制:
```csharp
services.Configure(o =>
{
o.MaxSuccessResponseBytes = 10 * 1024 * 1024; // 10 MB
});
```
超大文件请改用流式落盘(`[FilePath]` 下载路径不受守卫约束)。
#### 接口级动态属性
支持在接口上定义 `[Query]` 或 `[Path]` 属性,作为所有方法的默认查询参数或路径参数。生成的实现类将包含对应的可读写属性:
```csharp
[HttpClientApi(HttpClient = "IEnhancedHttpClient")]
[BasePath("{tenantId}/api/v1")]
public interface ITenantApi
{
[Path("tenantId")]
string TenantId { get; set; }
[Query("apiKey")]
string ApiKey { get; set; }
[Get("users")]
Task> GetUsersAsync();
}
// 使用
var api = serviceProvider.GetRequiredService();
api.TenantId = "tenant-123";
api.ApiKey = "my-api-key";
await api.GetUsersAsync();
// 实际请求: /tenant-123/api/v1/users?apiKey=my-api-key
```
#### QueryMap 查询参数映射
`[QueryMap]` 支持将对象属性展开为查询参数,支持字典类型和 POCO 对象:
```csharp
public class SearchCriteria
{
public string? Keyword { get; set; }
public int Page { get; set; }
}
[Get("/api/search")]
Task SearchAsync(
[QueryMap(PropertySeparator = "_", SerializationMethod = QuerySerializationMethod.ToString)]
SearchCriteria criteria);
// 字典类型
[Get("/api/search")]
Task SearchAsync([QueryMap] IDictionary filters);
```
`QueryMapAttribute` 属性:
| 属性 | 类型 | 默认值 | 说明 |
| --------------------- | -------------------------- | ---------- | --------------------------------- |
| `PropertySeparator` | `string` | `"_"` | 嵌套属性名称分隔符 |
| `SerializationMethod` | `QuerySerializationMethod` | `ToString` | 序列化方法(`ToString` / `Json`) |
| `UrlEncode` | `bool` | `true` | 是否对查询参数值进行 URL 编码 |
| `IncludeNullValues` | `bool` | `false` | 是否包含值为 null 的属性 |
#### Base Path 支持
支持在接口级别定义统一的路径前缀:
```csharp
[HttpClientApi(HttpClient = "IEnhancedHttpClient")]
[BasePath("api/v1")]
public interface IUserApi
{
[Get("users/{id}")] // 实际路径: /api/v1/users/{id}
Task GetUserAsync([Path] int id);
[Get("/admin/users")] // 以 / 开头,忽略 BasePath,实际路径: /admin/users
Task> GetAllUsersAsync();
}
```
#### Response\ 包装类型
`Response` 类型同时返回响应内容和元数据(状态码、响应头):
```csharp
[Get("/users/{id}")]
Task> GetUserAsync([Path] int id);
// 使用
var response = await api.GetUserAsync(1);
var user = response.Data; // 响应内容
var status = response.StatusCode; // HTTP 状态码
var headers = response.Headers; // 响应头
```
> **注意**:不建议将 `Response` 与 `[Cache]` 特性组合使用,缓存会存储整个 `Response` 对象(包括 StatusCode 和 Headers),可能导致后续请求返回过期的状态码和响应头。生成器会对此组合发出 HTTPCLIENT011 编译警告。
#### 继承与事件处理器
```csharp
// 继承
[HttpClientApi("https://api.example.com", IsAbstract = true)]
public interface IBaseApi { }
[HttpClientApi("https://api.example.com", InheritedFrom = "BaseApiClass")]
public interface IUserApi : IBaseApi { }
// 事件处理器
[GenerateEventHandler(EventType = "UserCreatedEvent", HandlerClassName = "UserCreatedEventHandler")]
public class UserCreatedEvent { }
```
### 🏗️ 项目结构
```
MudHttpUtils/
├── Mud.HttpUtils/ # 元包:一站式引用 + DI 注册
│ └── ServiceCollectionExtensions # AddMudHttpUtils() 一站式注册
├── Mud.HttpUtils.Abstractions/ # 接口定义层(最小依赖)
│ ├── IBaseHttpClient # 基础 HTTP 操作接口
│ ├── IEnhancedHttpClient # 增强客户端组合接口
│ ├── IEncryptionProvider # 加密提供程序接口
│ ├── ITokenManager # 令牌管理接口
│ ├── ITokenProvider # Token 提供器接口(统一封装 Token 获取逻辑)
│ ├── ICurrentUserContext # 当前用户上下文接口(线程安全的用户 ID 传播)
│ ├── TokenRequest # Token 请求参数
│ ├── ITokenStore / IUserTokenStore # 令牌持久化存储契约
│ ├── IHttpClientResolver # 命名客户端解析接口
│ ├── TokenManagerBase # 令牌管理器抽象基类
│ ├── TokenTypes # 令牌类型常量
│ └── IMudAppContext # 应用上下文接口
├── Mud.HttpUtils.Attributes/ # 特性定义层
│ ├── HttpClientApiAttribute # API 接口标注
│ ├── Get/Post/Put/Delete/... # HTTP 方法特性
│ └── Path/Query/Body/Token/... # 参数特性
├── Mud.HttpUtils.Client/ # 客户端实现层
│ ├── EnhancedHttpClient # 增强 HTTP 客户端基类
│ ├── DirectEnhancedHttpClient # 直接构造的增强客户端
│ ├── HttpClientFactoryEnhancedClient # IHttpClientFactory 实现
│ ├── DefaultAesEncryptionProvider # AES 加密默认实现
│ ├── HttpClientResolver # 命名客户端解析器
│ ├── MemoryTokenStore # 内存令牌存储默认实现
│ ├── MemoryUserTokenStore # 内存用户令牌存储默认实现
│ ├── MemoryEncryptedTokenStore # 内存加密令牌存储默认实现
│ ├── DefaultFormContent # 默认表单内容实现
│ └── ServiceCollectionExtensions # AddMudHttpClient() 注册
├── Mud.HttpUtils.Resilience/ # 弹性策略扩展包
│ ├── ResilientHttpClient # 装饰器(重试/超时/熔断)
│ ├── PollyResiliencePolicyProvider # Polly 策略提供器
│ ├── HttpRequestMessageCloner # 请求克隆工具
│ └── ServiceCollectionExtensions # AddMudHttpResilienceDecorator() 注册
├── Mud.HttpUtils.Generator/ # 源代码生成器(含分析器与代码修复)
│ ├── HttpInvokeClassSourceGenerator # 实现类生成器
│ └── HttpInvokeRegistrationGenerator # 注册代码生成器(含 Timeout 配置)
├── Mud.HttpUtils.OpenTelemetry/ # OpenTelemetry 可观测性适配
│ ├── MudHttpOpenTelemetryExtensions # 一键开启 Tracing + Metrics
│ └── MudHttpOpenTelemetryOptions # 配置选项
├── Mud.HttpUtils.Newtonsoft.Json/ # Newtonsoft.Json 序列化器适配
├── Mud.HttpUtils.Xml/ # XML 序列化器适配
├── Mud.HttpUtils.CodeFixes/ # 代码修复提供器(打包进 Generator,不单独发布)
├── Mud.HttpUtils.JsonContextScaffolder/ # JsonSerializerContext 脚手架
├── Demos/ # 示例项目
└── Tests/ # 测试项目
```
### 📚 详细文档
| 包名 | 说明 | 文档 |
| ----------------------------------- | ---------------------------------- | ------------------------------------------------------------- |
| Mud.HttpUtils | 元包,一站式引用 + DI 注册 | [README](Mud.HttpUtils/README.md) |
| Mud.HttpUtils.Abstractions | 接口定义,最小依赖 | [README](Mud.HttpUtils.Abstractions/README.md) |
| Mud.HttpUtils.Attributes | 特性标注 | [README](Mud.HttpUtils.Attributes/README.md) |
| Mud.HttpUtils.Client | 客户端实现 | [README](Mud.HttpUtils.Client/README.md) |
| Mud.HttpUtils.Resilience | 弹性策略 | [README](Mud.HttpUtils.Resilience/README.md) |
| Mud.HttpUtils.Generator | 源代码生成器(含分析器与代码修复) | [README](Mud.HttpUtils.Generator/README.md) |
| Mud.HttpUtils.OpenTelemetry | OpenTelemetry 可观测性 | [README](Mud.HttpUtils.OpenTelemetry/README.md) |
| Mud.HttpUtils.Newtonsoft.Json | Newtonsoft.Json 序列化器适配 | [README](Mud.HttpUtils.Newtonsoft.Json/README.md) |
| Mud.HttpUtils.Xml | XML 序列化器适配 | [README](Mud.HttpUtils.Xml/README.md) |
| Mud.HttpUtils.JsonContextScaffolder | JsonContext 脚手架 | [README](Tools/Mud.HttpUtils.JsonContextScaffolder/README.md) |
| 变更记录 | 行为基线与版本说明 | [CHANGELOG](CHANGELOG.md) |
### ⚡ 性能说明
Mud.HttpUtils 通过 Roslyn 源代码生成器在编译时生成强类型的 HTTP 调用代码,核心路径(JSON 序列化/反序列化、URL 构建、请求头处理)完全避免了运行时反射。
**存在反射的场景**(仅限以下高级特性):
| 场景 | 反射调用 | 影响范围 |
| ----------------------------------------------------------- | ------------------------------------------------------ | ----------------------------- |
| `[Body(ContentType = "application/x-www-form-urlencoded")]` | 使用 `FormUrlEncodedContent` 时通过反射读取对象属性 | 仅限 FormUrlEncoded Body 模式 |
| `[QueryMap]` 复杂类型展开 | 通过反射读取对象属性展开为查询参数 | 仅限 QueryMap 非字典类型 |
| XML 序列化/反序列化 | `XmlSerializer` 内部使用反射(已通过静态字段缓存优化) | 仅限 XML Content-Type |
对于性能敏感的场景,建议优先使用 JSON 序列化(`System.Text.Json` 原生支持 AOT)和简单类型的查询参数。
### 🚀 Native AOT 与裁剪支持
Mud.HttpUtils 在设计之初即面向 **Native AOT** 与**裁剪(Trimming)**:核心路径(JSON 序列化/反序列化、URL 构建、请求头处理)完全避免运行时反射,由源代码生成器在编译期产出强类型实现。
**各包 AOT/裁剪兼容情况:**
| 包 | AOT/裁剪 | 说明 |
|----|:--------:|------|
| Mud.HttpUtils(元包) | ✅ | 聚合核心子包,AOT 兼容 |
| Mud.HttpUtils.Abstractions | ✅ | 纯接口定义,无反射 |
| Mud.HttpUtils.Attributes | ✅ | 仅特性定义,无反射 |
| Mud.HttpUtils.Client | ✅ | `System.Text.Json` 序列化,AOT 安全 |
| Mud.HttpUtils.Resilience | ✅ | 装饰器与策略编排均为静态类型与委托 |
| Mud.HttpUtils.Generator | ✅ | 生成 AOT/裁剪安全代码,内含 `AOT001`–`AOT007` 编译期诊断 |
| Mud.HttpUtils.OpenTelemetry | ✅ | 自有 API 无反射;导出能力依赖上游 OpenTelemetry SDK |
| Mud.HttpUtils.JsonContextScaffolder | ✅ | 生成 `JsonSerializerContext`,消除 JSON 反射 |
| Mud.HttpUtils.Newtonsoft.Json | ❌ | Newtonsoft.Json 依赖运行时反射,已标注 `[RequiresUnreferencedCode]` |
| Mud.HttpUtils.Xml | ❌ | `XmlSerializer` 运行期生成动态程序集,已标注 `[RequiresDynamicCode]` |
**启用 Native AOT:**
```xml
true
true
```
1. 保持使用 `System.Text.Json`(默认 AOT 安全);如需为特定 DTO 生成序列化上下文,运行脚手架工具 `mud-jsonctx`(`Mud.HttpUtils.JsonContextScaffolder`)为 `[HttpJsonSerializable]` 标注类型产出 `JsonSerializerContext`。
2. 源生成器会读取 `IsAotCompatible` / `PublishAot` 等构建属性,在编译期给出 `AOT001`–`AOT007` 诊断。
3. CI 严格模式可用 `-p:AotStrictMode=true` 将 `AOT004`–`AOT007` 及相关 IL 警告升级为 Error(详见「编译警告参考」)。
> ⚠️ Native AOT 项目中请勿使用 `Mud.HttpUtils.Newtonsoft.Json` 与 `Mud.HttpUtils.Xml`,二者依赖运行时反射/动态代码生成,与 AOT/裁剪不兼容。
### 🔔 编译警告参考
源代码生成器在编译时会对不合理的 API 定义产生警告或错误,帮助开发者在编译阶段发现问题。
| Diagnostic ID | 严重级别 | 触发条件 | 解决方案 |
| ------------------- | -------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `HTTPCLIENT001` | Error | 生成接口实现时发生异常 | 检查接口定义是否正确,查看内部异常信息 |
| `HTTPCLIENT003` | Error | 接口语法分析失败 | 确保接口定义符合 C# 语法规范 |
| `HTTPCLIENT004` | Error | 参数配置错误 | 检查参数特性配置是否正确 |
| `HTTPCLIENT005` | Error | URL 模板格式无效 | 检查 `[Get]`/`[Post]` 等特性中的 URL 模板 |
| `HTTPCLIENT007` | Error | 同时指定 `HttpClient` 和 `TokenManage` | 两者互斥,只设置其中一个 |
| `HTTPCLIENT008` | Error | 加密配置但 HttpClient 类型不支持加密 | 使用 `IEnhancedHttpClient` 或移除加密配置 |
| `HTTPCLIENT009` | Warning | XML 请求但 HttpClient 类型不支持 XML | 使用 `IEnhancedHttpClient` 或修改 Content-Type |
| `HTTPCLIENT011` | Warning | `[Cache]` 与 `Response` 返回类型组合 | 缓存会存储状态码和响应头,建议使用普通返回类型 |
| `HTTPCLIENT012` | Info | 泛型接口:生成器将转发类型参数与约束 | 无需处理,仅供感知(泛型接口**已支持**代码生成) |
| `HTTPCLIENT013` | Error | URL 模板中的路径占位符与 `[Path]` 参数不匹配 | 确保 URL 模板中的 `{placeholder}` 与方法中的 `[Path]` 参数一一对应 |
| `HTTPCLIENT014` | Warning | 指定的 `HttpClient` 类型在当前编译中未找到 | 确认类型名称正确,或确保已注册对应命名客户端 |
| `HTTPCLIENT015` | Error | `TokenManage` 类型未找到 | 确认类型名称正确,或确保包含该类型的项目已引用 |
| `HTTPCLIENT016` | Error | `TokenManage` 类型缺少必需方法 | 提供 `IMudAppContext GetDefaultApp()` / `GetApp(string)` 或实现 `IAppManager` |
| `HTTPCLIENT017` | Warning | `HttpClient` 类型无法解析,加密/XML 兼容性校验被跳过 | 使用完全限定名确保类型可解析 |
| `HTTPCLIENT018` | Warning | `TokenManagerKey` 使用默认推断值 | 多接口共享同一 TokenManager 时显式指定 `TokenManagerKey` 或 `TokenType` |
| ~~`HTTPCLIENT019`~~ | — | ❌ 已移除(CFG-27):其唯一触发点 `CacheAttribute.Priority` 已删除 | 无需处理(ID 保留为未使用占位) |
| `HTTPCLIENT020` | Warning | 非幂等方法声明 `[Retry]` 但未设 `AllowNonIdempotent` | 运行时将跳过重试;如服务端可安全重复执行请显式开启 |
| `HTTPCLIENT021` | Warning | 方法级 `[Timeout]` 超过接口级 `HttpClient` 超时 | `HttpClient.Timeout` 是硬上限,调小 `[Timeout]` 或提高 `[HttpClientApi(Timeout=…)]` |
| `HTTPCLIENT022` | Warning | 方法使用 `Path`/`HmacSignature` 令牌注入模式 | 令牌恢复处理器(`TokenRecoveryDelegatingHandler`/`TokenRecoveryEnhancedClient`)不支持这两种模式,刷新后的新令牌无法重新注入,恢复将静默失败并返回 401。如需令牌恢复能力请改用 `Header`/`Query`/`ApiKey`/`Cookie`/`BasicAuth` 模式 |
| `HTTPCLIENTREG001` | Error | 注册代码生成失败 | 检查接口定义和 DI 注册配置 |
| `HTTPCLIENTREG002` | Error | `RegistryGroupName` 不是有效 C# 标识符 | 使用字母、数字、下划线组成,以字母或下划线开头 |
| `EHSG001` | Error | 事件处理器代码生成失败 | 检查被处理类型定义与配置 |
| `FORM001` | Error | FormContent 代码生成错误 | 检查 FormContent 类定义 |
| `FORM002` | Error | FormContent 缺少 `[FilePath]` 属性 | 必须且只能有一个属性标记 `[FilePath]` |
| `FORM003` | Error | FormContent 存在多个 `[FilePath]` 属性 | 只保留一个 `[FilePath]` 属性 |
| `MUD004` | Warning | `ITokenManager` 实现未注册为 Singleton | `ITokenManager` 的实现类内部维护令牌缓存与并发锁(如 `SemaphoreSlim`),Scoped/Transient 注册会使每个请求持有独立缓存实例,导致并发安全机制失效与重复刷新令牌。请改用 `AddSingleton`/`TryAddSingleton` |
> **注**:`HTTPCLIENT002`、`HTTPCLIENT006`、`HTTPCLIENT010`、`HTTPCLIENT019` 当前**未使用**(ID 保留为占位,不重新分配)。
>
> - `HTTPCLIENT010`:`HttpClientApiAttribute.BaseAddress` **已移除**(CFG-27),使用直接编译错误 `CS0117`。
> - `HTTPCLIENT019`:`CacheAttribute.Priority` **已移除**(CFG-27),`[Cache]` 已无被忽略的属性。
### 🧪 测试
```bash
# 运行所有测试
dotnet test
# 运行特定测试项目
dotnet test Tests/Mud.HttpUtils.Tests
dotnet test Tests/Mud.HttpUtils.Client.Tests
dotnet test Tests/Mud.HttpUtils.Resilience.Tests
dotnet test Tests/Mud.HttpUtils.Generator.Tests
dotnet test Tests/Mud.HttpUtils.OpenTelemetry.Tests
```
### 🤖 默认参数推断
未标注任何 HTTP 参数特性的方法参数,代码生成器会根据参数类型自动推断处理方式:
- **简单类型**(`string`、`int`、`long`、`Guid`、`DateTime` 等及其数组和可空类型)→ 自动作为 `[Query]` 查询参数处理
- **复杂类型**(自定义对象、`List`、`Dictionary` 等)→ 自动作为 `[Body]` 请求体进行 JSON 序列化处理
- **特殊类型**(`CancellationToken`、`IProgress`)→ 不参与推断,保持原有处理
```csharp
[HttpClientApi(HttpClient = "IEnhancedHttpClient")]
public interface IUserApi
{
// string keyword 自动推断为 [Query("keyword")]
[Get("users/search")]
Task> SearchUsersAsync(string keyword, CancellationToken ct = default);
// User user 自动推断为 [Body]
[Post("users")]
Task CreateUserAsync(User user, CancellationToken ct = default);
// 混合使用:keyword → [Query],criteria → [Body]
[Post("users/advanced-search")]
Task> AdvancedSearchAsync(string keyword, SearchCriteria criteria, CancellationToken ct = default);
}
```
### 📊 OpenTelemetry 可观测性
通过 `Mud.HttpUtils.OpenTelemetry` 包一键开启分布式追踪与指标采集:
```csharp
builder.Services.AddMudHttpOpenTelemetry(options =>
{
options.OtlpEndpoint = new Uri("http://otel-collector:4317");
});
```
自动采集 HTTP 请求计数、请求耗时、缓存命中、令牌刷新、重试次数、熔断器状态、下载字节数、下载耗时等指标。
### ⚠️ 破坏性变更(升级前必读)
> 汇总自 **2.0.9** 起的行为变更。以下条目**不改变公共 API 签名**(少数例外已标注),但会改变运行期行为,升级前请逐条自查。
#### 1. 自动重定向已关闭(跨主机重定向改由手动逐跳复验)
主链路(`EnhancedHttpClient` 及其 primary handler)现统一 `AllowAutoRedirect = false`,重定向由 `EnhancedHttpClient.SendCoreAsync` 的手动循环承接:
- 每一跳都重新执行 `UrlValidator` 全套校验(白名单 / HTTPS / 私网 / 回环)。
- `301` / `302` / `303`:非 GET / HEAD 请求降级为 GET(丢弃请求体)。
- `307` / `308`:保留原方法与请求体,但**仅内存型可重放内容**(`ByteArrayContent` / `MultipartContent` 等)可继续;不可重放内容(一次性流)直接中止并抛出,避免发出空体请求。
- 逐跳剥离 `Authorization` / `Cookie` 等凭据头,跳数上限 10。
**替代路径**:若业务依赖自动重定向,请自行在 primary handler 上开启 `AllowAutoRedirect`(例如自定义 `SocketsHttpHandler`)。此时跨主机重定向**不再被复验**,白名单 / HTTPS / 私网防护对该跳失效,SSRF 风险由使用方自行承担。
#### 2. 注入的 `JsonSerializerOptions` 现在生效
`IHttpContentSerializer` 的 DI 路径此前会**静默丢弃**消费方注入的 `JsonSerializerOptions`(如 camelCase、枚举字符串化、自定义 Converter)。现改为以 `new JsonSerializerOptions(injected)` 副本为合并基座,再叠加框架 resolver 与内置默认值。
- 若你此前"注入了设置但依赖其不生效",请改为显式传 `null`。
- 合并顺序:`injected`(副本)→ 框架 resolver → 内置默认 → JIT 兜底默认。
#### 3. 401 恢复的重试轮次不再复用已被拒绝的令牌
`RefreshDedupTable.GetOrRefreshAsync` 新增 `forceRefresh` 参数;401 恢复执行器自第 2 轮起以 `forceRefresh = true` 透传,窗口内不再复用"已被服务端拒绝"的刷新结果。
#### 4. 请求头值校验更严格
`HttpHeaderValueValidator` 由"仅拒绝 CR/LF"改为**拒绝全部 C0 控制字符与 DEL**(保留 HTAB);Token / ApiKey 注入前同样复核。含控制字符的头值现会被跳过(生成器侧)或拒绝(运行期)。
#### 5. 敏感词表收窄与补充(URL / 日志脱敏覆盖面变化)
- **新增掩码**:`pwd` / `credential` / `sessionid` / `bearer` / `sign` / `auth` / `auth_code` / `authorization_code` / `verify_code` / `sms_code` / `captcha` / `otp` / `home_address` / `detail_address` / `billing_address` / `shipping_address`,以及姓名键 `user_name` / `userName` / `full_name` / `fullName`。
- **不再掩码**:通用键 `code` / `nonce` / `address` / `name`(过于宽泛,误伤业务字段与排障)。依赖旧行为掩码这些键的场景请改用具体变体键名。
- **Base64 启发式收紧**:字符串被判为"疑似令牌"需同时满足长度 ≥ 16 与 `=` 填充,普通单词(如 `testuser`)不再被整体掩码。
#### 6. AES 0x04 密文在未启用密钥分离的实例上显式拒绝
密文版本字节为 `0x04`(CBC + HMAC、密钥分离)而解密实例 `AesEncryptionOptions.EnableKeySeparation = false` 时,现**前置显式拒绝**并抛 `CryptographicException`(提示改配置或由加密侧产出 v3 信封),不再回落主密钥后误报"密文完整性校验失败"。
**兼容窗口**:跨版本对端(旧版加密 → 新版解密)需同步升级 `EnableKeySeparation` 配置,或由加密侧在窗口内产出 v3 信封。
#### 7. 其他行为变更(非破坏)
- `ProgressableStreamContent` 默认 `bufferSize` 由 `4096` 提升至 `81920`(与下载 / 执行器路径一致);显式传参不受影响。
- 严格模式(`AllowCustomBaseUrls = false`)下默认自动接线连接期 SSRF 校验(此前需显式 opt-in)。
- 连接期拒连异常消息只回显主机名,不再回显 DNS 解析出的候选 IP 列表(防内网拓扑泄露)。
- 加密令牌缓存的解密失败 Warning 日志中,缓存键(含 `userId`)改为脱敏输出。
### 🤝 贡献
欢迎提交 Issue 和 Pull Request 来改进这个项目!
### 📄 许可证
本项目遵循 MIT 许可证。详细信息请参见 [LICENSE](LICENSE) 文件。
---