@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.
- package/dist/index.js +83 -88
- package/knowledge/Furion_Teaching_Manual/04-1-/351/205/215/347/275/256.md +442 -0
- package/knowledge/Furion_Teaching_Manual/04-2-/351/201/270/351/240/205.md +363 -0
- package/knowledge/Furion_Teaching_Manual/05-1-/345/213/225/346/205/213WebAPI.md +825 -0
- package/knowledge/Furion_Teaching_Manual/05-2-HttpContext.md +217 -0
- 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
- 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
- package/knowledge/Furion_Teaching_Manual/05-5-/344/270/255/344/273/213/350/273/237/351/253/224Middleware.md +328 -0
- 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
- 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
- package/knowledge/Furion_Teaching_Manual/06-2/347/254/254/344/270/211/346/226/271API_Scalar.md +91 -0
- 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
- 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
- package/knowledge/Furion_Teaching_Manual/10-1-SqlSugar/346/225/264/345/220/210.md +336 -0
- 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
- package/knowledge/Furion_Teaching_Manual/12-furion-dependency-injection.md +408 -0
- 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
- package/knowledge/Furion_Teaching_Manual/14-/345/210/206/345/270/203/345/274/217/347/274/223/345/255/230.md +311 -0
- package/knowledge/Furion_Teaching_Manual/15-/345/256/211/345/205/250/351/211/264/346/235/203.md +832 -0
- 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
- package/knowledge/Furion_Teaching_Manual/18-/346/227/245/350/252/214/350/250/230/351/214/204.md +639 -0
- 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
- 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
- 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
- package/knowledge/Furion_Teaching_Manual/20-/350/263/207/346/226/231/345/212/240/350/247/243/345/257/206.md +286 -0
- 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
- 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
- package/knowledge/Furion_Teaching_Manual/22-/344/272/213/344/273/266/345/214/257/346/265/201/346/216/222EventBus.md +448 -0
- package/knowledge/Furion_Teaching_Manual/23-JSON/345/272/217/345/210/227/345/214/226.md +340 -0
- package/knowledge/Furion_Teaching_Manual/24-/345/215/263/346/231/202/351/200/232/350/250/212SignalR.md +247 -0
- 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
- 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
- package/knowledge/Furion_Teaching_Manual/26-2-Cron/350/241/250/351/201/224/345/274/217.md +203 -0
- package/knowledge/Furion_Teaching_Manual/26-3-/344/273/273/345/213/231/344/275/207/345/210/227TaskQueue.md +215 -0
- package/knowledge/Furion_Teaching_Manual/27-/345/210/206/346/225/243/345/274/217ID/347/224/237/346/210/220.md +86 -0
- package/knowledge/Furion_Teaching_Manual/28-/346/250/241/347/265/204/345/214/226/351/226/213/347/231/274.md +68 -0
- package/knowledge/Furion_Teaching_Manual/29-/346/265/201/350/256/212/347/211/251/344/273/266Clay.md +313 -0
- 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
- 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
- package/knowledge/Furion_Teaching_Manual/33-IPC/347/250/213/345/272/217/351/200/232/350/250/212.md +98 -0
- package/knowledge/Furion_Teaching_Manual/34-2-Docker/351/203/250/347/275/262.md +313 -0
- package/knowledge/Furion_Teaching_Manual/34-3-Nginx/351/203/250/347/275/262.md +157 -0
- package/knowledge/Furion_Teaching_Manual/34-8-WindowsService/351/203/250/347/275/262.md +112 -0
- 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
- 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
- package/knowledge/Furion_Teaching_Manual/36-3-/345/237/272/346/272/226/346/270/254/350/251/246Benchmarking.md +80 -0
- package/knowledge/attributes.md +153 -0
- package/knowledge/config.md +147 -0
- package/knowledge/entity.md +115 -0
- package/knowledge/event.md +124 -0
- package/knowledge/plugin.md +136 -0
- package/knowledge/service.md +146 -0
- package/knowledge/sqlsugar.md +172 -0
- package/knowledge/vue-typescript.md +338 -0
- 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
|
+
```
|