@elf-express/admin-net-mcp 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/index.js +83 -88
  2. package/knowledge/Furion_Teaching_Manual/04-1-/351/205/215/347/275/256.md +442 -0
  3. package/knowledge/Furion_Teaching_Manual/04-2-/351/201/270/351/240/205.md +363 -0
  4. package/knowledge/Furion_Teaching_Manual/05-1-/345/213/225/346/205/213WebAPI.md +825 -0
  5. package/knowledge/Furion_Teaching_Manual/05-2-HttpContext.md +217 -0
  6. package/knowledge/Furion_Teaching_Manual/05-3-/347/257/251/351/201/270/345/231/250/346/224/224/346/210/252/345/231/250AOP.md +581 -0
  7. package/knowledge/Furion_Teaching_Manual/05-4-/350/253/213/346/261/202/347/250/275/346/240/270/346/227/245/350/252/214.md +129 -0
  8. package/knowledge/Furion_Teaching_Manual/05-5-/344/270/255/344/273/213/350/273/237/351/253/224Middleware.md +328 -0
  9. package/knowledge/Furion_Teaching_Manual/05-6-Vue-React-Angular/344/273/213/351/235/242/344/273/243/347/220/206.md +317 -0
  10. package/knowledge/Furion_Teaching_Manual/06-1/350/246/217/347/257/204/345/214/226/346/216/245/345/217/243.md +1458 -0
  11. package/knowledge/Furion_Teaching_Manual/06-2/347/254/254/344/270/211/346/226/271API_Scalar.md +91 -0
  12. package/knowledge/Furion_Teaching_Manual/07-/345/217/213/345/245/275/344/276/213/345/244/226/350/231/225/347/220/206.md +511 -0
  13. package/knowledge/Furion_Teaching_Manual/08-1-/350/263/207/346/226/231/351/251/227/350/255/211/345/237/272/347/244/216/344/275/277/347/224/250.md +587 -0
  14. package/knowledge/Furion_Teaching_Manual/10-1-SqlSugar/346/225/264/345/220/210.md +336 -0
  15. package/knowledge/Furion_Teaching_Manual/11-SaaS /345/244/232/347/247/237/346/210/266/347/255/206/350/250/230.md" +271 -0
  16. package/knowledge/Furion_Teaching_Manual/12-furion-dependency-injection.md +408 -0
  17. package/knowledge/Furion_Teaching_Manual/13-/347/211/251/344/273/266/350/263/207/346/226/231/346/230/240/345/260/204/357/274/210Mapster/357/274/211.md +162 -0
  18. package/knowledge/Furion_Teaching_Manual/14-/345/210/206/345/270/203/345/274/217/347/274/223/345/255/230.md +311 -0
  19. package/knowledge/Furion_Teaching_Manual/15-/345/256/211/345/205/250/351/211/264/346/235/203.md +832 -0
  20. package/knowledge/Furion_Teaching_Manual/17-/346/252/242/350/246/226/347/257/204/346/234/254/345/274/225/346/223/216.md +327 -0
  21. package/knowledge/Furion_Teaching_Manual/18-/346/227/245/350/252/214/350/250/230/351/214/204.md +639 -0
  22. package/knowledge/Furion_Teaching_Manual/19-1-HTTP/351/201/240/347/253/257/350/253/213/346/261/202/345/237/272/347/244/216/344/275/277/347/224/250.md +621 -0
  23. package/knowledge/Furion_Teaching_Manual/19-2-HTTP/351/201/240/347/253/257/350/253/213/346/261/202/351/200/262/351/232/216/346/214/207/345/215/227.md +928 -0
  24. package/knowledge/Furion_Teaching_Manual/19-3-HTTP/351/201/240/347/253/257/350/253/213/346/261/202/345/270/270/350/246/213/345/225/217/351/241/214.md +362 -0
  25. package/knowledge/Furion_Teaching_Manual/20-/350/263/207/346/226/231/345/212/240/350/247/243/345/257/206.md +286 -0
  26. package/knowledge/Furion_Teaching_Manual/20-/351/231/204/351/214/204-KSort/350/263/207/346/226/231/347/260/275/345/220/215/345/256/214/346/225/264/345/216/237/345/247/213/347/242/274.md +305 -0
  27. package/knowledge/Furion_Teaching_Manual/21-/345/205/250/347/220/203/345/214/226/345/222/214/346/234/254/345/234/260/345/214/226.md +342 -0
  28. package/knowledge/Furion_Teaching_Manual/22-/344/272/213/344/273/266/345/214/257/346/265/201/346/216/222EventBus.md +448 -0
  29. package/knowledge/Furion_Teaching_Manual/23-JSON/345/272/217/345/210/227/345/214/226.md +340 -0
  30. package/knowledge/Furion_Teaching_Manual/24-/345/215/263/346/231/202/351/200/232/350/250/212SignalR.md +247 -0
  31. package/knowledge/Furion_Teaching_Manual/25-/350/274/224/345/212/251/350/247/222/350/211/262/346/234/215/345/213/231WorkerService.md +295 -0
  32. package/knowledge/Furion_Teaching_Manual/26-1-/346/216/222/347/250/213/344/275/234/346/245/255/345/256/232/346/231/202/344/273/273/345/213/231.md +631 -0
  33. package/knowledge/Furion_Teaching_Manual/26-2-Cron/350/241/250/351/201/224/345/274/217.md +203 -0
  34. package/knowledge/Furion_Teaching_Manual/26-3-/344/273/273/345/213/231/344/275/207/345/210/227TaskQueue.md +215 -0
  35. package/knowledge/Furion_Teaching_Manual/27-/345/210/206/346/225/243/345/274/217ID/347/224/237/346/210/220.md +86 -0
  36. package/knowledge/Furion_Teaching_Manual/28-/346/250/241/347/265/204/345/214/226/351/226/213/347/231/274.md +68 -0
  37. package/knowledge/Furion_Teaching_Manual/29-/346/265/201/350/256/212/347/211/251/344/273/266Clay.md +313 -0
  38. package/knowledge/Furion_Teaching_Manual/30-/350/204/253/346/225/217/350/231/225/347/220/206/357/274/210Sensitive Detection).md" +168 -0
  39. package/knowledge/Furion_Teaching_Manual/32-/346/234/203/350/251/261/345/222/214/347/213/200/346/205/213/347/256/241/347/220/206.md +147 -0
  40. package/knowledge/Furion_Teaching_Manual/33-IPC/347/250/213/345/272/217/351/200/232/350/250/212.md +98 -0
  41. package/knowledge/Furion_Teaching_Manual/34-2-Docker/351/203/250/347/275/262.md +313 -0
  42. package/knowledge/Furion_Teaching_Manual/34-3-Nginx/351/203/250/347/275/262.md +157 -0
  43. package/knowledge/Furion_Teaching_Manual/34-8-WindowsService/351/203/250/347/275/262.md +112 -0
  44. package/knowledge/Furion_Teaching_Manual/35-1-Docker/347/222/260/345/242/203/346/214/201/347/272/214/351/203/250/347/275/262Jenkins.md +169 -0
  45. package/knowledge/Furion_Teaching_Manual/36-1-/345/226/256/345/205/203/346/270/254/350/251/246/346/225/264/345/220/210/346/270/254/350/251/246.md +275 -0
  46. package/knowledge/Furion_Teaching_Manual/36-3-/345/237/272/346/272/226/346/270/254/350/251/246Benchmarking.md +80 -0
  47. package/knowledge/attributes.md +153 -0
  48. package/knowledge/config.md +147 -0
  49. package/knowledge/entity.md +115 -0
  50. package/knowledge/event.md +124 -0
  51. package/knowledge/plugin.md +136 -0
  52. package/knowledge/service.md +146 -0
  53. package/knowledge/sqlsugar.md +172 -0
  54. package/knowledge/vue-typescript.md +338 -0
  55. package/package.json +3 -2
@@ -0,0 +1,928 @@
1
+ # 19.Ⅱ HTTP 遠端請求 — 進階指南
2
+
3
+ > **版本說明**:僅適用於 Furion 4.9.6+(HttpAgent),不支援 .NET8 以下版本。
4
+
5
+ ---
6
+
7
+ ## 19.6 IHttpContentProcessor 內容處理器
8
+
9
+ 用於根據原始請求內容和類型建構 `HttpContent` 實例,並設定為 `HttpRequestMessage.Content`。
10
+
11
+ ### 19.6.1 內建內容處理器
12
+
13
+ | 處理器 | 適用條件 | 產出型別 |
14
+ |--------|---------|----------|
15
+ | `StringContentProcessor` | `application/json`、`application/json-patch+json`、`application/xml`、`text/xml`、`text/html`、`text/plain`、`application/soap+xml`(含 charset) | `StringContent` |
16
+ | `StreamContentProcessor` | `Stream` / `StreamContent` | `StreamContent` |
17
+ | `ByteArrayContentProcessor` | `byte[]` / `ByteArrayContent`(非 FormUrlEncoded / StringContent) | `ByteArrayContent` |
18
+ | `FormUrlEncodedContentProcessor` | `FormUrlEncodedContent` 或 `application/x-www-form-urlencoded` | `FormUrlEncodedContent` |
19
+ | `StringContentForFormUrlEncodedContentProcessor` | 同上 + `useStringContent = true`(派生自上者) | `StringContent` |
20
+ | `ReadOnlyMemoryContentProcessor` | `ReadOnlyMemory<byte>` / `ReadOnlyMemoryContent` | `ReadOnlyMemoryContent` |
21
+ | `MultipartFormDataContentProcessor` | `MultipartFormDataContent` 或 `multipart/form-data` | `MultipartFormDataContent` |
22
+ | `MessagePackContentProcessor` | `application/msgpack` | `ByteArrayContent` |
23
+
24
+ > `StringContentProcessor` 預設採用 `JsonSerializerOptions.Web` 配置。
25
+
26
+ 自訂 JSON 序列化:
27
+
28
+ ```csharp
29
+ services.AddHttpRemote(builder => {})
30
+ .ConfigureOptions(options =>
31
+ {
32
+ options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
33
+ });
34
+ ```
35
+
36
+ ### 19.6.2 IHttpContentProcessorFactory 工廠
37
+
38
+ 單例服務,按**最後新增**的處理器開始查詢 `CanProcess` 方法。找不到匹配時拋出 `InvalidOperationException`。
39
+
40
+ ```csharp
41
+ public class YourService(IHttpContentProcessorFactory factory)
42
+ {
43
+ public async Task<string> GetStringAsync()
44
+ {
45
+ var msg = new HttpRequestMessage(HttpMethod.Post, "https://furion.net/");
46
+ msg.Content = factory.Build(new { id = 1, name = "Furion" }, "application/json");
47
+ using var client = new HttpClient();
48
+ var res = await client.SendAsync(msg);
49
+ return await res.Content.ReadAsStringAsync();
50
+ }
51
+ }
52
+ ```
53
+
54
+ ### 19.6.3 自訂內容處理器
55
+
56
+ ```csharp
57
+ public class CustomStringContentProcessor : HttpContentProcessorBase
58
+ {
59
+ public override bool CanProcess(object? rawContent, string contentType)
60
+ => contentType == "application/json";
61
+
62
+ public override HttpContent? Process(object? rawContent, string contentType, Encoding? encoding)
63
+ {
64
+ if (TryProcess(rawContent, contentType, encoding, out var httpContent)) return httpContent;
65
+ var content = rawContent is string or JsonElement
66
+ ? rawContent.ToString()
67
+ : JsonConvert.SerializeObject(rawContent, new JsonSerializerOptions());
68
+ return new StringContent(content!, encoding,
69
+ new MediaTypeHeaderValue(contentType) { CharSet = encoding?.BodyName ?? "utf-8" });
70
+ }
71
+ }
72
+ ```
73
+
74
+ 套用方式:
75
+
76
+ ```csharp
77
+ // 單次請求
78
+ HttpRequestBuilder.Post("https://furion.net/")
79
+ .AddHttpContentProcessors(() => [new CustomStringContentProcessor()])
80
+ .SetJsonContent(new { id = 1, name = "Furion" });
81
+
82
+ // 全域設定
83
+ services.AddHttpRemote(b => b.AddHttpContentProcessors(() => [new CustomStringContentProcessor()]));
84
+ ```
85
+
86
+ > `HttpContentProcessorBase` 內建 `ServiceProvider` 屬性,可解析 DI 服務。
87
+
88
+ ### 19.6.4 MessagePack 支援
89
+
90
+ ```bash
91
+ dotnet add package MessagePack
92
+ ```
93
+
94
+ ```csharp
95
+ public class MessagePackContentProcessor : HttpContentProcessorBase
96
+ {
97
+ public override bool CanProcess(object? rawContent, string contentType)
98
+ => contentType == "application/msgpack";
99
+
100
+ public override HttpContent? Process(object? rawContent, string contentType, Encoding? encoding)
101
+ {
102
+ if (TryProcess(rawContent, contentType, encoding, out var httpContent)) return httpContent;
103
+ var content = rawContent as byte[] ?? MessagePackSerializer.Serialize(rawContent);
104
+ var byteContent = new ByteArrayContent(content);
105
+ byteContent.Headers.ContentType = new MediaTypeHeaderValue(contentType) { CharSet = encoding?.BodyName };
106
+ return byteContent;
107
+ }
108
+ }
109
+ ```
110
+
111
+ 模型需加 `[MessagePackObject]` 與 `[MessagePack.Key(n)]` 特性。
112
+
113
+ > 系統已內建 `MessagePackContentProcessor`(透過反射),安裝套件即自動啟用。對效能有極高要求時可自訂實作。
114
+
115
+ ### 19.6.5 Protobuf 支援
116
+
117
+ ```bash
118
+ dotnet add protobuf-net
119
+ ```
120
+
121
+ 實作方式同上,`CanProcess` 匹配 `application/x-protobuf`,使用 `Serializer.Serialize(ms, rawContent)` 轉為 `byte[]`。模型需加 `[ProtoContract]` 與 `[ProtoMember(n)]`。
122
+
123
+ ---
124
+
125
+ ## 19.7 IHttpContentConverter 內容轉換器
126
+
127
+ 用於將 `HttpResponseMessage` 轉換為目標型別。
128
+
129
+ ### 19.7.1 內建內容轉換器
130
+
131
+ | 轉換器 | 目標型別 | 內部實作 |
132
+ |--------|---------|----------|
133
+ | `StringContentConverter` | `string` | `ReadAsStringAsync` |
134
+ | `StreamContentConverter` | `Stream` | `ReadAsStreamAsync` |
135
+ | `ByteArrayContentConverter` | `byte[]` | `ReadAsByteArrayAsync` |
136
+ | `HttpResponseMessageConverter` | `HttpResponseMessage` | 直接返回 |
137
+ | `VoidContentConverter` | `void` / `VoidContent` | 返回空值 |
138
+ | `IActionResultContentConverter` | `IActionResult` | 轉換為 ActionResult |
139
+ | `ObjectContentConverter` | 其他型別 | `ReadFromJsonAsync` |
140
+
141
+ ### 19.7.2 IHttpContentConverterFactory 工廠
142
+
143
+ 單例服務,按最後新增的轉換器查詢泛型型別匹配。找不到時回退到 `IObjectContentConverterFactory`。
144
+
145
+ ### 19.7.3 IObjectContentConverterFactory
146
+
147
+ 回退工廠,預設返回 `ObjectContentConverter()` 實例,使用 `ReadFromJsonAsync` + `System.Text.Json`。
148
+
149
+ > 手動轉換建議使用 `IHttpContentConverterFactory`(已內部包含 `IObjectContentConverterFactory`)。
150
+
151
+ ### 19.7.4 自訂物件內容轉換器
152
+
153
+ 需同時提供泛型和非泛型版本:
154
+
155
+ ```csharp
156
+ public class CustomObjectContentConverter : ObjectContentConverter
157
+ {
158
+ public override async Task<object?> ReadAsync(Type resultType, HttpResponseMessage res, CancellationToken ct = default)
159
+ {
160
+ var ctx = HttpRemoteUtility.ResolveJsonSerializationContext(resultType, res, ServiceProvider);
161
+ var value = await res.Content.ReadFromJsonAsync(ctx.ResultType, ctx.JsonSerializerOptions, ct);
162
+ return ctx.GetResultValue(value);
163
+ }
164
+ // 同步版本 Read() 類似
165
+ }
166
+
167
+ public class CustomObjectContentConverter<T> : CustomObjectContentConverter, IHttpContentConverter<T>
168
+ {
169
+ public virtual async Task<T?> ReadAsync(HttpResponseMessage res, CancellationToken ct = default)
170
+ => (T?)await base.ReadAsync(typeof(T), res, ct);
171
+ }
172
+ ```
173
+
174
+ 建立自訂工廠並註冊:
175
+
176
+ ```csharp
177
+ public sealed class CustomObjectContentConverterFactory : IObjectContentConverterFactory
178
+ {
179
+ public IHttpContentConverter<T> GetConverter<T>(HttpResponseMessage res)
180
+ => res.IsXmlContent() ? new XmlObjectContentConverter<T>() : new CustomObjectContentConverter<T>();
181
+ public IHttpContentConverter GetConverter(Type type, HttpResponseMessage res)
182
+ => res.IsXmlContent() ? new XmlObjectContentConverter() : new CustomObjectContentConverter();
183
+ }
184
+
185
+ services.AddHttpRemote(b => b.UseObjectContentConverterFactory<CustomObjectContentConverterFactory>());
186
+ ```
187
+
188
+ ### 19.7.5 自訂內容轉換器
189
+
190
+ ```csharp
191
+ public class SpanCharContentConverter : HttpContentConverterBase<Span<char>>
192
+ {
193
+ public override async Task<Span<char>?> ReadAsync(HttpResponseMessage res, CancellationToken ct = default)
194
+ {
195
+ var str = await res.Content.ReadAsStringAsync(ct);
196
+ return str.AsSpan();
197
+ }
198
+ }
199
+
200
+ // 全域
201
+ services.AddHttpRemote(b => b.AddHttpContentConverters(() => [new SpanCharContentConverter()]));
202
+ // 單次
203
+ builder.AddHttpContentConverters(() => [new SpanCharContentConverter()]);
204
+ ```
205
+
206
+ ---
207
+
208
+ ## 19.8 IHttpRemoteService 服務 ✨
209
+
210
+ HTTP 遠端請求的核心入口服務,預設以**單例**模式註冊。
211
+
212
+ ```csharp
213
+ services.AddHttpRemote();
214
+ // 若遇到二義性錯誤:
215
+ services.AddHttpRemote(builder => {});
216
+ ```
217
+
218
+ 注入方式:
219
+
220
+ ```csharp
221
+ // 建構子注入(.NET8+)
222
+ public class YourService(IHttpRemoteService httpRemoteService) { }
223
+
224
+ // 方法注入
225
+ public Task<string> GetResource([FromServices] IHttpRemoteService httpRemoteService) { }
226
+
227
+ // 無 DI 環境
228
+ var httpRemoteService = App.GetRequiredService<IHttpRemoteService>();
229
+ ```
230
+
231
+ ### 19.8.1 HttpRemoteBuilder 設定一覽
232
+
233
+ ```csharp
234
+ services.AddHttpRemote(builder =>
235
+ {
236
+ builder.AddHttpContentProcessors(() => [...]);
237
+ builder.AddHttpContentConverters(() => [...]);
238
+ builder.UseObjectContentConverterFactory<CustomFactory>();
239
+ builder.AddHttpDeclarative<IHttpService>();
240
+ builder.AddHttpDeclaratives([typeof(IHttpService), typeof(IHttpService2)]);
241
+ builder.AddHttpDeclarativesFromAssemblies([Assembly.GetEntryAssembly()]);
242
+ builder.AddHttpDeclarativeExtractors(() => [...]);
243
+ builder.AddHttpDeclarativeExtractorsFromAssemblies([...]);
244
+ });
245
+ ```
246
+
247
+ ### 19.8.2 HttpRemoteOptions 設定選項
248
+
249
+ ```csharp
250
+ services.AddHttpRemote(builder => {})
251
+ .ConfigureOptions(options =>
252
+ {
253
+ options.DefaultContentType = "application/json";
254
+ options.DefaultFileDownloadDirectory = @"C:\Downloads\";
255
+ options.ProfilerLogLevel = LogLevel.Warning;
256
+ options.AllowAutoRedirect = true; // 預設 true
257
+ options.MaximumAutomaticRedirections = 50; // 預設 50
258
+ options.FallbackBaseAddress = new Uri("https://localhost:5000");
259
+ options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
260
+ options.Configuration = builder.Configuration; // Furion: App.Configuration
261
+ options.UrlParameterFormatter = new UrlParameterFormatter();
262
+ options.FallbackLogger = Console.WriteLine;
263
+ options.HttpRequestBuilderConfigurer = null;
264
+ });
265
+
266
+ // 支援服務解析的重載
267
+ .ConfigureOptions((options, serviceProvider) => { });
268
+ ```
269
+
270
+ ### 19.8.3 統一設定 HttpClient
271
+
272
+ ```csharp
273
+ services.ConfigureHttpClientDefaults(b => b.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()));
274
+ // 或
275
+ services.AddHttpRemote().ConfigureHttpClientDefaults(b => { });
276
+ ```
277
+
278
+ ### 19.8.4 內建方法速查
279
+
280
+ **核心方法**(以 `HttpRequestBuilder` 為參數):
281
+
282
+ | 方法模式 | 回傳型別 |
283
+ |---------|---------|
284
+ | `Send / SendAsync` | `HttpResponseMessage` |
285
+ | `SendAs<T> / SendAsAsync<T>` | `T` |
286
+ | `Send<T> / SendAsync<T>` | `HttpRemoteResult<T>` |
287
+ | `SendAs(resultType, ...) / SendAsAsync(resultType, ...)` | `object` |
288
+ | `SendAsString / SendAsStringAsync` | `string` |
289
+ | `SendAsByteArray / SendAsByteArrayAsync` | `byte[]` |
290
+ | `SendAsStream / SendAsStreamAsync` | `Stream` |
291
+
292
+ **請求謂詞方法**(GET / POST / PUT / DELETE / HEAD / OPTIONS / TRACE / PATCH):
293
+
294
+ 每個謂詞提供上述全部回傳模式,簽名為:
295
+
296
+ ```csharp
297
+ httpRemoteService.GetAsync(requestUri, configure, cancellationToken);
298
+ httpRemoteService.GetAsAsync<T>(requestUri, configure, cancellationToken);
299
+ httpRemoteService.GetAsync<T>(requestUri, configure, cancellationToken); // HttpRemoteResult<T>
300
+ httpRemoteService.GetAsStringAsync(requestUri, configure, cancellationToken);
301
+ httpRemoteService.GetAsByteArrayAsync(requestUri, configure, cancellationToken);
302
+ httpRemoteService.GetAsStreamAsync(requestUri, configure, cancellationToken);
303
+ // 各謂詞均有同步版本和含 completionOption 的重載
304
+ ```
305
+
306
+ **特定功能方法**:`DownloadFile(Async)`、`UploadFile(Async)`、`ServerSentEvents(Async)`、`StressTestHarness(Async)`、`LongPolling(Async)`、`Declarative(Async<T>)`。
307
+
308
+ ### 19.8.5 擴充 IHttpRemoteService
309
+
310
+ ```csharp
311
+ public static class HttpRemoteServiceExtensions
312
+ {
313
+ public static async Task<Span<char>> SendAsSpanAsync(this IHttpRemoteService svc,
314
+ HttpRequestBuilder builder, CancellationToken ct = default)
315
+ {
316
+ var str = await svc.SendAsStringAsync(builder, ct);
317
+ return str.AsSpan();
318
+ }
319
+ }
320
+ ```
321
+
322
+ ---
323
+
324
+ ## 19.9 HttpRemoteResult\<TResult\> 回傳值
325
+
326
+ 不含 `As` 關鍵字的泛型方法回傳 `HttpRemoteResult<T>`,封裝了完整的回應資訊。
327
+
328
+ 主要屬性:`ResponseMessage`、`ContentType`、`CharSet`、`StatusCode`、`IsSuccessStatusCode`、`Result`、`RequestDuration`、`Headers`、`ContentHeaders`、`RawSetCookies`、`SetCookies`、`Version`、`HttpClientName`。
329
+
330
+ ```csharp
331
+ // 解構表達式
332
+ var (result, response) = await httpRemoteService.GetAsync<string>("https://furion.net/");
333
+ var (result, response, isSuccess, statusCode) = await httpRemoteService.GetAsync<string>("https://furion.net/");
334
+
335
+ // ToString() 輸出完整請求/回應標頭
336
+ Console.WriteLine(httpResult.ToString());
337
+ ```
338
+
339
+ ---
340
+
341
+ ## 19.10 下載網路資源
342
+
343
+ ### 基本方式
344
+
345
+ ```csharp
346
+ var stream = await httpRemoteService.GetAsStreamAsync("https://furion.net/img/furionlogo.png");
347
+ using var fs = new FileStream(@"C:\file.png", FileMode.Create, FileAccess.Write, FileShare.None, 4096, true);
348
+ await stream.CopyToAsync(fs);
349
+ ```
350
+
351
+ ### 框架內建下載
352
+
353
+ ```csharp
354
+ var result = await httpRemoteService.DownloadFileAsync(
355
+ "https://example.com/file.exe", @"C:\Downloads\",
356
+ async progress => { progress.UpdateConsoleProgress(); await Task.CompletedTask; },
357
+ fileExistsBehavior: FileExistsBehavior.Overwrite);
358
+ ```
359
+
360
+ `FileExistsBehavior`:`CreateNew`(預設,已存在則拋例外)、`Overwrite`(覆蓋)、`Skip`(跳過)。
361
+
362
+ `FileTransferResult` 屬性:`IsSuccess`、`RequestUri`、`FilePath`、`FileSize`、`ElapsedMilliseconds`、`StatusCode`。
363
+
364
+ `FileTransferProgress` 屬性:`FilePath`、`FileName`、`FileSize`、`Transferred`、`PercentageComplete`、`TransferRate`、`TimeElapsed`、`EstimatedTimeRemaining`。方法:`ToString()`、`ToSummaryString()`、`UpdateConsoleProgress()`。
365
+
366
+ ### 19.10.1 HttpFileDownloadBuilder
367
+
368
+ ```csharp
369
+ HttpRequestBuilder.DownloadFile("https://furion.net/img/logo.png", @"C:\Downloads\")
370
+ .SetBufferSize(80 * 1024)
371
+ .SetDestinationPath(@"C:\Downloads\")
372
+ .SetFileExistsBehavior(FileExistsBehavior.Overwrite)
373
+ .SetProgressInterval(TimeSpan.FromSeconds(1))
374
+ .SetOnTransferStarted(() => {})
375
+ .SetOnProgressChanged(async progress => { })
376
+ .SetOnTransferCompleted(duration => {})
377
+ .SetOnTransferFailed(exception => {})
378
+ .SetOnFileExistAndSkip(() => {})
379
+ .SetEventHandler<CustomFileTransferEventHandler>()
380
+ .WithRequest(builder => {})
381
+ .SetMaxThreads(4);
382
+
383
+ var result = await httpRemoteService.SendAsync(downloadBuilder, ct);
384
+ ```
385
+
386
+ ### 19.10.2 IHttpFileTransferEventHandler
387
+
388
+ ```csharp
389
+ public class CustomFileTransferEventHandler : IHttpFileTransferEventHandler
390
+ {
391
+ public void OnTransferStarted() {}
392
+ public Task OnProgressChangedAsync(FileTransferProgress progress) => Task.CompletedTask;
393
+ public void OnTransferCompleted(long duration) {}
394
+ public void OnTransferFailed(Exception exception) {}
395
+ }
396
+
397
+ services.TryAddTransient<CustomFileTransferEventHandler>();
398
+ ```
399
+
400
+ > 事件觸發順序:Builder 回呼 → EventHandler 方法。
401
+
402
+ ---
403
+
404
+ ## 19.11 上傳檔案資源
405
+
406
+ ### Form 表單方式
407
+
408
+ ```csharp
409
+ await httpRemoteService.PostAsync("https://localhost:7044/HttpRemote/AddFile",
410
+ b => b.SetMultipartContent(m => m.AddFileAsStream(@"C:\file.jpg", "file")));
411
+
412
+ // 多檔上傳
413
+ await httpRemoteService.PostAsync("https://localhost:7044/HttpRemote/AddFiles",
414
+ b => b.SetMultipartContent(m => m
415
+ .AddFileAsStream(@"C:\file1.jpg", "files")
416
+ .AddFileFromRemote("https://example.com/logo.png", "files")));
417
+ ```
418
+
419
+ ### 框架內建上傳(帶進度)
420
+
421
+ ```csharp
422
+ await httpRemoteService.UploadFileAsync("https://localhost:7044/HttpRemote/AddFile",
423
+ @"C:\file.jpg", "file",
424
+ async progress => { progress.UpdateConsoleProgress(); await Task.CompletedTask; });
425
+ ```
426
+
427
+ 限制檔案類型與大小:
428
+
429
+ ```csharp
430
+ HttpRequestBuilder.UploadFile("https://...", @"C:\file.jpg", "file")
431
+ .SetAllowedFileExtensions(".jpg;.png")
432
+ .SetMaxFileSizeInBytes(5 * 1024 * 1024);
433
+ ```
434
+
435
+ ### 19.11.1 HttpFileUploadBuilder
436
+
437
+ ```csharp
438
+ HttpRequestBuilder.UploadFile("https://...", @"C:\file.jpg", "file")
439
+ .SetContentType("image/jpeg")
440
+ .SetAllowedFileExtensions([".jpg", ".png"])
441
+ .SetMaxFileSizeInBytes(5 * 1024 * 1024)
442
+ .SetProgressInterval(TimeSpan.FromSeconds(1))
443
+ .SetOnTransferStarted(() => {})
444
+ .SetOnProgressChanged(async progress => { })
445
+ .SetOnTransferCompleted(duration => {})
446
+ .SetOnTransferFailed(exception => {})
447
+ .SetEventHandler<CustomFileTransferEventHandler>()
448
+ .WithRequest(builder => {});
449
+ ```
450
+
451
+ > `UploadFile` 擴充僅支援**單檔**上傳。使用上傳功能時建議**停用請求分析工具**。
452
+
453
+ ---
454
+
455
+ ## 19.12 壓力與模擬測試
456
+
457
+ ```csharp
458
+ var result = await httpRemoteService.StressTestHarnessAsync("https://furion.net/");
459
+ Console.WriteLine(result.ToString());
460
+ ```
461
+
462
+ `StressTestHarnessResult` 屬性:`TotalRequests`、`TotalTimeInSeconds`、`SuccessfulRequests`、`FailedRequests`、`QueriesPerSecond`、`MinResponseTime`、`MaxResponseTime`、`AverageResponseTime`、`Percentile10/25/50/75/90/99/9999ResponseTime`。
463
+
464
+ 自訂參數:
465
+
466
+ ```csharp
467
+ HttpRequestBuilder.StressTestHarness("https://furion.net/")
468
+ .SetNumberOfRequests(1000)
469
+ .SetNumberOfRounds(5)
470
+ .SetMaxDegreeOfParallelism(500)
471
+ .DisableCache()
472
+ .WithRequest(builder => {});
473
+ ```
474
+
475
+ > 壓測自動加入 `X-Stress-Test: Harness` 標頭,且預設停用請求分析工具。
476
+
477
+ ---
478
+
479
+ ## 19.13 長輪詢 Long Polling
480
+
481
+ ```csharp
482
+ await httpRemoteService.LongPollingAsync("https://localhost:7044/HttpRemote/LongPolling",
483
+ async (res, token) => { Console.WriteLine(await res.Content.ReadAsStringAsync(token)); await Task.CompletedTask; },
484
+ cancellationToken: ct);
485
+ ```
486
+
487
+ ### HttpLongPollingBuilder
488
+
489
+ ```csharp
490
+ HttpRequestBuilder.LongPolling("https://...", async (res, token) => { })
491
+ .SetRetryInterval(TimeSpan.FromSeconds(2))
492
+ .SetMaxRetries(500)
493
+ .SetTimeout(TimeSpan.FromMinutes(5))
494
+ .SetOnError(async res => {})
495
+ .SetOnEndOfStream(async res => {})
496
+ .SetEventHandler<CustomLongPollingEventHandler>()
497
+ .WithRequest(builder => {});
498
+ ```
499
+
500
+ `IHttpLongPollingEventHandler`:`OnDataReceivedAsync`、`OnErrorAsync`、`OnEndOfStreamAsync`。
501
+
502
+ > 終止方式:`CancellationToken` 或回應標頭包含 `X-End-Of-Stream`。
503
+
504
+ ---
505
+
506
+ ## 19.14 Server-Sent Events 單向通訊
507
+
508
+ ```csharp
509
+ await httpRemoteService.ServerSentEventsAsync("https://localhost:7044/HttpRemote/Events",
510
+ async (data, token) => { Console.WriteLine(data.Data.ToString()); await Task.CompletedTask; },
511
+ cancellationToken: ct);
512
+ ```
513
+
514
+ `ServerSentEventsData` 屬性:`Event`、`Data`、`Id`、`Retry`、`CustomFields`。
515
+
516
+ ### HttpServerSentEventsBuilder
517
+
518
+ ```csharp
519
+ HttpRequestBuilder.ServerSentEvents("https://...", async (data, token) => { })
520
+ .SetDefaultRetryInterval(2000)
521
+ .SetMaxRetries(500)
522
+ .SetOnOpen(() => Console.WriteLine("連線成功"))
523
+ .SetOnError(ex => Console.WriteLine(ex.Message))
524
+ .SetEventHandler<CustomServerSentEventsEventHandler>()
525
+ .WithRequest(builder => {});
526
+ ```
527
+
528
+ > ⚠️ 發送 SSE 請求時應**停用請求分析工具**。框架支援任意請求謂詞配置 SSE。
529
+
530
+ ---
531
+
532
+ ## 19.15 WebSocket 雙工通訊
533
+
534
+ ```csharp
535
+ using var ws = new WebSocketClient("wss://localhost:7044/ws");
536
+
537
+ ws.Connected += (s, e) => Console.WriteLine("連線成功");
538
+ ws.Closed += (s, e) => Console.WriteLine("連線關閉");
539
+ ws.TextReceived += (s, r) => Console.WriteLine(r.Message);
540
+ ws.BinaryReceived += (s, r) => Console.WriteLine(r.Message);
541
+
542
+ await ws.ConnectAsync(ct);
543
+ await ws.SendAsync("Hello", cancellationToken: ct);
544
+ await ws.CloseAsync(ct);
545
+ ```
546
+
547
+ `WebSocketClientOptions` 屬性:`ServerUri`、`ReconnectInterval`(預設 2 秒)、`MaxReconnectRetries`(預設 10)、`Timeout`、`ReceiveBufferSize`、`Configure`。
548
+
549
+ 事件:`Connecting`、`Connected`、`Reconnecting`、`Reconnected`、`Closing`、`Closed`、`ReceivingStarted`、`ReceivingStopped`、`TextReceived`、`BinaryReceived`。
550
+
551
+ ---
552
+
553
+ ## 19.16 HttpContext 轉發和代理 ✨
554
+
555
+ ### 前置設定
556
+
557
+ ```csharp
558
+ services.AddHttpContextAccessor(); // Furion 已預設注入
559
+ app.UseEnableBuffering();
560
+ ```
561
+
562
+ ### 基本使用
563
+
564
+ ```csharp
565
+ [HttpGet]
566
+ public Task<IActionResult?> ForwardToWebSite()
567
+ => httpContextAccessor.HttpContext.ForwardAsResultAsync("https://github.com");
568
+
569
+ [HttpPost]
570
+ public Task<YourModel?> ForwardToForm(int id, [FromForm] YourModel model)
571
+ => httpContextAccessor.HttpContext.ForwardAsAsync<YourModel>("https://localhost:7044/HttpRemote/AddForm");
572
+ ```
573
+
574
+ ### [Forward] 特性
575
+
576
+ ```csharp
577
+ [HttpGet]
578
+ [Forward("https://github.com")]
579
+ public Task<IActionResult?> ForwardToWebSite() => throw new NotImplementedException();
580
+ ```
581
+
582
+ `ForwardAttribute` 屬性:`RequestUri`、`Method`、`HttpClientName`、`CompletionOption`、`WithQueryParameters`、`WithRequestHeaders`、`WithResponseStatusCode`、`WithResponseHeaders`、`WithResponseContentHeaders`、`IgnoreRequestHeaders`、`IgnoreResponseHeaders`、`ResetHostRequestHeader`。
583
+
584
+ ### HttpContextForwardOptions
585
+
586
+ | 屬性 | 預設值 | 說明 |
587
+ |------|--------|------|
588
+ | `WithQueryParameters` | `true` | 轉發查詢參數 |
589
+ | `WithRequestHeaders` | `true` | 轉發請求標頭 |
590
+ | `WithResponseStatusCode` | `true` | 轉發回應狀態碼 |
591
+ | `WithResponseHeaders` | `true` | 轉發回應標頭 |
592
+ | `WithResponseContentHeaders` | `true` | 轉發回應內容標頭 |
593
+ | `ResetHostRequestHeader` | `true` | 重設 Host 標頭 |
594
+ | `IgnoreRequestHeaders` | — | 忽略的請求標頭 |
595
+ | `IgnoreResponseHeaders` | — | 忽略的回應標頭 |
596
+ | `OnForward` | — | 轉發前自訂操作 |
597
+
598
+ 轉發擴充方法涵蓋:`Forward(Async)`、`ForwardAs<T>(Async)`、`ForwardAsString(Async)`、`ForwardAsByteArray(Async)`、`ForwardAsStream(Async)`、`ForwardAsResult(Async)`。
599
+
600
+ ### 微服務應用
601
+
602
+ 透過 `X-Forward-To` 請求標頭可動態取得轉發位址,配合負載均衡策略實現動態請求分發、API Gateway 等。
603
+
604
+ ### 轉發忽略標頭
605
+
606
+ 請求標頭自動忽略:`X-Forward-To`、`Host`、`Accept`、`Accept-CH/Charset/Encoding/Language/Patch/Post/Ranges`。
607
+ 回應標頭自動忽略:`Content-Type`、`Transfer-Encoding`、`Keep-Alive`、`Upgrade`、`Proxy-Connection`。
608
+
609
+ ---
610
+
611
+ ## 19.17 FTP 客戶端功能展望
612
+
613
+ > 框架尚未正式整合 FTP 客戶端功能,相關開發已初步完成。未來視需求增長可能納入。
614
+
615
+ ---
616
+
617
+ ## 19.18 CancellationToken 取消 HTTP 請求
618
+
619
+ ```csharp
620
+ // 方式一:Controller Action 參數
621
+ public async Task<string?> GetContent(CancellationToken cancellationToken)
622
+ => await httpRemoteService.GetAsAsync<string>("https://furion.net/", cancellationToken: cancellationToken);
623
+
624
+ // 方式二:HttpContext.RequestAborted
625
+ await httpRemoteService.GetAsAsync<string>("https://furion.net/",
626
+ cancellationToken: httpContextAccessor.HttpContext.RequestAborted);
627
+
628
+ // 方式三:手動建立 CancellationTokenSource
629
+ using var cts = new CancellationTokenSource();
630
+ cts.CancelAfter(100);
631
+ await httpRemoteService.GetAsAsync<string>("https://furion.net/", cancellationToken: cts.Token);
632
+ ```
633
+
634
+ ---
635
+
636
+ ## 19.19 HttpClient 實例設定 ✨
637
+
638
+ ### 19.19.1 常見屬性
639
+
640
+ ```csharp
641
+ services.AddHttpClient(string.Empty, client =>
642
+ {
643
+ client.BaseAddress = new Uri("http://localhost:5000");
644
+ client.Timeout = TimeSpan.FromMinutes(10);
645
+ client.MaxResponseContentBufferSize = 5 * 1024;
646
+ client.DefaultRequestVersion = HttpVersion.Version10;
647
+ client.DefaultRequestHeaders.Add("User-Agent", "...");
648
+ client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
649
+ });
650
+ ```
651
+
652
+ ### 19.19.2 IHttpClientBuilder
653
+
654
+ ```csharp
655
+ services.AddHttpClient(string.Empty, client => {})
656
+ .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
657
+ {
658
+ AllowAutoRedirect = true,
659
+ UseDefaultCredentials = true,
660
+ UseCookies = true,
661
+ });
662
+ ```
663
+
664
+ ### 19.19.3 SSL 憑證
665
+
666
+ ```csharp
667
+ // 客戶端憑證
668
+ .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
669
+ {
670
+ ClientCertificates = { X509CertificateLoader.LoadPkcs12FromFile("cert.pfx", "password") }
671
+ });
672
+
673
+ // 忽略 SSL 驗證
674
+ .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
675
+ {
676
+ ServerCertificateCustomValidationCallback = HttpRemoteUtility.IgnoreSslErrors,
677
+ SslProtocols = HttpRemoteUtility.AllSslProtocols
678
+ });
679
+
680
+ // SocketsHttpHandler 方式
681
+ .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
682
+ {
683
+ SslOptions = new SslClientAuthenticationOptions
684
+ {
685
+ RemoteCertificateValidationCallback = HttpRemoteUtility.IgnoreSocketSslErrors,
686
+ EnabledSslProtocols = HttpRemoteUtility.AllSslProtocols
687
+ }
688
+ });
689
+
690
+ // 單次請求
691
+ HttpRequestBuilder.Get("https://furion.net/")
692
+ .SetHttpClientProvider(() => (new HttpClient(new HttpClientHandler
693
+ {
694
+ ServerCertificateCustomValidationCallback = HttpRemoteUtility.IgnoreSslErrors,
695
+ SslProtocols = HttpRemoteUtility.AllSslProtocols
696
+ }), client => client.Dispose()));
697
+ ```
698
+
699
+ ### 19.19.4 Proxy 代理
700
+
701
+ ```csharp
702
+ .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
703
+ {
704
+ Proxy = new WebProxy("http://proxyserver:8080", false)
705
+ {
706
+ Credentials = new NetworkCredential("username", "password")
707
+ },
708
+ UseProxy = true
709
+ });
710
+ ```
711
+
712
+ ### 19.19.5 HTTP/3
713
+
714
+ ```csharp
715
+ services.AddHttpClient(string.Empty, client =>
716
+ {
717
+ client.DefaultRequestVersion = HttpVersion.Version30;
718
+ client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact;
719
+ });
720
+ ```
721
+
722
+ ### 19.19.6 擴充設定(JSON 序列化)
723
+
724
+ ```csharp
725
+ services.AddHttpClient(string.Empty)
726
+ .ConfigureOptions(options =>
727
+ {
728
+ options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
729
+ });
730
+
731
+ // 支援服務解析
732
+ .ConfigureOptions((options, serviceProvider) => { });
733
+ ```
734
+
735
+ ### 19.19.7 服務解析設定
736
+
737
+ ```csharp
738
+ services.AddHttpClient(string.Empty, (serviceProvider, client) =>
739
+ {
740
+ var config = serviceProvider.GetRequiredService<IConfiguration>();
741
+ client.BaseAddress = new Uri(config["your-base-address"]);
742
+ });
743
+ ```
744
+
745
+ ---
746
+
747
+ ## 19.20 DelegatingHandler 請求處理委託(攔截)
748
+
749
+ 可視為 HTTP 請求的「中介軟體」,用於請求前處理、回應後處理、錯誤處理、日誌記錄等。
750
+
751
+ ### 19.20.2 自訂 DelegatingHandler
752
+
753
+ ```csharp
754
+ public class CustomHandler : DelegatingHandler
755
+ {
756
+ protected override async Task<HttpResponseMessage> SendAsync(
757
+ HttpRequestMessage request, CancellationToken ct)
758
+ {
759
+ request.Headers.Add("Custom-Header", "Value"); // 請求前
760
+ var response = await base.SendAsync(request, ct);
761
+ Console.WriteLine($"Status: {response.StatusCode}"); // 回應後
762
+ return response;
763
+ }
764
+ }
765
+
766
+ services.TryAddTransient<CustomHandler>();
767
+ services.AddHttpClient(string.Empty).AddHttpMessageHandler<CustomHandler>();
768
+ ```
769
+
770
+ > 支援多次呼叫 `.AddHttpMessageHandler<T>()` 串接多個處理器,**執行順序與註冊順序一致**。
771
+
772
+ ### 19.20.3 自動重新整理授權 Token
773
+
774
+ ```csharp
775
+ public class AuthorizationDelegatingHandler : DelegatingHandler
776
+ {
777
+ protected override async Task<HttpResponseMessage> SendAsync(
778
+ HttpRequestMessage request, CancellationToken ct)
779
+ {
780
+ var clonedRequest = await request.CloneAsync(ct);
781
+ var response = await base.SendAsync(clonedRequest, ct);
782
+
783
+ if (response.StatusCode != HttpStatusCode.Unauthorized) return response;
784
+
785
+ var newToken = await GetNewTokenAsync();
786
+ clonedRequest = await clonedRequest.CloneAsync(ct);
787
+ clonedRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken);
788
+ return await base.SendAsync(clonedRequest, ct);
789
+ }
790
+ }
791
+ ```
792
+
793
+ ---
794
+
795
+ ## 19.21 HttpClientHandler 底層處理器
796
+
797
+ **呼叫鏈**:`CustomHandler3 → CustomHandler2 → CustomHandler1 → HttpClientHandler → 網路`
798
+
799
+ - `DelegatingHandler`:可插拔的中介層邏輯
800
+ - `HttpClientHandler`:最終執行網路 I/O 的處理器
801
+
802
+ 核心功能:`AllowAutoRedirect`、`UseCookies` + `CookieContainer`、`Proxy` + `UseProxy`、`ServerCertificateCustomValidationCallback`、`UseDefaultCredentials`、`AutomaticDecompression`。
803
+
804
+ > **最佳實踐**:業務邏輯用 `DelegatingHandler`;底層 TLS/代理/Cookie 控制才考慮自訂 `HttpClientHandler`。
805
+
806
+ ---
807
+
808
+ ## 19.22 RateLimitedStream 帶速率限制的串流
809
+
810
+ ```csharp
811
+ var stream = await httpRemoteService.GetAsStreamAsync("https://furion.net/", HttpCompletionOption.ResponseHeadersRead);
812
+ var rateLimitedStream = new RateLimitedStream(stream, 1024 * 1024 * 1); // 限制 1MB/s
813
+ ```
814
+
815
+ > 基於**令牌桶演算法**實作,實際速率可能有 ~5% 誤差。適用於 SaaS/PaaS 平台的上傳/下載速率控制。
816
+
817
+ ---
818
+
819
+ ## 19.23 FileTypeMapper 檔案 MIME 類型映射
820
+
821
+ ```csharp
822
+ var mapper = new FileTypeMapper();
823
+ mapper.TryGetContentType("image.jpg", out var mimeType); // "image/jpeg"
824
+ ```
825
+
826
+ 內建 389 種副檔名映射。所有檔案上傳方法(`AddFile`、`AddFileAsStream` 等)未指定 `contentType` 時自動解析,找不到則預設 `application/octet-stream`。
827
+
828
+ ---
829
+
830
+ ## 19.24 DigestCredentials 摘要身分認證
831
+
832
+ ```csharp
833
+ var digestCredentials = DigestCredentials.GetDigestCredentials(
834
+ "https://furion.net/digest", "admin", "a123456789", HttpMethod.Get);
835
+ httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Digest", digestCredentials);
836
+ ```
837
+
838
+ ---
839
+
840
+ ## 19.25 請求和回應策略(重試)
841
+
842
+ ```csharp
843
+ // 基本重試
844
+ var str = await Policy<string>.Retry(3)
845
+ .ExecuteAsync(async () => await httpRemoteService.GetAsStringAsync("https://furion.net/"));
846
+
847
+ // 進階重試
848
+ var str = await Policy<string>.Retry(3)
849
+ .Handle<HttpRequestException>()
850
+ .OnWaitRetry((ctx, delay) => Console.WriteLine($"等待 {delay.TotalSeconds} 秒"))
851
+ .OnRetrying(ctx => Console.WriteLine($"重試第 {ctx.RetryCount} 次"))
852
+ .WaitAndRetry(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(3))
853
+ .ExecuteAsync(async () => await httpRemoteService.GetAsStringAsync("https://furion.net/",
854
+ builder => builder.EnsureSuccessStatusCode()));
855
+ ```
856
+
857
+ > 僅在 `ExecuteAsync` 內部拋出例外時觸發重試。建議搭配 `EnsureSuccessStatusCode()`。
858
+
859
+ ---
860
+
861
+ ## 19.26 HTTP 請求日誌(關閉)
862
+
863
+ ```json
864
+ {
865
+ "Logging": {
866
+ "LogLevel": {
867
+ "System.Net.Http.HttpClient": "Warning"
868
+ }
869
+ }
870
+ }
871
+ ```
872
+
873
+ ```csharp
874
+ services.AddHttpClient(string.Empty).RemoveAllLoggers();
875
+ // 一鍵關閉
876
+ services.ConfigureHttpClientDefaults(b => b.RemoveAllLoggers());
877
+ ```
878
+
879
+ ---
880
+
881
+ ## 19.27 應用案例
882
+
883
+ ### 19.27.1 Clay 流變物件
884
+
885
+ ```csharp
886
+ // 設定序列化轉換器
887
+ services.AddHttpRemote(o => {})
888
+ .ConfigureOptions(o => o.JsonSerializerOptions.AddClayConverters());
889
+
890
+ // 發送
891
+ dynamic payload = new Clay();
892
+ payload.id = 1;
893
+ payload.name = "furion";
894
+ var content = await httpRemoteService.PostAsStringAsync("https://...",
895
+ b => b.SetJsonContent(payload));
896
+ dynamic clay = Clay.Parse(content);
897
+ ```
898
+
899
+ 自訂 `ClayContentConverter` 與 `DynamicContentConverter` 可直接使用 `Clay` / `dynamic` 作為泛型參數接收:
900
+
901
+ ```csharp
902
+ services.AddHttpRemote(o =>
903
+ o.AddHttpContentConverters(() => [new ClayContentConverter(), new DynamicContentConverter()]));
904
+
905
+ dynamic clay = await httpRemoteService.PostAsAsync<Clay>("https://...", b => b.SetJsonContent(payload));
906
+ dynamic clay = await httpRemoteService.PostAsAsync<dynamic>("https://...", b => b.SetJsonContent(payload));
907
+ ```
908
+
909
+ ### 19.27.2 Blazor WebAssembly
910
+
911
+ ```csharp
912
+ builder.Services.AddHttpRemote()
913
+ .ConfigureOptions((options, sp) =>
914
+ {
915
+ options.FallbackBaseAddress = new Uri(sp.GetRequiredService<NavigationManager>().BaseUri);
916
+ });
917
+ ```
918
+
919
+ ```razor
920
+ @inject IHttpRemoteService Http
921
+
922
+ @code {
923
+ protected override async Task OnInitializedAsync()
924
+ {
925
+ forecasts = await Http.GetAsAsync<WeatherForecast[]>("sample-data/weather.json");
926
+ }
927
+ }
928
+ ```