Skip to content

Commit cf5bea2

Browse files
author
shiliu
committed
Merge branch 'master' of https://gitee.com/mudtools/MudHttpUtils
2 parents 1fdaa0d + 2a64622 commit cf5bea2

594 files changed

Lines changed: 77427 additions & 3453 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<!-- 请保持模板结构;不适用的检查项请显式写「不适用」并说明原因。 -->
2+
3+
## 变更说明
4+
5+
<!-- 做了什么、为什么这样做;若修复缺陷,请写出触发场景与根因。 -->
6+
7+
## 验证方式
8+
9+
<!-- 跑了哪些构建/测试/发布;无法本地验证的部分请写明由哪个 CI 作业兜底。 -->
10+
11+
- [ ] `dotnet build Mud.HttpUtils.slnx -c Release` 无新增警告/错误
12+
- [ ] 相关测试工程通过(`Tests/Mud.HttpUtils.Generator.Tests``Tests/Mud.HttpUtils.Client.Tests` 等)
13+
- [ ] 涉及 AOT 的改动已在本地跑过 `dotnet publish ... -p:AotStrictMode=true`(或说明由 CI 的 aot-publish / aot-packageref 作业兜底)
14+
15+
## 检查项
16+
17+
- [ ] **AOT 判定语义同步**:若改动 `Mud.HttpUtils.Generator``AotModeResolver` 三态判定语义,必须同步 `Mud.HttpUtils.Attributes/build/Mud.HttpUtils.JsonContextScaffolder.targets``MudEnableJsonContextScaffolder` 判定,以及 `.github/workflows/ci.yml` 中 aot-publish / aot-packageref 两个作业的探针用例(「Verify scaffolder activation aligns with AotModeResolver」)
18+
- [ ] **诊断契约**:改动诊断 ID / 级别 / 消息格式 / 定位位置时,已同步 `AnalyzerReleases.Unshipped.md`、README 与相关测试
19+
- [ ] **AOT 信号源穷举**:若为 XML/JSON 判定新增了信号来源,已同步 `AotXmlRejectionAnalyzer.MayUseXml`(AOT007 预门控)与 `AotDtoCoverageAnalyzer`(AOT004/AOT005 豁免口径),否则会静默漏报
20+
- [ ] **文档同步**:CHANGELOG 与相关 README 已更新(行为变更 / API 变更 / 新增开关)

.github/workflows/ci.yml

Lines changed: 627 additions & 0 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 450 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
5+
<OutputType>Exe</OutputType>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<IsAotCompatible>true</IsAotCompatible>
9+
<PublishAot>true</PublishAot>
10+
<!-- [v4 Phase 2] 启用 TrimMode=full 验证完整裁剪模式。
11+
AotVerificationDemo 使用默认的 TrimMode=partial(仅裁剪明确未使用的程序集),
12+
本项目启用 full 模式(裁剪所有程序集中的未使用成员),验证库代码是否有遗漏的反射依赖。 -->
13+
<TrimMode>full</TrimMode>
14+
<!-- 关闭单警告模式,使所有裁剪告警都可见 -->
15+
<TrimmerSingleWarn>false</TrimmerSingleWarn>
16+
<InvariantGlobalization>true</InvariantGlobalization>
17+
<GenerateDocumentationFile>false</GenerateDocumentationFile>
18+
</PropertyGroup>
19+
20+
<!-- AOT 严格模式清单定义在 Directory.Build.targets(全仓统一口径),此处不再重复定义。 -->
21+
22+
<ItemGroup>
23+
<ProjectReference Include="..\..\Mud.HttpUtils.Client\Mud.HttpUtils.Client.csproj" />
24+
<ProjectReference Include="..\..\Mud.HttpUtils.Attributes\Mud.HttpUtils.Attributes.csproj" />
25+
<ProjectReference Include="..\..\Mud.HttpUtils.Generator\Mud.HttpUtils.Generator.csproj" OutputItemType="Analyzer" />
26+
<ProjectReference Include="..\..\Mud.HttpUtils.Resilience\Mud.HttpUtils.Resilience.csproj" />
27+
</ItemGroup>
28+
29+
<ItemGroup>
30+
<CompilerVisibleProperty Include="IsAotCompatible" />
31+
<CompilerVisibleProperty Include="PublishAot" />
32+
<!-- 与包路径(Mud.HttpUtils.Generator.props)保持一致:Nullable → 生成代码 #nullable enable;
33+
HttpClientOptionsName → 生成的 Options 类名。缺失时读取恒取默认值,配置静默失效。 -->
34+
<CompilerVisibleProperty Include="Nullable" />
35+
<CompilerVisibleProperty Include="HttpClientOptionsName" />
36+
</ItemGroup>
37+
38+
<ItemGroup>
39+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
40+
</ItemGroup>
41+
42+
</Project>
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
// -----------------------------------------------------------------------
2+
// 作者:Mud Studio 版权所有 (c) Mud Studio 2026
3+
// Mud.HttpUtils 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
4+
// 本项目主要遵循 MIT 许可证进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 文件。
5+
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目开发而产生的一切法律纠纷和责任,我们不承担任何责任。
6+
// -----------------------------------------------------------------------
7+
8+
using System.Text.Json;
9+
using System.Text.Json.Serialization;
10+
using Mud.HttpUtils;
11+
using Mud.HttpUtils.Attributes;
12+
13+
namespace AotFullTrimVerificationDemo;
14+
15+
/// <summary>
16+
/// TrimMode=full 验证 Demo。
17+
/// 验证 Mud.HttpUtils 库在完整裁剪模式(TrimMode=full)下无遗漏的反射依赖。
18+
/// </summary>
19+
/// <remarks>
20+
/// 与 AotVerificationDemo 的区别:
21+
/// - AotVerificationDemo 使用默认 TrimMode=partial + TrimmerSingleWarn=true
22+
/// - 本项目使用 TrimMode=full + TrimmerSingleWarn=false,暴露所有裁剪告警
23+
///
24+
/// 退出码契约:任一场景失败 → 输出 <c>AOT_FAIL</c> 并返回 1;全部通过 → 输出 <c>AOT_OK</c> 并返回 0。
25+
/// 该契约使 <c>test.ps1 -AOT -FullTrim</c> 能可靠判定成功/失败(此前仅打印一行提示、无退出码)。
26+
/// </remarks>
27+
internal static class Program
28+
{
29+
private static int s_failed;
30+
31+
private static async Task<int> Main(string[] args)
32+
{
33+
Console.WriteLine("=== Mud.HttpUtils TrimMode=full AOT 验证 ===");
34+
Console.WriteLine();
35+
36+
await RunScenarioAsync("JsonSerialization", VerifyJsonSerializationAsync);
37+
await RunScenarioAsync("AotSafeMasker", () =>
38+
{
39+
VerifyAotSafeMasker();
40+
return Task.CompletedTask;
41+
});
42+
await RunScenarioAsync("EncryptContent", () =>
43+
{
44+
VerifyEncryptContent();
45+
return Task.CompletedTask;
46+
});
47+
await RunScenarioAsync("QueryParameters", () =>
48+
{
49+
VerifyQueryParameters();
50+
return Task.CompletedTask;
51+
});
52+
await RunScenarioAsync("GeneratedContext", VerifyGeneratedContextAsync);
53+
54+
Console.WriteLine();
55+
56+
if (s_failed > 0)
57+
{
58+
Console.WriteLine($"AOT_FAIL (failed={s_failed})");
59+
return 1;
60+
}
61+
62+
Console.WriteLine("=== 所有验证场景通过 ===");
63+
Console.WriteLine("AOT_OK");
64+
return 0;
65+
}
66+
67+
/// <summary>
68+
/// 执行一个验证场景。
69+
/// </summary>
70+
/// <param name="name">场景标识(ASCII,供 CI grep 断言场景确实执行)。</param>
71+
/// <param name="scenario">场景委托。</param>
72+
/// <remarks>
73+
/// [修复] 原实现仅在失败分支打印场景名,CI 却把"输出中存在场景名"当作成功断言——
74+
/// 断言方向与实现相反(成功即不出现 → 门禁必然红)。现在成功/失败都打印 <c>[SCENE]</c> 标记行,
75+
/// 标识使用 ASCII 以避免跨平台 shell 的中文编码差异。
76+
/// </remarks>
77+
private static async Task RunScenarioAsync(string name, Func<Task> scenario)
78+
{
79+
Console.WriteLine($"[SCENE] {name}");
80+
try
81+
{
82+
await scenario().ConfigureAwait(false);
83+
}
84+
catch (Exception ex)
85+
{
86+
s_failed++;
87+
Console.WriteLine($" [FAIL] {name}: {ex.GetType().Name}: {ex.Message}");
88+
}
89+
}
90+
91+
private static void Assert(bool condition, string scenario)
92+
{
93+
if (condition) return;
94+
s_failed++;
95+
Console.WriteLine($" [FAIL] {scenario}");
96+
}
97+
98+
private static async Task VerifyJsonSerializationAsync()
99+
{
100+
Console.WriteLine("[场景 1] JSON 序列化/反序列化(JsonTypeInfo)...");
101+
102+
var dto = new TestDto { Id = 42, Name = "AOT Full Trim Test", Timestamp = DateTimeOffset.UtcNow };
103+
var json = JsonSerializer.Serialize(dto, FullTrimJsonContext.Default.TestDto);
104+
var deserialized = JsonSerializer.Deserialize(json, FullTrimJsonContext.Default.TestDto);
105+
106+
Assert(deserialized?.Id == 42 && deserialized.Name == "AOT Full Trim Test",
107+
"JSON 序列化/反序列化结果不一致");
108+
109+
Console.WriteLine(" ✓ JsonTypeInfo 序列化/反序列化成功");
110+
await Task.CompletedTask;
111+
}
112+
113+
private static void VerifyAotSafeMasker()
114+
{
115+
Console.WriteLine("[场景 2] AOT 安全脱敏器...");
116+
117+
var masker = new AotSafeSensitiveDataMasker();
118+
masker.Register<TestDto>(dto => $"***{dto.Id}***");
119+
120+
var masked = masker.Mask("sensitive-token-value", SensitiveDataMaskMode.Mask, 2, 2);
121+
Assert(masked.Contains('*'), "字符串脱敏结果异常");
122+
123+
var objMasked = masker.MaskObject(new TestDto { Id = 99, Name = "test" });
124+
Assert(objMasked.Contains("99"), "对象脱敏结果异常");
125+
126+
Console.WriteLine(" ✓ AOT 安全脱敏器正常工作");
127+
}
128+
129+
private static void VerifyEncryptContent()
130+
{
131+
Console.WriteLine("[场景 3] EncryptContent<T> 泛型重载...");
132+
133+
// [T10 修复] 注入测试用 IEncryptionProvider,做真实加密 round-trip 断言。
134+
// 使用固定密钥的异或加密(Demo 专用,非生产级),验证 EncryptContent<T> 泛型重载在 TrimMode=full 下可正确执行。
135+
var encryptionProvider = new XorEncryptionProvider();
136+
var client = new EncryptTestClient(encryptionProvider);
137+
138+
var dto = new TestDto { Id = 77, Name = "encrypt-test", Timestamp = DateTimeOffset.UtcNow };
139+
var encrypted = client.EncryptContent(dto);
140+
Assert(!string.IsNullOrEmpty(encrypted), "EncryptContent<T> 返回空字符串");
141+
142+
// 解密并验证 round-trip。
143+
// [AOT 体系修复] 断言需与源生成 Context 对齐:JsonSerializerOptions 命名策略为 CamelCase,
144+
// 故解密后应是 "id":77(原断言只认 "Id",在正确配置下必然失败)。
145+
var decrypted = client.DecryptContent(encrypted);
146+
Assert(decrypted.Contains("\"id\":77") || decrypted.Contains("\"Id\":77"),
147+
$"EncryptContent<T> round-trip 解密后内容不一致:{decrypted}");
148+
149+
Console.WriteLine(" ✓ EncryptContent<T> 泛型重载加密 round-trip 成功");
150+
}
151+
152+
private static void VerifyQueryParameters()
153+
{
154+
Console.WriteLine("[场景 4] 查询参数格式化...");
155+
156+
// 验证基本的 URL 查询参数构建(不使用反射式 DefaultUrlParameterFormatter)
157+
var builder = new QueryParameterBuilder();
158+
builder.Add("page", "1");
159+
builder.Add("size", "20");
160+
builder.Add("filter", "active");
161+
162+
var queryString = builder.ToString();
163+
Assert(queryString.Contains("page=1") && queryString.Contains("size=20"),
164+
"查询参数构建结果异常");
165+
166+
Console.WriteLine(" ✓ 查询参数构建正常");
167+
}
168+
169+
/// <summary>
170+
/// 场景 5:生成器路径。
171+
/// <c>[HttpClientApi]</c> 接口在编译期由源生成器生成实现类(并触发 AotStrictMode 下的生成代码检查);
172+
/// <c>[HttpJsonSerializable]</c> DTO 由源生成 Context 覆盖,运行时经 <c>JsonTypeInfo</c> round-trip 验证。
173+
/// 覆盖了此前完全缺失的“生成实现类 + 源生成 Context”在 TrimMode=full 下的行为。
174+
/// </summary>
175+
private static async Task VerifyGeneratedContextAsync()
176+
{
177+
Console.WriteLine("[场景 5] 生成器路径(源生成 Context + [HttpClientApi] 实现类)...");
178+
179+
// 生成器已为 IFullTrimApi 生成实现类(编译期产物);此处通过源生成 Context 做 AOT 安全 round-trip。
180+
var dto = new FullTrimDto { Id = 7, Name = "generated" };
181+
var json = JsonSerializer.Serialize(dto, FullTrimJsonContext.Default.FullTrimDto);
182+
var roundTrip = JsonSerializer.Deserialize(json, FullTrimJsonContext.Default.FullTrimDto);
183+
184+
Assert(roundTrip?.Id == 7 && roundTrip.Name == "generated",
185+
"源生成 Context round-trip 结果不一致");
186+
187+
// [AOT 体系修复] 以编译期类型引用生成实现类,避免其在 full trim 下被判定为不可达而裁剪。
188+
// 不能改用 RestService.ForGenerated<IFullTrimApi>():工厂注册代码只为**默认模式**接口
189+
// ([HttpClientApi] 且未指定 HttpClient/TokenManager 包装类型)生成
190+
// (HttpInvokeRegistrationGenerator.GenerateFactoryRegistrationCall 仅遍历 defaultModeApis),
191+
// 而 IFullTrimApi 使用 IEnhancedHttpClient 模式 → ForGenerated 必然抛
192+
// "No generated factory registered",使本场景恒失败。
193+
// 生成实现类命名约定:{接口所在命名空间}.Internal.{去掉 I 前缀的接口名}。
194+
var generatedImplementation = typeof(AotFullTrimVerificationDemo.Internal.FullTrimApi);
195+
Assert(generatedImplementation.Name == "FullTrimApi",
196+
$"生成实现类缺失或命名不符:{generatedImplementation.FullName}");
197+
198+
Console.WriteLine(" ✓ 源生成 Context round-trip 与生成实现类解析均正常");
199+
await Task.CompletedTask;
200+
}
201+
}
202+
203+
/// <summary>
204+
/// 测试用 DTO。
205+
/// </summary>
206+
public class TestDto
207+
{
208+
public int Id { get; set; }
209+
public string Name { get; set; } = string.Empty;
210+
public DateTimeOffset Timestamp { get; set; }
211+
}
212+
213+
/// <summary>
214+
/// 生成器路径验证用 DTO(由源生成 Context 覆盖)。
215+
/// </summary>
216+
[HttpJsonSerializable]
217+
public class FullTrimDto
218+
{
219+
public int Id { get; set; }
220+
public string Name { get; set; } = string.Empty;
221+
}
222+
223+
/// <summary>
224+
/// 生成器路径验证用 API 接口(由 [HttpClientApi] 源生成实现类 + ModuleInitializer 工厂注册)。
225+
/// </summary>
226+
[HttpClientApi(HttpClient = "IEnhancedHttpClient")]
227+
public interface IFullTrimApi
228+
{
229+
[Get("/api/items/{id}")]
230+
Task<FullTrimDto?> GetAsync([Path] int id);
231+
}
232+
233+
/// <summary>
234+
/// JSON 序列化上下文(AOT 源生成)。
235+
/// </summary>
236+
[JsonSerializable(typeof(TestDto))]
237+
[JsonSerializable(typeof(FullTrimDto))]
238+
[JsonSourceGenerationOptions(WriteIndented = false, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
239+
internal partial class FullTrimJsonContext : JsonSerializerContext;
240+
241+
/// <summary>
242+
/// [T10] Demo 专用异或加密提供器(非生产级,仅用于验证 EncryptContent&lt;T&gt; 代码路径在 TrimMode=full 下可正确执行)。
243+
/// </summary>
244+
internal sealed class XorEncryptionProvider : IEncryptionProvider
245+
{
246+
private static readonly byte[] Key = "MudAotDemoKey!"u8.ToArray();
247+
248+
public string Encrypt(string plainText)
249+
{
250+
var bytes = System.Text.Encoding.UTF8.GetBytes(plainText);
251+
return Convert.ToBase64String(EncryptBytes(bytes));
252+
}
253+
254+
public string Decrypt(string cipherText)
255+
{
256+
var bytes = Convert.FromBase64String(cipherText);
257+
return System.Text.Encoding.UTF8.GetString(DecryptBytes(bytes));
258+
}
259+
260+
public byte[] EncryptBytes(byte[] data)
261+
{
262+
var result = new byte[data.Length];
263+
for (int i = 0; i < data.Length; i++)
264+
result[i] = (byte)(data[i] ^ Key[i % Key.Length]);
265+
return result;
266+
}
267+
268+
public byte[] DecryptBytes(byte[] encryptedData)
269+
{
270+
// XOR 对称性:解密与加密相同
271+
return EncryptBytes(encryptedData);
272+
}
273+
}
274+
275+
/// <summary>
276+
/// [T10] 测试用 EnhancedHttpClient 子类,覆盖 EncryptionProvider 以注入 <see cref="XorEncryptionProvider"/>。
277+
/// </summary>
278+
/// <remarks>
279+
/// [AOT 体系修复] 必须显式注入源生成 <c>JsonTypeInfoResolver</c>:否则序列化器退回
280+
/// 库内置 <c>MudHttpJsonContext</c>(不含本 Demo 的 <c>TestDto</c>)→ EncryptContent 抛
281+
/// <c>NotSupportedException</c>,场景 3 恒失败(此前 T10 的"去虚化"断言恰好暴露了这一点)。
282+
/// </remarks>
283+
internal sealed class EncryptTestClient : EnhancedHttpClient
284+
{
285+
private readonly IEncryptionProvider _encryptionProvider;
286+
287+
public EncryptTestClient(IEncryptionProvider encryptionProvider)
288+
: base(new HttpClient(), new EnhancedHttpClientOptions
289+
{
290+
JsonTypeInfoResolver = FullTrimJsonContext.Default,
291+
})
292+
{
293+
_encryptionProvider = encryptionProvider;
294+
}
295+
296+
/// <inheritdoc/>
297+
protected override IEncryptionProvider? EncryptionProvider => _encryptionProvider;
298+
}

0 commit comments

Comments
 (0)