# 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
[![Mud.HttpUtils](https://img.shields.io/nuget/v/Mud.HttpUtils?label=Mud.HttpUtils "Mud.HttpUtils")](https://www.nuget.org/packages/Mud.HttpUtils/ "Mud.HttpUtils") [![downloads](https://img.shields.io/nuget/dt/Mud.HttpUtils "downloads")](https://www.nuget.org/packages/Mud.HttpUtils/ "downloads") [![Mud.HttpUtils.Abstractions](https://img.shields.io/nuget/v/Mud.HttpUtils.Abstractions?label=Mud.HttpUtils.Abstractions "Mud.HttpUtils.Abstractions")](https://www.nuget.org/packages/Mud.HttpUtils.Abstractions/ "Mud.HttpUtils.Abstractions") [![downloads](https://img.shields.io/nuget/dt/Mud.HttpUtils.Abstractions "downloads")](https://www.nuget.org/packages/Mud.HttpUtils.Abstractions/ "downloads") [![Mud.HttpUtils.Attributes](https://img.shields.io/nuget/v/Mud.HttpUtils.Attributes?label=Mud.HttpUtils.Attributes "Mud.HttpUtils.Attributes")](https://www.nuget.org/packages/Mud.HttpUtils.Attributes/ "Mud.HttpUtils.Attributes") [![downloads](https://img.shields.io/nuget/dt/Mud.HttpUtils.Attributes "downloads")](https://www.nuget.org/packages/Mud.HttpUtils.Attributes/ "downloads") [![Mud.HttpUtils.Client](https://img.shields.io/nuget/v/Mud.HttpUtils.Client?label=Mud.HttpUtils.Client "Mud.HttpUtils.Client")](https://www.nuget.org/packages/Mud.HttpUtils.Client/ "Mud.HttpUtils.Client") [![downloads](https://img.shields.io/nuget/dt/Mud.HttpUtils.Client "downloads")](https://www.nuget.org/packages/Mud.HttpUtils.Client/ "downloads") [![Mud.HttpUtils.Resilience](https://img.shields.io/nuget/v/Mud.HttpUtils.Resilience?label=Mud.HttpUtils.Resilience "Mud.HttpUtils.Resilience")](https://www.nuget.org/packages/Mud.HttpUtils.Resilience/ "Mud.HttpUtils.Resilience") [![downloads](https://img.shields.io/nuget/dt/Mud.HttpUtils.Resilience "downloads")](https://www.nuget.org/packages/Mud.HttpUtils.Resilience/ "downloads") [![Mud.HttpUtils.Generator](https://img.shields.io/nuget/v/Mud.HttpUtils.Generator?label=Mud.HttpUtils.Generator "Mud.HttpUtils.Generator")](https://www.nuget.org/packages/Mud.HttpUtils.Generator/ "Mud.HttpUtils.Generator") [![downloads](https://img.shields.io/nuget/dt/Mud.HttpUtils.Generator "downloads")](https://www.nuget.org/packages/Mud.HttpUtils.Generator/ "downloads") [![Mud.HttpUtils.OpenTelemetry](https://img.shields.io/nuget/v/Mud.HttpUtils.OpenTelemetry?label=Mud.HttpUtils.OpenTelemetry "Mud.HttpUtils.OpenTelemetry")](https://www.nuget.org/packages/Mud.HttpUtils.OpenTelemetry/ "Mud.HttpUtils.OpenTelemetry") [![downloads](https://img.shields.io/nuget/dt/Mud.HttpUtils.OpenTelemetry "downloads")](https://www.nuget.org/packages/Mud.HttpUtils.OpenTelemetry/ "downloads") [![Mud.HttpUtils.Newtonsoft.Json](https://img.shields.io/nuget/v/Mud.HttpUtils.Newtonsoft.Json?label=Mud.HttpUtils.Newtonsoft.Json "Mud.HttpUtils.Newtonsoft.Json")](https://www.nuget.org/packages/Mud.HttpUtils.Newtonsoft.Json/ "Mud.HttpUtils.Newtonsoft.Json") [![downloads](https://img.shields.io/nuget/dt/Mud.HttpUtils.Newtonsoft.Json "downloads")](https://www.nuget.org/packages/Mud.HttpUtils.Newtonsoft.Json/ "downloads") [![Mud.HttpUtils.Xml](https://img.shields.io/nuget/v/Mud.HttpUtils.Xml?label=Mud.HttpUtils.Xml "Mud.HttpUtils.Xml")](https://www.nuget.org/packages/Mud.HttpUtils.Xml/ "Mud.HttpUtils.Xml") [![downloads](https://img.shields.io/nuget/dt/Mud.HttpUtils.Xml "downloads")](https://www.nuget.org/packages/Mud.HttpUtils.Xml/ "downloads") [![Mud.HttpUtils.JsonContextScaffolder](https://img.shields.io/nuget/v/Mud.HttpUtils.JsonContextScaffolder?label=Mud.HttpUtils.JsonContextScaffolder "Mud.HttpUtils.JsonContextScaffolder")](https://www.nuget.org/packages/Mud.HttpUtils.JsonContextScaffolder/ "Mud.HttpUtils.JsonContextScaffolder") [![downloads](https://img.shields.io/nuget/dt/Mud.HttpUtils.JsonContextScaffolder "downloads")](https://www.nuget.org/packages/Mud.HttpUtils.JsonContextScaffolder/ "downloads") [![License](https://img.shields.io/badge/license-MIT-blue.svg)](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 | [![Nuget](https://img.shields.io/nuget/v/Mud.HttpUtils.svg)](https://www.nuget.org/packages/Mud.HttpUtils/) | ![Nuget](https://img.shields.io/nuget/dt/Mud.HttpUtils.svg) | | **Mud.HttpUtils.Abstractions** | 纯接口定义,最小依赖 | [![Nuget](https://img.shields.io/nuget/v/Mud.HttpUtils.Abstractions.svg)](https://www.nuget.org/packages/Mud.HttpUtils.Abstractions/) | ![Nuget](https://img.shields.io/nuget/dt/Mud.HttpUtils.Abstractions.svg) | | **Mud.HttpUtils.Attributes** | 特性定义 | [![Nuget](https://img.shields.io/nuget/v/Mud.HttpUtils.Attributes.svg)](https://www.nuget.org/packages/Mud.HttpUtils.Attributes/) | ![Nuget](https://img.shields.io/nuget/dt/Mud.HttpUtils.Attributes.svg) | | **Mud.HttpUtils.Client** | 客户端实现 + DI 注册 | [![Nuget](https://img.shields.io/nuget/v/Mud.HttpUtils.Client.svg)](https://www.nuget.org/packages/Mud.HttpUtils.Client/) | ![Nuget](https://img.shields.io/nuget/dt/Mud.HttpUtils.Client.svg) | | **Mud.HttpUtils.Resilience** | 弹性策略(Polly) | [![Nuget](https://img.shields.io/nuget/v/Mud.HttpUtils.Resilience.svg)](https://www.nuget.org/packages/Mud.HttpUtils.Resilience/) | ![Nuget](https://img.shields.io/nuget/dt/Mud.HttpUtils.Resilience.svg) | | **Mud.HttpUtils.Generator** | 源代码生成器(已内置接口规范分析器与代码修复) | [![Nuget](https://img.shields.io/nuget/v/Mud.HttpUtils.Generator.svg)](https://www.nuget.org/packages/Mud.HttpUtils.Generator/) | ![Nuget](https://img.shields.io/nuget/dt/Mud.HttpUtils.Generator.svg) | | **Mud.HttpUtils.OpenTelemetry** | OpenTelemetry 可观测性适配 | [![Nuget](https://img.shields.io/nuget/v/Mud.HttpUtils.OpenTelemetry.svg)](https://www.nuget.org/packages/Mud.HttpUtils.OpenTelemetry/) | ![Nuget](https://img.shields.io/nuget/dt/Mud.HttpUtils.OpenTelemetry.svg) | | **Mud.HttpUtils.Newtonsoft.Json** | Newtonsoft.Json 序列化器适配 | [![Nuget](https://img.shields.io/nuget/v/Mud.HttpUtils.Newtonsoft.Json.svg)](https://www.nuget.org/packages/Mud.HttpUtils.Newtonsoft.Json/) | ![Nuget](https://img.shields.io/nuget/dt/Mud.HttpUtils.Newtonsoft.Json.svg) | | **Mud.HttpUtils.Xml** | XML 序列化器适配 | [![Nuget](https://img.shields.io/nuget/v/Mud.HttpUtils.Xml.svg)](https://www.nuget.org/packages/Mud.HttpUtils.Xml/) | ![Nuget](https://img.shields.io/nuget/dt/Mud.HttpUtils.Xml.svg) | | **Mud.HttpUtils.JsonContextScaffolder** | JsonSerializerContext 脚手架工具 | [![Nuget](https://img.shields.io/nuget/v/Mud.HttpUtils.JsonContextScaffolder.svg)](https://www.nuget.org/packages/Mud.HttpUtils.JsonContextScaffolder/) | ![Nuget](https://img.shields.io/nuget/dt/Mud.HttpUtils.JsonContextScaffolder.svg) | > 说明:上表仅列出已发布到 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) 文件。 ---