# HttpAgent 文档全文 > 本文件由网站构建流程自动生成,包含全部文档与博客正文。索引见 https://http.furion.net/llms.txt --- # 1.1 HTTP 远程请求概述 > 原文链接:https://http.furion.net/docs/getting-started/intro/ `HTTP` 远程请求是指客户端(如 `Web` 浏览器、移动应用等)通过 `HTTP` 协议向远程服务器发送请求,以获取所需资源的过程。它是现代互联网应用中最基础且核心的通信方式之一。 ![httpagent](/images/httpagent.jpg) [**查看高清架构图**](https://gitee.com/dotnetchina/HttpAgent/blob/master/drawio/HttpAgent.drawio) ### 应用场景 `HTTP` 远程请求在互联网应用系统中应用广泛,涵盖以下主要场景: - **资源获取**:从服务器获取互联网资源,如网页、图片、视频等。 - **数据抓取**:用于网络爬虫抓取网页数据或进行数据分析。 - **文件传输**:支持文件的上传与下载操作。 - **API 对接**:与第三方 `API` 接口进行数据交互。 - **系统集成**:实现异构系统之间的互联互通。 - **配置管理**:用于配置中心的动态配置拉取与更新。 - **微服务通信**:支持微服务架构中的服务间调用。 - **负载均衡**:通过请求分发实现资源优化与高可用性。 - **压力测试**:用于模拟高并发请求,进行系统性能测试。 - **请求代理**:实现请求的代理与转发,支持跨域或安全访问。 - **其他场景**:适用于多种需要远程通信的场景。 `HTTP` 远程请求为互联网应用提供了高效、灵活的通信能力,是构建分布式系统和实现数据交互的重要技术基础。 --- # 1.2 安装与注册服务 > 原文链接:https://http.furion.net/docs/getting-started/installation/ > **安装包说明** `Furion` 框架已内置该功能,无需额外安装 `NuGet` 包。若使用非 `Furion` 框架,可通过以下命令安装 `HttpAgent` 或 `HttpAgent.AspNetCore` 包: - 适用于任何 `.NET/C#` 应用: ```bash showLineNumbers dotnet add package HttpAgent ``` - 适用于 `Web` 应用(包含 `HttpAgent` 且提供 `HttpContext` 转发功能): ```bash showLineNumbers dotnet add package HttpAgent.AspNetCore ``` 在发起 `HTTP` 远程请求前,需在 `Startup.cs` 或 `Program.cs` 文件中注册并配置 `HttpRemote` 服务。 ```cs showLineNumbers {2,5} // 在 Startup.cs 中注册: services.AddHttpRemote(); // 在 Program.cs 中,注册方式如下: // builder.Services.AddHttpRemote(); ``` > **解决 `AddHttpRemote` 二义性错误** 若遇到 `AddHttpRemote` 方法的二义性错误,可通过为其添加一个空的委托参数来解决,示例如下: ```cs showLineNumbers services.AddHttpRemote(builder => {}); ``` 随后,在您的服务、控制器或任何支持依赖注入的类中,注入 `IHttpRemoteService` 服务。 ```cs showLineNumbers {3,5} public class YourService { private readonly IHttpRemoteService _httpRemoteService; public YourService(IHttpRemoteService httpRemoteService) { _httpRemoteService = httpRemoteService; } } ``` 若您使用的是 `.NET 8` 及以上版本时,可通过[主构造函数](https://learn.microsoft.com/zh-cn/dotnet/csharp/whats-new/tutorials/primary-constructors)注入简化代码: ```cs showLineNumbers {1} public class YourService(IHttpRemoteService httpRemoteService) { // 使用 httpRemoteService 变量 } ``` 或者,您也可以在特定方法中按需注入: ```cs showLineNumbers {3} public class YourService { public Task GetResource([FromServices] IHttpRemoteService httpRemoteService) { // 您的代码逻辑 } } ``` > **在没有依赖注入的环境中的使用说明** 在 `.NET Core` 应用开发中,推荐使用依赖注入的方式来构建应用程序。因此,建议在条件允许的情况下,优先采用依赖注入方式管理服务。但在某些特定场景下,例如控制台应用(`Console`)、`WinForms` 或 `WPF` 项目中,`.NET` 并未默认集成完整的依赖注入容器。此时,您可以使用以下两种方式手动获取所需服务: - `Furion` 框架 如果项目使用了 `Furion` 框架,可在启动时将根容器注入 `HttpRemoteClient`。之后即可通过 `HttpRemoteClient.Service` 静态属性发起 `HTTP` 请求: ```cs showLineNumbers var app = builder.Build().UseHttpRemoteClient(); // 将根容器注入 HttpRemoteClient ``` ```cs showLineNumbers var result = await HttpRemoteClient.Service.GetAsStringAsync("https://furion.net/"); ``` 也可以直接解析 `HTTP` 远程请求服务并发送请求: ```cs showLineNumbers var httpRemoteService = App.GetRequiredService(); ``` - 其他项目(`Console/WinForms/WPF`) 对于一般的 `Console/WinForms/WPF` 项目,可以使用 `HttpRemoteClient` 静态类提供的 `Service` 属性发起远程 `HTTP` 请求: ```cs showLineNumbers var result = await HttpRemoteClient.Service.GetAsStringAsync("https://furion.net/"); ``` **使用建议:上述两种方式应作为依赖注入机制的补充手段使用,而非替代方案。在支持依赖注入的环境中,请尽量遵循标准的依赖注入实践来构建和管理应用程序服务。** > 有关 `HttpRemoteClient` 的详细配置、自定义服务注册以及如何将外部依赖注入容器与静态类集成,请参阅**8.16 在非依赖注入环境中使用(`Console/WinForms/WPF`)**。 --- # 1.3 在 Claude Code / Codex 中使用(llms.txt) > 原文链接:https://http.furion.net/docs/getting-started/ai-assistants/ 本网站在线提供 `llms.txt` 与 `llms-full.txt` 两份 AI 友好文件,让 AI 助手(如 `Claude Code`、`Codex`)无需逐页抓取网页,即可一次性读取全部文档。 - `llms.txt`:全站文档索引(标题 + 链接 + 一句话简介); - `llms-full.txt`:全站文档正文合集(单文件); - 英文版:`https://http.furion.net/en/llms.txt` 与 `https://http.furion.net/en/llms-full.txt`。 > **为什么推荐 `llms-full.txt`** `llms-full.txt` 包含全部正文,AI 一次读取即可掌握整个框架,无需逐页抓取。 **1. 在 `Claude Code` 中使用** 方式一:直接让 `Claude` 在线读取: ```bash showLineNumbers # 在 Claude Code 会话中直接输入: # 请先阅读 https://http.furion.net/llms.txt,再回答 HttpAgent 相关问题 ``` 方式二:下载到本地并写入 `CLAUDE.md`,离线可用: ```bash showLineNumbers curl https://http.furion.net/llms-full.txt -o .claude/httpagent-docs.txt ``` 在项目根目录的 `CLAUDE.md` 中添加: ```md showLineNumbers # HttpAgent 使用说明 - 所有 HttpAgent 相关问题,先阅读 .claude/httpagent-docs.txt 再作答。 ``` **2. 在 `Codex` 中使用** 下载文档并在 `AGENTS.md` 中引用: ```bash showLineNumbers curl https://http.furion.net/llms-full.txt -o docs/httpagent-llms-full.txt ``` 在 `AGENTS.md` 中添加: ```md showLineNumbers # HttpAgent - 编写 HttpAgent 相关代码前,先阅读 docs/httpagent-llms-full.txt。 - 官方文档索引:https://http.furion.net/llms.txt ``` > **提示** - 中文环境使用 `https://http.furion.net/llms.txt`;英文环境使用 `https://http.furion.net/en/llms.txt`; - 文档更新后重新执行 `curl` 命令即可刷新本地副本。 --- # 1.4 与其他 HTTP 客户端库对比 > 原文链接:https://http.furion.net/docs/getting-started/comparison/ `HttpAgent`、[`Refit`](https://github.com/reactiveui/refit) 和 [`RestSharp`](https://github.com/restsharp/RestSharp) 都是 `.NET` 生态中广受欢迎的 `HTTP` 客户端解决方案。三者定位不同:`Refit` 主打「接口即 `API`」的声明式编程,`RestSharp` 是经典老牌的流畅式客户端,而 `HttpAgent` 则是**全功能型**选手——除了常规请求能力外,还内置了 `SSE`、`WebSocket`、长轮询、压力测试与 `Profiler` 流量分析等「生产级」能力,且**零第三方依赖**。 > **先说结论** 三个库都是优秀的开源项目,各有擅长的场景,没有绝对的好坏。本页对比基于三者的公开文档与典型用法整理,力求客观公正:它们都基于 `HttpClient` 底层实现,常规 `REST` 请求三者都能出色胜任,差异主要体现在「调试体验」与「能力边界」上。 ### 典型用法对比 同样一句「获取用户列表」,三个库的写法如下: **`HttpAgent`:构建器 / 谓词式,两种风格任选** ```cs showLineNumbers // 方式一:流畅式构建器 var users = await httpRemoteService.SendAsAsync>( HttpRequestBuilder.Get("https://api.example.com/users")); // 方式二:请求谓词(语法糖) var users = await httpRemoteService.GetAsAsync>("https://api.example.com/users"); ``` **`Refit`:接口即 `API`** ```cs showLineNumbers public interface IUsersApi { [Get("/users")] Task> GetUsersAsync(); } var api = RestService.For("https://api.example.com"); var users = await api.GetUsersAsync(); ``` **`RestSharp`:经典 `RestClient` + `RestRequest`** ```cs showLineNumbers var client = new RestClient("https://api.example.com"); var request = new RestRequest("/users"); var users = await client.GetAsync>(request); ``` 可以看到:`Refit` 需要先定义接口契约,`RestSharp` 与 `HttpAgent` 都是即写即用的流畅风格,而 `HttpAgent` 同时支持两者。 ### 常用 `REST` 能力对比 | 能力 | `HttpAgent` | `Refit` | `RestSharp` | | --- | --- | --- | --- | | 标准请求动词(`GET`/`POST`/`PUT`/`DELETE` 等) | ✅ 9 种动词 | ✅ 接口特性声明 | ✅ `RestRequest` | | 流畅式构建器 | ✅ `HttpRequestBuilder` | ❌(接口式,无构建器) | ✅ `RestClient` + `RestRequest` | | 接口式声明请求(代理) | ✅ 声明式代理 | ✅(核心设计) | ❌ | | 查询参数 / 请求标头 / 请求正文 | ✅ | ✅ | ✅ | | `JSON`/`XML` 等序列化 | ✅ 内置内容处理器 | ✅ `System.Text.Json`(可替换) | ✅ 可插拔序列化器 | | 多部分表单上传 | ✅ `MultipartFile.CreateFromPath` | ✅ `StreamPart` 等 | ✅ `.AddFile(...)` | | 文件上传 / 下载(含进度) | ✅ 内置进度打印 | ✅ 流式响应 | ✅ `DownloadDataAsync` 等 | | 身份认证 | ✅ `Bearer`/`Basic`/`Digest`/`Access Token` 自动管理 + `JWT` 工具 | ✅ `[Authorize]` 自定义处理器 | ✅ `OAuth1/OAuth2/JWT/NTLM` 内置认证器 | | 重试策略 | ✅ 内置重试 + 配额策略 | ❌(需自行配合 `Polly` 等) | ✅ 官方示例配合 `Polly` | | 超时设置 | ✅ 内置(设置超时时间) | ✅ 通过 `HttpClientHandler` 配置 | ✅ `client.Timeout` | | 重定向处理 | ✅ 内置(可配置委托) | ✅ 客户端默认跟随 | ✅ 可配置跟随 | | `HTTP` 代理 | ✅ 内置配置 | ✅ 通过 `HttpMessageHandler` | ✅ 通过 `HttpMessageHandler` | | 响应自动解压(`gzip`/`deflate`/`brotli`/`zstd`) | ✅ 内置 | ✅ 通过 `HttpMessageHandler` | ✅ 通过 `HttpMessageHandler` | | `Cookie` 管理 | ✅ 内置 | ❌(需自行处理) | ✅ 内置 | | 异常处理 | ✅ 统一异常 + 异常抑制 | ✅ `ApiException` | ✅ 内置异常 | | 取消令牌(`CancellationToken`) | ✅ | ✅ | ✅ | ### `HttpAgent` 的特色能力(两者暂未内置) | 能力 | `HttpAgent` | `Refit` | `RestSharp` | | --- | --- | --- | --- | | `SSE`(`Server-Sent Events`,含 `IAsyncEnumerable`) | ✅ 内置 | ❌ | ❌ | | `WebSocket` 双工通信 | ✅ 内置 | ❌ | ❌ | | 轮询机制(标准轮询 + 长轮询,间隔可配置) | ✅ 内置 | ❌ | ❌ | | `Profiler` 流量分析引擎(请求/响应捕获与可视化) | ✅ 工业级内置 | ❌ | ❌ | | 压力 / 性能 / 模拟测试(并发数与轮次可配、生成报告) | ✅ 内置 | ❌ | ❌ | | 请求断言(发送前 / 响应后校验) | ✅ `Asserts(...)` | ❌ | ❌ | | 内容处理器 / 转换器双管道(可插拔扩展) | ✅ 内置 | ❌ | ❌ | | `MessagePack` 序列化 | ✅ 内置 | ❌ | ❌ | | `WebService` / `SOAP` 支持(含 `SOAPAction`) | ✅ 内置 | ❌ | ❌ | | `OData` 内置客户端(`$filter`/`$select`/`$expand`) | ✅ 内置 | ❌ | ❌ | | 请求代理与转发(微服务集成) | ✅ 内置 | ❌ | ❌ | | 请求日志审计(自定义 `Logger`) | ✅ 内置 | ❌ | ❌ | | 官网「工坊」可视化代码生成器 | ✅(builder/谓词/`cURL`/声明式/`JSON` 五种代码一键生成) | ❌ | ❌ | | 官网「HttpAgent 助手」AI 问答 | ✅(基于官方文档实时解答,选中任意文字即可「问 AI」,`Key` 仅存本地) | ❌ | ❌ | | `cURL` 命令导入 | ✅ 内置(官网工坊可视化生成) | ❌ | ❌ | | `JSON` 配置导入 | ✅ 内置(`HttpRequestBuilder.FromJson()`) | ❌ | ❌ | | `ETag` 缓存 / 请求配额 | ✅ 内置 | ❌ | ❌ | | `HttpContext` 转发 | ✅ 内置 | ❌ | ❌ | | `MCP` 消息内容 | ✅ 内置 | ❌ | ❌ | | `Mock` 模拟测试 | ✅ 内置 | ❌ | ❌ | | 第三方依赖 | 零依赖 | `System.Text.Json` 等 | 序列化器等 | ### 什么场景选谁 - **只对接少量 `REST` 接口、偏好接口契约风格** → `Refit` 非常合适; - **历史项目、追求生态成熟与海量社区资料** → `RestSharp` 稳妥可靠; - **需要「请求 + 调试 + 压测 + 实时通信」全家桶、不想拼装多个库** → `HttpAgent` 是理想选择:零依赖、内置 `Profiler`,`SSE`/`WebSocket`/长轮询/压力测试/声明式请求全部开箱即用;官网还配套 [【工坊】可视化代码生成器](/workshop/) 与「HttpAgent 助手」AI 问答两大工具,学习与排错成本更低。 > **官网两大配套工具** 除了库本身的能力外,官网还提供两个 `Refit`、`RestSharp` 都不具备的配套工具: - [**【工坊】**](/workshop/):可视化代码生成器——在线配置请求即可一键生成 builder、请求谓词、`cURL`、声明式接口与 `JSON` 五种代码,新手也能快速上手; - **【HttpAgent 助手】**:站点右下角的 AI 问答助手,答案完全基于官方文档(可开启全文增强模式),全站选中任意文字即可就地「问 AI」追问;部署自己的 `DeepSeek` `API Key` 即可实时提问,`Key` 仅保存在本地浏览器。 > **客观提醒** `Refit` 与 `RestSharp` 同样在持续迭代,选择前建议结合官方文档确认最新能力;如果项目已经深度使用其中一个库且没有遇到瓶颈,也没有必要强行迁移。工具是为效率服务的,合适的才是最好的。 --- # 1.5 关于作者 > 原文链接:https://http.furion.net/docs/getting-started/about-author/ > **孤芳自赏** 纵你阅人何其多,再无一人恰似我。 ### 网名 - **百小僧** - MonkSoul - ~~新生帝~~ ### 专属头像 ![furionlogo](/images/furionlogo.png) 该头像由我于 2016 年 7 月 14 日亲自设计。 ### 个性签名 > 2012.06.29 > 成在经营,败在管理,错在不学习。 > > 2024.07.08 更迭为: > 想,只有困难;做,才有答案。 > > 2025.02.17 更迭为: > **一梦十年,十年一梦。方知我是我。** ### 奉献宣言 无私奉献不是天方夜谭,有时候,我们也可以做到。 ### 开源初衷 开源如同人的脸,好坏一面便知,缺点可能会受到嘲讽批评,优点也会收获赞扬尊重。别担心,他们正在塑造更好的你。 ### 兴趣爱好 对新技术满怀好奇,钟情于开源事业,乐于分享技术心得;着迷纹身文化,热衷科技产品,闲暇时把玩掌机,偶尔在《穿越火线》与《我的世界》里放松心情。常驻足于开源中国、博客园、知乎、IT 之家、GitHub 和 Gitee,也刷抖音、逛 B 站、看动漫、追美剧。 ### 个人主页 - Gitee:[https://gitee.com/monksoul](https://gitee.com/monksoul) - Github:[https://github.com/monksoul](https://github.com/monksoul) ### 技术能力 自 2008 年与编程结缘,十余载光阴略通互联网主流技术皮毛,最熟悉的语言是 `C#` 和 `JavaScript`。 ### 常用工具 `Visual Studio 2026`、`JetBrains Rider`、`Vim/NeoVim`、`Visual Studio Code`、`PostgreSQL`。 ### 领域兴趣 对软件工程、架构设计、底层原理、算法、嵌入式/单片机、网络编程等始终抱有热情。 --- # 2.1 获取网站内容 > 原文链接:https://http.furion.net/docs/quick-start/get-content/ 获取网站内容是一个常见的需求,例如获取 `Furion` 框架网站(`https://furion.net`)的首页内容。以下展示了多种使用 `httpRemoteService` 来实现这一需求的方法。 ```cs showLineNumbers // 直接获取字符串内容 var content = await httpRemoteService.GetAsStringAsync("https://furion.net"); ``` 除了上述方法外,还支持以下多种方式: **1. 使用构建器方式 ✅** - 直接获取字符串类型内容: ```cs showLineNumbers var content = await httpRemoteService.SendAsStringAsync(HttpRequestBuilder.Get("https://furion.net")); // var content = await httpRemoteService.SendAsStringAsync(HttpBuilder.Get("https://furion.net")); // 可使用 HttpBuilder 替代 HttpRequestBuilder ``` - 通过泛型指定字符串类型: ```cs showLineNumbers var content = await httpRemoteService.SendAsAsync(HttpRequestBuilder.Get("https://furion.net")); ``` - 获取 `HttpRemoteResult` 类型,并从中提取结果: ```cs showLineNumbers {1} var result = await httpRemoteService.SendAsync(HttpRequestBuilder.Get("https://furion.net")); var content = result.Result; ``` - 获取 `HttpResponseMessage` 类型,并读取其内容: ```cs showLineNumbers {1} var httpResponseMessage = await httpRemoteService.SendAsync(HttpRequestBuilder.Get("https://furion.net")); var content = await httpResponseMessage.Content.ReadAsStringAsync(); ``` **2. 使用请求谓词方式** - 通过泛型指定字符串类型并直接获取: ```cs showLineNumbers {1,4,7} var content = await httpRemoteService.GetAsAsync("https://furion.net"); // 配置 HttpRequestBuilder // var content = await httpRemoteService.GetAsAsync("https://furion.net", builder => builder.Profiler()); // ✅ 语法糖:可使用 HttpRequestBuilder.Setup 或 HttpBuilder.Setup 替代 builder => builder 写法 // var content = await httpRemoteService.GetAsAsync("https://furion.net", HttpBuilder.Setup.Profiler()); ``` - 获取 `HttpRemoteResult` 类型,并从中提取结果: ```cs showLineNumbers {1} var result = await httpRemoteService.GetAsync("https://furion.net"); var content = result.Result; ``` - 获取 `HttpResponseMessage` 类型,并读取其内容: ```cs showLineNumbers {1} var httpResponseMessage = await httpRemoteService.GetAsync("https://furion.net"); var content = await httpResponseMessage.Content.ReadAsStringAsync(); ``` 这些方式提供了灵活的选择,可以根据具体需求选择最适合的方法来获取网站内容。 --- # 2.2 携带请求数据 > 原文链接:https://http.furion.net/docs/quick-start/with-data/ 在获取第三方 `API` 数据时,通常需要携带请求数据,这些数据可以是 `URL` 地址参数或请求内容。最常见的做法是通过 `URL` 地址传递参数,以及发送 `JSON` 格式的数据。 ```cs showLineNumbers {3-4} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddModel", builder => builder .WithQueryParameters(new { query1 = 1, query2 = "furion" }) // 设置 URL 查询参数 .SetJsonContent(new { id = 1, name = "furion" })); // 设置请求的 JSON 内容 ``` 除了上述方式,还支持以下几种方法: ```cs showLineNumbers {3-5} // 使用构建器模式 var content = await httpRemoteService.SendAsAsync(HttpRequestBuilder.Post("https://localhost:7044/HttpRemote/AddModel") .WithQueryParameter("query1", 1) // 设置查询参数(支持单个设置) .WithQueryParameter("query2", "furion") // 设置查询参数(支持单个设置) .SetJsonContent("{\"id\":1,\"name\":\"furion\"}")); // 设置请求内容(支持直接传入 JSON 字符串) // 更多详细用法可参考第 2.1 节 ``` 此外,您还可以使用 `SetContent` 方法来设置请求内容,该方法支持设置任意类型的请求内容。事实上,`SetJsonContent` 方法内部也是通过调用 `SetContent` 来实现的。 ```cs showLineNumbers {5,11,17} // 自定义 Content-Type var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddModel", builder => builder .WithQueryParameters(new { query1 = 1, query2 = "furion" }) // 设置查询参数 .SetContent(new { id = 1, name = "furion" }, "application/json")); // 设置请求内容 // 自定义 Content-Type 支持配置 Charset var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddModel", builder => builder .WithQueryParameters(new { query1 = 1, query2 = "furion" }) // 设置查询参数 .SetContent(new { id = 1, name = "furion" }, "application/json;charset=utf-8")); // 设置请求内容 // 自定义 Content-Type 支持配置请求编码 var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddModel", builder => builder .WithQueryParameters(new { query1 = 1, query2 = "furion" }) // 设置查询参数 .SetContent(new { id = 1, name = "furion" }, "application/json;charset=utf-8", Encoding.UTF8)); // 设置请求内容 ``` --- # 2.3 Form 表单提交(URL 编码) > 原文链接:https://http.furion.net/docs/quick-start/form-urlencoded/ 在互联网应用中,保存用户自定义的数据最常见的方式是使用 `Form` 表单提交。`Form` 表单不仅能携带文本数据,还能携带二进制数据,如文件。 ```cs showLineNumbers {2} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddForm?id=1", builder => builder.SetMultipartContent(multipart => multipart // 设置多部分表单内容 .AddJson(new { id = 1, name = "furion" }) // 设置 JSON 数据 .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"))); // 设置文件(支持流方式、字节数组方式、远程 URL 地址和 Base64 字符串) ``` **`SetMultipartContent` 方法专门用于设置请求类型为 `multipart/form-data` 的表单数据**,并提供了丰富的扩展选项,包括: - `Boundary` 或 `SetBoundary(boundary)`:设置多部分表单内容边界。 - `AddJson(rawJson)`:添加 `JSON` 内容。 - `AddFormItem(value, name)`:添加单个表单项内容。 - `AddHtml(htmlString, name)`:添加 `HTML` 内容。 - `AddXml(xmlString, name)`:添加 `XML` 内容。 - `AddText(text, name)`:添加文本内容。 - `AddObject(rawObject, name)`:添加对象内容。 - `AddFileFromRemote(url, name)`:添加互联网文件内容。 - `AddFileFromBase64String(base64String, name, fileName)`:添加 `Base64` 字符串文件内容。 - `AddFileAsStream(path, name)`:添加本地文件作为流内容。 - `AddFileWithProgressAsStream(path, channel, name)`:添加本地文件作为流内容(带文件传输进度)。 - `AddFileAsByteArray(path, name)`:添加本地文件作为字节数组内容。 - `AddFile(multipartFile, name)`:添加 `MultipartFile` 文件内容。 - `AddFile(fileInfo, name)`:添加 `FileInfo` 文件内容。 - `AddFile(IFormFile)`:添加 `IFormFile` 文件内容。 - `AddFiles(IFormFileCollection)`:添加 `IFormFileCollection` 文件内容。 - `AddFile(IBrowserFile)`:添加 `IBrowserFile` 文件内容。 - `AddFiles(IEnumerable)`:添加多个 `IBrowserFile` 文件内容。 - `AddStream(stream, name)`:添加二进制流内容。 - `AddByteArray(byteArray, name)`:添加二进制字节数组内容。 - `Add(httpContent)`:添加 `HttpContent` 内容。 这些只是常用的设置方法,`SetMultipartContent` 提供了更多的灵活性。 除了上述方式,还支持以下几种方法: ```cs showLineNumbers {3-5} // 使用构建器模式 var content = await httpRemoteService.SendAsAsync(HttpRequestBuilder.Post("https://localhost:7044/HttpRemote/AddForm?id=1") .SetMultipartContent(multipart => multipart // 设置多部分表单内容 .AddJson(new { id = 1, name = "furion" }) // 设置 JSON 数据 .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"))); // 设置文件(支持流方式、字节数组方式、远程 URL 地址和 Base64 字符串 // 更多详细用法可参考第 2.1 节 ``` 以下是一些 `Form` 表单提交的常见例子: ```cs showLineNumbers {4,5,7,9,11} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddForm?id=1", builder => builder.SetMultipartContent(multipart => multipart // 设置多部分表单内容 .AddJson(new { id = 1, name = "furion" }) // 设置 JSON 数据 .AddFormItem("age", "Age") // 支持设置单个值 .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file") // 设置单个文件(对应表单 File 字段) // 支持互联网文件地址 .AddFileFromRemote("https://furion.net/img/furionlogo.png", "files") // 设置多个文件(对应表单 Files 字段) // 支持读取本地文件作为字节数组 .AddFileAsByteArray(@"C:\Workspaces\httptest.jpg", "files")); // 设置多个文件(对应表单 Files 字段) // 添加 MultipartFile 文件 .AddFile(MultipartFile.CreateFromPath(@"C:\Workspaces\httptest.jpg"))); ``` > **特别说明** 如果使用 `SetContent` 方法来设置请求类型为 `multipart/form-data` 的内容且不是 `MultipartContent` 类型实例时,将会触发 `NotSupportedException` 异常。异常信息提示如下: ```cs showLineNumbers The method does not support setting the request content type to `multipart/form-data`. Please use the `SetMultipartContent` method instead. If you are using an HTTP declarative requests, define the parameter with the `Action` type or annotate the parameter with the `MultipartAttribute`. ``` **在需要设置请求内容类型为 `multipart/form-data` 类型时,应正确使用 `SetMultipartContent` 方法,而非 `SetContent`。** - **`URL` 编码表单** 除了包含多个部分的 `multipart/form-data` 表单请求外,还有一种常见的请求类型是 `application/x-www-form-urlencoded`,它以 `URL` 编码形式发送数据。这种表单的特点是,所有特殊字符都会进行 `URL` 编码,通常用于无需上传文件等二进制数据的简单表单提交场景。 以下示例将展示如何构建符合 `application/x-www-form-urlencoded` 提交类型的表单数据。 ```cs showLineNumbers {3,8} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddURLForm", builder => builder .SetFormUrlEncodedContent(new { id = 1, name = "furion" })); // 设置 application/x-www-form-urlencoded 请求内容 // 支持 URL 编码字符串格式 var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddURLForm", builder => builder .SetFormUrlEncodedContent("id=1&name=furion", useStringContent: true); ``` > **`URL` 编码表单内容说明** - **默认情况下,`URL` 编码表单通过 [`FormUrlEncodedContent`](https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Net.Http/src/System/Net/Http/FormUrlEncodedContent.cs#L44) 类型进行构建,但此类型不支持自定义请求内容编码,它默认使用 `Encoding.Latin1` 而不是 `UTF-8`。** 这可能在提交到某些接口时引发异常。 为解决此问题,可以通过设置参数 `useStringContent` 为 `true` 来采用 `StringContent` 方式构建表单数据,从而允许自定义编码为 `UTF-8`。 ```cs showLineNumbers {3} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddURLForm", builder => builder .SetFormUrlEncodedContent(new { id = 1, name = "furion" }, useStringContent: true)); ``` - 某些服务器要求显式声明字符集(`charset`),此时可通过 `contentEncoding` 参数指定编码方式,例如使用 `UTF-8`: ```cs showLineNumbers {3} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddURLForm", builder => builder .SetFormUrlEncodedContent(new { id = 1, name = "furion" }, Encoding.UTF8)); ``` 此设置在发送远程请求时会生成如下 `Content-Type` 请求头:`application/x-www-form-urlencoded; charset=UTF-8`。 --- # 2.4 下载网络资源 > 原文链接:https://http.furion.net/docs/quick-start/download/ `HTTP` 远程请求最常见的应用场景之一是下载网络资源并将其保存到本地磁盘,这包括下载网页内容、图片、压缩包以及安装软件等。以下示例展示了如何下载 `ASP.NET Core` 运行时: ```cs showLineNumbers {3} // 从指定 URL 下载 ASP.NET Core 运行时,并保存到 C:\Workspaces\ 目录中 // 如果未指定文件名,框架将自动从下载地址中解析出文件名,例如:aspnetcore-runtime-8.0.10-win-x64.exe var fileTransferResult = await httpRemoteService.DownloadFileAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\"); // 如需指定文件名可设置为 C:\Workspaces\aspnetcore-runtime.exe ``` > **下载文件保存路径说明** - 如果未指定下载文件的名称,框架将自动从下载地址中解析出文件名。 - 如果提供了自定义文件名,则该名称将被用于保存最终下载的文件。 - 此外,如果您仅提供了一个目标文件夹(目录)用于存放下载文件,请确保该文件夹(目录)路径以斜杠(`/`)结尾。 文件下载完成后,框架将返回一个 `FileTransferResult` 对象,包含以下属性: - `IsSuccess`:传输是否成功完成(`bool` 类型)。**注意:因文件存在而跳过也被视为成功。** - `RequestUri`:文件传输地址(`string` 类型)。文件下载时,为下载地址;文件上传时,为上传地址。 - `FilePath`:文件的路径(`string` 类型)。 - `FileSize`:文件的大小(以字节为单位的 `long` 类型)。 - `ElapsedMilliseconds`:传输耗时(以毫秒为单位的 `long` 类型)。 - `StatusCode`:响应状态(`HttpStatusCode` 类型)。 若本地文件已存在,将会抛出 `InvalidOperationException` 异常,`System.InvalidOperationException: The destination path 'C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe' already exists.`。此时,您可以通过 `fileExistsBehavior` 参数来指定文件存在时的行为: ```cs showLineNumbers {3} var fileTransferResult = await httpRemoteService.DownloadFileAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\" , fileExistsBehavior: FileExistsBehavior.Overwrite); // 若文件存在时则覆盖 ``` `FileExistsBehavior` 枚举包含以下选项: - `CreateNew`(默认值):若文件已存在,则抛出异常;否则,创建新文件。 - `Overwrite`:覆盖现有文件。 - `Skip`:保留现有文件,并跳过下载操作。 在下载文件时,您还可以获取实时的下载进度。以下示例展示了如何打印下载进度: ```cs showLineNumbers {3-6} var fileTransferResult = await httpRemoteService.DownloadFileAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\" , async progress => { Console.WriteLine(await progress.ToSummaryStringAsync()); // 输出简要进度字符串 } , fileExistsBehavior: FileExistsBehavior.Overwrite); ``` 下载进度的控制台输出示例(使用 `progress.ToSummaryString()`): ```bash showLineNumbers Transferred 0.26MB of 10.09MB (2.63% complete, Speed: 3.86MB/s, Time: 0.07s, ETA: 2.55s). File: aspnetcore-runtime-8.0.10-win-x64.exe, Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe. Transferred 10.09MB of 10.09MB (100.00% complete, Speed: 9.99MB/s, Time: 1.01s, ETA: 0.00s). Done! File: aspnetcore-runtime-8.0.10-win-x64.exe, Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe. ``` 若需在控制台中实时显示文件下载进度,推荐使用 `UpdateConsoleProgressAsync()` 方法。示例如下: ```cs showLineNumbers {3,7} var fileTransferResult = await httpRemoteService.DownloadFileAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\" , progress => progress.UpdateConsoleProgressAsync() // 在控制台中更新文件传输进度条 , fileExistsBehavior: FileExistsBehavior.Overwrite); // ✅ 或使用 DownloadFileWithConsoleProgressAsync 方法(带控制台进度打印) var fileTransferResult = await httpRemoteService.DownloadFileWithConsoleProgressAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\" , fileExistsBehavior: FileExistsBehavior.Overwrite); ``` 执行后,控制台将显示如下进度信息: ```bash showLineNumbers {2} File: aspnetcore-runtime-8.0.10-win-x64.exe, Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe [############################## ] 61.35% (6.19MB/10.09MB) Speed: 5.81MB/s, Time: 1.07s, ETA: 0.67s. ``` 若使用 `progress.ToString()`,则控制台输出将包含更详细的进度信息: ```bash showLineNumbers Transfer Progress: File Name: aspnetcore-runtime-8.0.10-win-x64.exe File Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe File Size: 10.09MB Transferred: 0.12MB Percentage Complete: 1.23% Transfer Rate: 2.20MB/s Time Elapsed (s): 0.06 Estimated Time Remaining (s): 4.52 Transfer Progress: File Name: aspnetcore-runtime-8.0.10-win-x64.exe File Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe File Size: 10.09MB Transferred: 10.09MB Percentage Complete: 100.00% Transfer Rate: 9.77MB/s Time Elapsed (s): 1.03 Estimated Time Remaining (s): 0.00 ``` `progress` 参数的类型为 `FileTransferProgress`,包含以下属性和方法: - **属性**: - `FilePath`:文件的路径(`string` 类型)。 - `FileName`:文件的名称(`string` 类型)。 - `FileSize`:文件的大小(以字节为单位的 `long` 类型)。 - `Transferred`:已传输的数据量(以字节为单位的 `long` 类型)。 - `PercentageComplete`:已完成的传输百分比(`double` 类型)。 - `TransferRate`:当前的传输速率(以字节/秒为单位的 `double` 类型)。 - `TimeElapsed`:从开始传输到现在的持续时间(`TimeSpan` 类型)。 - `EstimatedTimeRemaining`:预估的剩余传输时间(`TimeSpan` 类型)。 - **方法**: - `ToString()`:输出带缩进的详细进度字符串。 - `ToStringAsync()`:输出带缩进的详细进度字符串。 - `ToSummaryString()`:输出简要的进度字符串。 - `ToSummaryStringAsync()`:输出简要的进度字符串。 - `UpdateConsoleProgress()`:在控制台中更新(打印)文件传输进度条。 - `UpdateConsoleProgressAsync()`:在控制台中更新(打印)文件传输进度条。 ### 并行下载多个文件 框架原生支持并行下载多个文件。借助 `ParallelUtility.ForEachAsync` 工具方法,可以轻松实现并发下载,并自动启用**多行进度条模式**——每个文件独占两行(文件头和进度条),所有进度条同屏实时刷新,互不干扰: ```cs showLineNumbers {10,12} var urls = new[] { "https://img-s.msn.cn/tenant/amp/entityid/AA296jTM.img?w=640&h=1068&m=6", "https://img-s.msn.cn/tenant/amp/entityid/AA297bnQ.img?w=640&h=1240&m=6&x=236&y=233&s=64&d=64", "https://img-s.msn.cn/tenant/amp/entityid/AA296Rr4.img?w=640&h=821&m=6" }; const string saveDir = @"C:\Workspaces\"; // 并行下载,默认最大并发数为 4 await ParallelUtility.ForEachAsync(urls, async (url, token) => { await _httpRemoteService.DownloadFileWithConsoleProgressAsync(url, saveDir, FileExistsBehavior.Overwrite, cancellationToken: token); }); ``` 执行后,控制台将同时显示所有文件的下载进度,每个文件的进度条独立刷新: ```bash showLineNumbers File: AA296jTM.img, Path: C:\Workspaces\AA296jTM.img [########............] 40.12% (0.05MB/0.12MB) Speed: 1.20MB/s, Time: 42ms, ETA: 58ms. File: AA297bnQ.img, Path: C:\Workspaces\AA297bnQ.img [######..............] 30.05% (0.04MB/0.12MB) Speed: 0.95MB/s, Time: 38ms, ETA: 84ms. File: AA296Rr4.img, Path: C:\Workspaces\AA296Rr4.img [##########..........] 50.33% (0.06MB/0.12MB) Speed: 1.55MB/s, Time: 45ms, ETA: 39ms. ``` 所有文件下载完成后,进度条将逐一显示 `Done!` 状态: ```bash showLineNumbers File: AA296jTM.img, Path: C:\Workspaces\AA296jTM.img [####################] 100.00% (0.12MB/0.12MB) Speed: 2.61MB/s, Time: 167ms. Done! File: AA297bnQ.img, Path: C:\Workspaces\AA297bnQ.img [####################] 100.00% (0.12MB/0.12MB) Speed: 1.81MB/s, Time: 203ms. Done! File: AA296Rr4.img, Path: C:\Workspaces\AA296Rr4.img [####################] 100.00% (0.12MB/0.12MB) Speed: 1.61MB/s, Time: 204ms. Done! ``` 您还可以通过 `maxDegreeOfParallelism` 参数控制最大并发数: ```cs showLineNumbers {5} await ParallelUtility.ForEachAsync(urls, async (url, token) => { await _httpRemoteService.DownloadFileWithConsoleProgressAsync(url, saveDir, FileExistsBehavior.Overwrite, cancellationToken: token); }, maxDegreeOfParallelism: 2); // 最多同时下载 2 个文件 ``` 除了上述方式,还支持以下几种方法下载网络资源: ```cs showLineNumbers {2} // 使用构建器模式 var fileTransferResult = await httpRemoteService.SendAsync(HttpRequestBuilder.DownloadFile("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\" , fileExistsBehavior: FileExistsBehavior.Overwrite)); // 更多详细用法可参考第 2.1 节 ``` --- # 2.5 上传文件资源(OSS) > 原文链接:https://http.furion.net/docs/quick-start/upload/ 在互联网应用中,用户上传文件是一项常见需求,涵盖设置头像、发布图文动态、上传相册至网盘、分享 `Vlog` 到视频社区等场景。以下展示了多种文件上传的实现方式。 **1. 使用 `Form` 表单方式上传** 通过 `Form` 表单方式上传文件的方式与第 2.3 章节中描述的 `Form` 表单提交方法一致。 ```cs showLineNumbers {2-3} await httpRemoteService.PostAsync("https://localhost:7044/HttpRemote/AddFile", builder => builder .SetMultipartContent(multipart => multipart .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"))); ``` 若需上传多个文件,只需在 `multipart` 中继续添加(需保持表单名一致,如 `files`): ```cs showLineNumbers {3-4} await httpRemoteService.PostAsync("https://localhost:7044/HttpRemote/AddFiles", builder => builder .SetMultipartContent(multipart => multipart .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "files") .AddFileFromRemote("https://furion.net/img/furionlogo.png", "files"))); ``` 此外,还支持使用构建器模式,以及获取上传文件的返回值。更多详情可参考第 2.1 节。 ```cs showLineNumbers {2} // 使用构建器模式 await httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://localhost:7044/HttpRemote/AddFile") .SetMultipartContent(multipart => multipart .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"))); // 更多详细用法可参考第 2.1 节 ``` **2. 使用非 `Form` 表单方式上传(`OSS`)** 在与某些 `OSS`(对象存储服务)或云网盘进行对接时,通常会遇到不支持传统 `Form` 表单方式上传文件的情况。此时,需要直接以文件字节数组或 `Stream` 流的形式上传文件。以下是具体实现示例: ```cs showLineNumbers {1,4} var fileStream = File.OpenRead("文件路径"); // 或使用:var fileBytes = File.ReadAllBytes("文件路径"); await httpRemoteService.PutAsync("https://localhost:7044/HttpRemote/AddFile", builder => builder .SetContent(fileStream)); // 或使用 .SetContent(fileBytes); ``` 在某些特殊场景下,可能需要显式移除 `Content-Type` 请求头(即将其设置为空)。此时,可通过调用 `SetOmitContentType(true)` 方法实现,示例如下: ```cs showLineNumbers {3} await httpRemoteService.PutAsync("https://localhost:7044/HttpRemote/AddFile", builder => builder .SetContent(fileStream) // 或使用 .SetContent(fileBytes); .SetOmitContentType(true); // .AutoSetHostHeader()); // 某些服务器可能会强制验证 Host 请求头(可选) ``` > **将 `IFormFile` 实例转换为 `Stream` 并上传** 在 Web 应用中,我们通常使用 `IFormFile` 类型来接收用户上传的文件。如果需要将该文件进一步上传至 `OSS`(对象存储服务)或云网盘,可通过以下步骤将其转换为 `Stream` 并完成上传: ```cs showLineNumbers {2} await httpRemoteService.PutAsync("https://localhost:7044/HttpRemote/AddFile", builder => builder .SetContent(formFile.OpenReadStream(), disposeResourcesOnRequestCompletion: true)); ``` **3. 使用 `UploadFile` 扩展方法(表单方式)** 在视频分享等应用中,用户上传文件时通常需要查看实时进度。为此,可使用 `UploadFile` 扩展方法,该方法支持实时进度获取,并允许对文件类型和大小进行限制。 以下示例展示了如何打印上传进度: ```cs showLineNumbers {2-5} await httpRemoteService.UploadFileAsync("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file" , async progress => { Console.WriteLine(await progress.ToSummaryStringAsync()); // 输出简要进度信息 }); ``` 控制台输出示例: ```bash showLineNumbers Transferred 0.01MB of 0.01MB (100.00% complete, Speed: 0.86MB/s, Time: 0.01s, ETA: 0.00s), File: httptest.jpg, Path: C:\Workspaces\httptest.jpg. ``` 若需在控制台中实时显示文件上传进度,推荐使用 `UpdateConsoleProgressAsync()` 方法。示例如下: ```cs showLineNumbers {2,5} await httpRemoteService.UploadFileAsync("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file" , progress => progress.UpdateConsoleProgressAsync()); // 在控制台中更新文件传输进度条 // ✅ 或使用 UploadFileWithConsoleProgressAsync 方法(带控制台进度打印) await httpRemoteService.UploadFileWithConsoleProgressAsync("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file"); ``` 执行后,控制台将显示如下进度信息: ```bash showLineNumbers {2} File: httptest.jpg, Path: C:\Workspaces\httptest.jpg. [##################################################] 61.35% (0.01MB/0.01MB) Speed: 0.86MB/s, Time: 0.01s, ETA: 0.00s. ``` 若需限制文件类型和大小,可如下操作: ```cs showLineNumbers {1,6-7} await httpRemoteService.SendAsync(HttpRequestBuilder.UploadFile("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file" , async progress => { Console.WriteLine(await progress.ToSummaryStringAsync()); // 输出简要进度信息 }) .SetAllowedFileExtensions(".jpg;.png") // 仅允许 jpg 和 png 类型 .SetMaxFileSizeInBytes(5 * 1024 * 1024)); // 限制文件大小为 5MB ``` 若需在上传文件时附加额外的表单参数,可如下操作: ```cs showLineNumbers {6-9} await httpRemoteService.SendAsync(HttpRequestBuilder.UploadFile("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file" , async progress => { Console.WriteLine(await progress.ToSummaryStringAsync()); // 输出简要进度信息 }) .WithMultipart(multipart => { multipart.AddText("Furion", "name"); }); ``` 通过上述方式,可以灵活满足各类文件上传需求。 > **关于多文件上传** `UploadFile` 扩展方式仅支持单个文件上传,无法同时处理多个文件的上传需求。 > **禁用请求分析工具** 在打印请求内容时,`Stream` 对象可能会被重复读取或变得不可读。这是因为流会被提前读取到内存中,其位置指针会移动到尾部。这会导致无法准确获取上传进度。 因此,在使用框架提供的专门上传功能时,建议禁用请求分析工具,以确保能够获取准确的上传进度信息。 --- # 2.6 HTTP 声明式请求(代理方式) > 原文链接:https://http.furion.net/docs/quick-start/declarative/ `HTTP` 声明式请求机制通过实现 `IHttpDeclarative` 接口,在程序运行时动态地构建实现类。该机制会智能地拦截符合特定规则的方法调用,并自动生成相应的 `HTTP` 远程请求代码。这种方法不仅极大地减轻了开发人员编写 `HTTP` 请求代码的负担,而且使得代码结构更加条理分明,更易于进行组织、维护和复用。 以下示例简单展示了如何定义和使用 `HTTP` 声明式请求: **1. 定义接口 `IHttpService` 并实现 `IHttpDeclarative`** ```cs showLineNumbers {1,4-5,8-10,13-14,17-18,21-22} public interface IHttpService : IHttpDeclarative { // 获取网站内容 [Get("https://furion.net")] Task GetWebSiteContent(); // 携带请求数据 [Post("https://localhost:7044/HttpRemote/AddModel")] [QueryParam("query1", 1)] // 设置查询参数 Task PostData([QueryParam(AliasAs = "query2")] string param, [Body(MediaTypeNames.Application.Json)] object data); // 设置查询参数并指定别名和请求内容 // Form 表单提交 [Post("https://localhost:7044/HttpRemote/AddForm?id=1")] Task PostForm(Action multipart); // Form 表单提交 [Post("https://localhost:7044/HttpRemote/AddForm?id=1")] Task PostForm2([Multipart(AsFormItem = false)] object obj, [Multipart("file", AsFileFrom = FileSourceType.Path)] string filePath); // URL 编码表单提交 [Post("https://localhost:7044/HttpRemote/AddURLForm")] Task PostURLForm([Body(MediaTypeNames.Application.FormUrlEncoded)] object data); } ``` **2. 注册 `IHttpService` 服务** 在 `Startup.cs` 或 `Program.cs` 文件中,注册并配置 `HttpRemote` 服务以支持 `HTTP` 声明式请求: ```cs showLineNumbers {1,4,7} services.AddHttpRemote(builder => { // 注册单个 HTTP 声明式请求接口 builder.AddHttpDeclarative(); // 扫描程序集批量注册 HTTP 声明式请求接口(推荐此方式注册) // builder.AddHttpDeclarativesFromAssemblies([Assembly.GetEntryAssembly()]); // 如果使用的是 Furion 框架,可直接传入 App.Assemblies }); ``` **3. 注入 `IHttpService` 服务** 在需要使用 `IHttpService` 的类中,通过依赖注入获取其实例: ```cs showLineNumbers {3,5} public class YourService { private readonly IHttpService _httpService; public YourService(IHttpService httpService) { _httpService = httpService; } } ``` 若您使用的是 `.NET 8` 及以上版本时,可通过[主构造函数](https://learn.microsoft.com/zh-cn/dotnet/csharp/whats-new/tutorials/primary-constructors)注入简化代码: ```cs showLineNumbers {1} public class YourService(IHttpService httpService) { // 使用 httpService 变量 } ``` 或者,您也可以在特定方法中按需注入: ```cs showLineNumbers {3} public class YourService { public Task GetResource([FromServices] IHttpService httpService) { // 您的代码逻辑 } } ``` **4. 调用 `IHttpService` 方法** 使用注入的 `IHttpService` 实例调用其方法,以发送 `HTTP` 请求并获取响应: ```cs showLineNumbers {2,5,8-10,12,15} // 获取网站内容 var content = await httpService.GetWebSiteContent(); // 携带请求数据 var content = await httpService.PostData("furion", new { id = 1, name = "furion" }); // Form 表单提交 var content = await httpService.PostForm(multipart => multipart .AddJson(new { id = 1, name = "furion" }) // 设置常规字段 .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file")); var content = await httpService.PostForm2(new { id = 1, name = "furion" }, @"C:\Workspaces\httptest.jpg"); // URL 编码表单提交 var content = await httpService.PostURLForm(new { id = 1, name = "furion" }); ``` 通过使用 `HTTP` 声明式请求,您可以显著减少编写 `HTTP` 请求代码的工作量,并使代码更加简洁、易于组织和维护。在大型项目或多人合作项目中,这种方式尤其推荐。 --- # 2.7 请求分析工具 > 原文链接:https://http.furion.net/docs/quick-start/profiler/ 在现代化的浏览器中,通常内置了开发者工具,这些工具能够捕获并直观展示用户访问网站时的所有请求与响应数据。类似地,我们也为 `HTTP` 远程请求模块提供了一套分析工具。 ### 启用方式 以下是如何启用请求分析工具的示例: ```cs showLineNumbers {4,8,12-15} // 构建器方式 await httpRemoteService.SendAsync(HttpRequestBuilder.Get("https://furion.net") .WithHeader("X-Header", "custom") .Profiler()); // 启用请求分析工具,或使用 Debugger() // HTTP 请求谓词方式 await httpRemoteService.GetAsync("https://furion.net" , builder => builder.Profiler()); // 启用请求分析工具,或使用 Debugger() // 还可以获取请求分析工具的分析数据 await httpRemoteService.GetAsync("https://furion.net" , builder => builder.Profiler(analyzer => { Console.WriteLine(analyzer.Data); })); ``` 启用后,当执行 `HTTP` 远程请求时,控制台将输出如下详细信息: ```bash showLineNumbers Request Headers: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0 X-Header: custom General: Request URL: https://furion.net/ Request Method: GET Status Code: 200 OK HTTP Version: 1.1 HTTP Content: Content Type: HttpClient Name: Request Duration (ms): 149.00 Response Headers: Server: nginx/1.22.1 Date: Thu, 14 Nov 2024 15:35:41 GMT Connection: keep-alive Vary: Accept-Encoding ETag: "67091697-f32f" Cache-Control: max-age=315360000 Accept-Ranges: bytes Content-Type: text/html Content-Length: 62255 Last-Modified: Fri, 11 Oct 2024 12:14:15 GMT Expires: Thu, 31 Dec 2037 23:55:55 GMT ``` > **关于 `Blazor WebAssembly` 项目的说明** 在 `Blazor WebAssembly` 应用中,请求分析工具的内容将在客户端(即浏览器)的开发者工具控制台中显示。请确保在开发过程中检查此控制台以获取相关分析信息。 ### 全局启用与高级配置 此外,除了为单个请求启用分析工具,还可以全局注册以在 `HttpClient` 中启用: ```cs showLineNumbers {3,7,10,13-14,17-18,21-22} // 为默认客户端启用 services.AddHttpClient(string.Empty) .AddProfilerDelegatingHandler(); // 还可以提供条件禁用,例如生产环境中禁用 services.AddHttpClient(string.Empty) .AddProfilerDelegatingHandler(disableIn: () => builder.Environment.EnvironmentName == "Production"); services.AddHttpClient(string.Empty) .AddProfilerDelegatingHandler(disableInProduction: true); // 为特定客户端启用 //services.AddHttpClient("weixin") // .AddProfilerDelegatingHandler(); // 还可以一键为所有客户端配置启用 services.ConfigureHttpClientDefaults(clientBuilder => clientBuilder.AddProfilerDelegatingHandler()); // 或使用 IHttpRemoteBuilder 扩展方法进行一键配置 services.AddHttpRemote() .ConfigureHttpClientDefaults(clientBuilder => clientBuilder.AddProfilerDelegatingHandler()); ``` ### 声明式请求中的使用 同时,`HTTP` 声明式请求也支持通过 `[Profiler]` 特性启用请求分析工具: ```cs showLineNumbers {1,7,17} [Profiler] // 为接口内所有方法启用请求分析工具 public interface IHttpService : IHttpDeclarative { [Get("https://furion.net")] Task ProfilerMethod(); [Profiler(false)] // 关闭该方法请求分析工具 [Get("https://furion.net")] Task NonProfilerMethod(); } public interface IHttpService : IHttpDeclarative { [Get("https://furion.net")] Task NonProfilerMethod(); [Profiler] // 启用该方法请求分析工具 [Get("https://furion.net")] Task ProfilerMethod(); } ``` 通过启用请求分析工具,开发者能够更直观、便捷地观察和调试 `HTTP` 请求,从而提升开发效率与调试准确性。 ### 自定义日志输出目标 默认情况下,请求分析工具的信息会通过 `Console.WriteLine` 输出到控制台。但在 `WinForms`、`WPF` 或 `MAUI` 等环境中,控制台输出可能不可见或不被支持。此时,您可以通过配置 `FallbackLogger` 将日志重定向到其他目标,例如调试输出(`System.Diagnostics.Debug.WriteLine`): ```cs showLineNumbers {2,5} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // 日志服务或控制台输出不可用时的备用日志输出委托 options.FallbackLogger = message => System.Diagnostics.Debug.WriteLine(message); }); ``` 这样,分析信息便会显示在 `Visual Studio` 的“输出”窗口(调试视图)或其他调试器所支持的位置。 > **生产环境禁用** 为了确保生产环境的最佳性能和安全性,建议在生产环境中**禁用**请求分析工具。 此外,打印请求内容时可能会导致 `Stream` 对象被重复读取或变得不可读,因为流会被提前读取到内存中,其 `Position` 随之移动到尾部。 **补充说明:** 请求分析工具默认仅展示请求或响应内容中最多 `5KB` 的内容数据。 --- # 2.8 添加授权凭证 > 原文链接:https://http.furion.net/docs/quick-start/authentication/ 在互联网社会中,网络安全愈发关键,特别是在与第三方接口对接时,通常需先通过鉴权授权才能访问。目前,互联网应用接口常用的授权方式包括 `Bearer` 身份认证、`Basic` 身份认证、`Digest` 摘要身份认证和 `OAuth` 身份认证。 以下示例展示了如何为 `HTTP` 远程请求添加授权: ```cs showLineNumbers {3,7,11,15} // 添加 Bearer 身份认证 await httpRemoteService.SendAsync(HttpRequestBuilder.Get("http://furion.net") .AddBearerAuthentication("your token")); // 添加 Basic 身份认证 await httpRemoteService.SendAsync(HttpRequestBuilder.Get("http://furion.net") .AddBasicAuthentication("username", "password")); // 添加 Digest 摘要身份认证 await httpRemoteService.SendAsync(HttpRequestBuilder.Get("http://furion.net") .AddDigestAuthentication("username", "password")); // 添加自定义 Schema 身份认证 await httpRemoteService.SendAsync(HttpRequestBuilder.Get("http://furion.net") .AddAuthentication(new AuthenticationHeaderValue("X-Token", "your token"))); ``` 若授权凭证正确,用户即可成功访问网络资源;否则,服务将返回 `401` 未授权错误。 除了为单个请求手动添加授权凭证外,您还可以通过创建一个自定义的 `AuthorizationDelegatingHandler` 类继承自 `DelegatingHandler` 类,实现全局授权凭证的注册: ```cs showLineNumbers {1,5,13,16-17,20-21,24} public class AuthorizationDelegatingHandler : DelegatingHandler { protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { // 参考 SendAsync 代码 return base.Send(request, cancellationToken); } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // 添加 Bearer 身份认证 request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "your token"); // 添加 Basic 身份认证 var base64Credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes("username" + ":" + "password")); request.Headers.Authorization = new AuthenticationHeaderValue("Basic", base64Credentials); // 添加 Digest 摘要身份认证 var digestCredentials = DigestCredentials.GetDigestCredentials($"https://furion.net/digest", "admin", "a123456789", HttpMethod.Get); request.Headers.Authorization = new AuthenticationHeaderValue("Digest", digestCredentials); // 添加自定义 Schema 身份认证 request.Headers.Authorization = new AuthenticationHeaderValue("X-Token", "your token"); return base.SendAsync(request, cancellationToken); } } ``` **注意:** 在实际应用中,您应该根据需求选择一种认证方式,而不是在一个请求中同时使用多种认证头。上述代码中的多种认证方式只是为了展示如何设置不同的认证头。 接下来,在 `Startup.cs` 或 `Program.cs` 文件中注册 `AuthorizationDelegatingHandler`: ```cs showLineNumbers {2,6,9-10} // 注册 AuthorizationDelegatingHandler 为服务 services.TryAddSingleton(); // 为默认客户端启用 services.AddHttpClient(string.Empty) .AddHttpMessageHandler(); // 为特定客户端启用 //services.AddHttpClient("weixin") // .AddHttpMessageHandler() ``` 这样,每当发送 `HTTP` 请求时,都会进入 `AuthorizationDelegatingHandler` 类的 `Send/SendAsync` 方法,从而自动为请求添加授权凭证。 --- # 2.9 设置 Cookie(模拟/自动登录) > 原文链接:https://http.furion.net/docs/quick-start/cookies/ `Cookie` 是服务器在 `HTTP` 响应中发送的一段数据。 客户端 (选择性地) 存储 `Cookie`,并在后续请求中返回它。 这允许客户端和服务器共享状态。在发送 `HTTP` 远程请求时,设置 `Cookie` 有以下两种方式。 - **通过 `HttpClient` 全局设置 `Cookie`** 这种方式允许在同源域名下共享 `Cookie`,并且如果服务器返回了新的 `Cookie`,这些 `Cookie` 将在后续请求中自动携带,非常适用于实现**网站自动登录**等功能。 ```cs showLineNumbers {1-3,7-12} var cookieContainer = new CookieContainer(); // 可选设置默认 Cookie cookieContainer.Add(new Uri("https://furion.net"), new Cookie("cookieName", "cookieValue")); // 默认客户端配置 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { CookieContainer = cookieContainer, UseCookies = true, // 自动处理 Cookies,将在后续请求自动携带 AllowAutoRedirect = true }); ``` > **自动处理 `Cookies` 安全性说明** 自动处理 `Cookies` 可能导致敏感信息的泄露,尤其是当应用程序在多个不同的域之间共享同一个 `HttpClient` 实例时。如果一个域的响应中包含了一个 `Cookie`,而这个 `Cookie` 被自动添加到对另一个域的请求中,可能会导致信息泄露。同时,自动处理 `Cookies` 增加了 `CSRF` 攻击的风险,因为攻击者可能利用已存在的 `Cookies` 发起未经用户同意的操作。 如果应用需要 `Cookie`,请考虑**禁用自动 `Cookie` 处理**,调用 `ConfigurePrimaryHttpMessageHandler` 以禁用自动 `Cookie` 处理: ```cs showLineNumbers {3-7} // 默认客户端配置 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { UseCookies = false // 禁用自动处理 Cookies }); ``` - **单个请求设置 `Cookie`** 这种方式仅对当前请求有效,如果服务器返回了新的 `Cookie`,它们不会被后续请求携带。 ```cs showLineNumbers {2-3} await httpRemoteService.SendAsync(HttpRequestBuilder.Get("http://furion.net") .WithCookie("cookieName", "cookieValue") // 设置单个 .WithCookies(new { name = "furion", author = "monksoul" })); // 设置多个 ``` **注意:** 在实际应用中,您可能需要根据具体需求选择适当的 `Cookie` 设置方式,并确保 `Cookie` 的安全性,例如避免跨站脚本攻击(`XSS`)和跨站请求伪造攻击(`CSRF`)。同时,对于敏感信息,建议使用更安全的认证机制,如 `OAuth`、`JWT` 等。 --- # 2.10 异常处理(异常抑制) > 原文链接:https://http.furion.net/docs/quick-start/exception-handling/ 在发起 `HTTP` 远程请求时,可能会遇到以下异常情况: - 目标主机不可达 - 请求被取消 - 请求超时 - 其他网络异常 默认情况下,这些异常会中断程序执行。为提升系统健壮性,框架提供以下异常处理方案: **1. 基础异常捕获(`try/catch` 模式)** ```cs showLineNumbers {1,5,7-8} try { var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/"); } catch(Exception ex) { var httpResponseMessage = ex.GetResponseMessage(); // 可通过扩展获取 HttpResponseMessage? 对象 var requestDuration = ex.GetRequestDuration(); // 可通过扩展获取请求耗时(毫秒) // 异常处理逻辑(如日志记录、降级处理) } ``` 适用场景:需要精确控制异常处理逻辑时(如记录特定异常日志、执行补偿操作)。 **2. 异常抑制(静默模式)** 虽然开发者通常使用 `try/catch` 进行异常处理,但在某些场景下,我们更希望异常发生时静默返回 `null` 而不中断流程。为此,框架提供了灵活的异常抑制功能。 - **抑制所有请求异常** ```cs showLineNumbers {2} var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/") .SuppressExceptions()); // 抑制所有异常 ``` 当请求发生异常时,代码不会中断,而是返回 `null`,即 `httpResponseMessage` 的值为 `null`。 在某些场景下,我们希望在抑制异常的同时,仍能捕获异常信息(例如记录到日志中),而不中断程序的正常执行。此时,可以通过 `SetOnRequestFailed` 回调来实现: ```cs showLineNumbers {2-3} HttpRequestBuilder.Post("https://furion.net/") .SuppressExceptions() .SetOnRequestFailed((exception, responseMessage) => // 注意:responseMessage 可能为空 { Console.WriteLine(exception.Message); }); ``` 该方法允许你在异常被抑制后,安全地处理错误信息,适用于日志记录、监控或其他错误响应逻辑。 - **仅抑制特定类型的异常** 框架还支持仅抑制特定类型的异常。例如,可以仅抑制超时异常和请求取消异常: ```cs showLineNumbers {2} var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/") .SuppressExceptions([typeof(TimeoutException), typeof(TaskCanceledException)])); // 抑制超时和取消异常 ``` - **禁用异常抑制配置** 若需恢复默认行为(即异常发生时中断程序),可显式禁用异常抑制: ```cs showLineNumbers {2} var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/") .SuppressExceptions(false); // 恢复缺省配置 ``` 此配置等效于未调用 `SuppressExceptions()`,当发生任何异常时,程序将中断执行。 > **注意事项** 当启用异常抑制功能时,请注意以下事项: 1. **覆盖规则** 多次调用 `SuppressExceptions()` 或相关配置时,**仅最后一次调用生效**。 2. **状态码检查与异常抑制的优先级** 即使已配置 `EnsureSuccessStatusCode()`,被抑制的异常仍会返回 `null`,不会触发状态码检查逻辑。 3. **异常抑制的优先级** 异常抑制功能的优先级高于状态码检查。如果同时启用状态码检查和异常抑制,异常抑制会优先生效。 4. **请求拦截器依旧可用** 若通过 `SetOnRequestFailed(ex, res)` 或其他请求处理机制捕获异常,即使异常被抑制,拦截器或回调方法仍会被调用。 5. **异常类型选择建议** 应根据具体业务场景谨慎选择需要抑制的异常类型,避免因过度抑制异常而掩盖潜在问题。 6. **自动输出抑制日志** 当异常被成功抑制时,框架会自动输出一条 `Warning` 级别日志(例如 `"An exception occurred but was suppressed by SuppressExceptionPipelineHandler."`),便于排查问题。 --- # 2.11 压力与模拟测试(性能测试) > 原文链接:https://http.furion.net/docs/quick-start/stress-test/ 在开发面向互联网或需承受多人并发访问的应用系统时,性能压测和接口自动化模拟测试成为部署前的关键环节。通过这两项测试获取的报告指标,我们能在系统上线前对代码进行优化,确保其满足最低上线要求。 以 `Furion` 框架官网为例,进行压力测试: ```cs showLineNumbers {1-2} var stressTestHarnessResult = await httpRemoteService.StressTestHarnessAsync("https://furion.net/"); Console.WriteLine(stressTestHarnessResult.ToString()); // 打印压力测试结果 ``` 测试结果概览: ```bash showLineNumbers Stress Test Harness Result: Total Requests: 100 // 总请求次数 Total Time (s): 7.95 // 总用时(秒) Successful Requests: 100 // 成功请求次数 Failed Requests: 0 // 失败请求次数 QPS: 12.58 // 每秒查询率 (QPS) Min RT (ms): 676.38 // 最小响应时间(毫秒) Max RT (ms): 7,419.72 // 最大响应时间(毫秒) Avg RT (ms): 3,314.94 // 平均响应时间(毫秒) P10 RT (ms): 1,288.82 // P10 响应时间(毫秒) P25 RT (ms): 2,057.10 // P25 响应时间(毫秒) P50 RT (ms): 3,064.56 // P50 响应时间(毫秒) P75 RT (ms): 4,100.03 // P75 响应时间(毫秒) P90 RT (ms): 5,026.08 // P90 响应时间(毫秒) P95 RT (ms): 7,330.71 // P95 响应时间(毫秒) P99 RT (ms): 7,416.20 // P99 响应时间(毫秒) P99.99 RT (ms): 7,419.72 // P99.99 响应时间(毫秒) ``` `stressTestHarnessResult` 变量类型为 `StressTestHarnessResult`,包含以下属性和方法: - **属性**: - `TotalRequests`:总请求次数(`long` 类型)。 - `TotalTimeInSeconds`:总用时(秒)(`double` 类型)。 - `SuccessfulRequests`:成功请求次数(`long` 类型)。 - `FailedRequests`:失败请求次数(`long` 类型)。 - `QueriesPerSecond`:每秒查询率 (`QPS`)(`double` 类型)。 - `MinResponseTime`:最小响应时间(毫秒)(`double` 类型)。 - `MaxResponseTime`:最大响应时间(毫秒)(`double` 类型)。 - `AverageResponseTime`:平均响应时间(毫秒)(`double` 类型)。 - `Percentile10ResponseTime`:`P10` 响应时间(毫秒)(`double` 类型)。 - `Percentile25ResponseTime`:`P25` 响应时间(毫秒)(`double` 类型)。 - `Percentile50ResponseTime`:`P50` 响应时间(毫秒)(`double` 类型)。 - `Percentile75ResponseTime`:`P75` 响应时间(毫秒)(`double` 类型)。 - `Percentile90ResponseTime`:`P90` 响应时间(毫秒)(`double` 类型)。 - `Percentile95ResponseTime`:`P95` 响应时间(毫秒)(`double` 类型)。 - `Percentile99ResponseTime`:`P99` 响应时间(毫秒)(`double` 类型)。 - `Percentile9999ResponseTime`:`P99.99` 响应时间(毫秒)(`double` 类型)。 - **方法**: - `ToString()`:输出带缩进的详细报告字符串。 默认情况下,压力测试执行 `1` 轮,每次包含 `100` 个并发请求,最大并发度为 `100`。为获取更精确的测试结果,可按需调整这些参数: ```cs showLineNumbers {2-4,7,9} var stressTestHarnessResult = await httpRemoteService.SendAsync(HttpRequestBuilder.StressTestHarness("https://furion.net/") .SetNumberOfRequests(1000) // 设置并发请求数量 .SetNumberOfRounds(5) // 设置压测轮次 .SetMaxDegreeOfParallelism(500)); // 设置最大并发度 // 在大多数情况下,只需要设置并发请求数量即可 var stressTestHarnessResult = await httpRemoteService.StressTestHarnessAsync("https://furion.net/", 500); var stressTestHarnessResult = await httpRemoteService.SendAsync(HttpRequestBuilder.StressTestHarness("https://furion.net/", 500)); ``` > **快速生成测试报告** 进行压力测试时,默认使用 `GET` 请求并下载完整响应内容(`HttpCompletionOption.ResponseContentRead`)。若无需完整响应内容,可选择 `HEAD` 请求,并将 `completionOption` 设置为 `ResponseHeadersRead`,以快速生成压力测试报告。 > **滥用说明** **在进行压力测试时,会自动添加 `X-Stress-Test: Harness` 请求标头,以防止滥用对目标系统造成损害。** 同时,由于测试结果受硬件设备、操作系统及代码实现等多种因素影响,仅供参考。 此外,**为获取更准确的数据,请求分析工具默认被禁用**。 --- # 2.12 长轮询 Long Polling > 原文链接:https://http.furion.net/docs/quick-start/long-polling/ 长轮询(`Long Polling`)是一种实现服务器向客户端推送数据的技术。它通过保持 `HTTP` 连接打开直到有新数据发送给客户端,或者直到超时为止,从而模拟了服务器推送的效果。长轮询是传统轮询(即客户端定期向服务器发送请求以检查是否有新的数据)的一种改进,可以减少不必要的请求,提高效率。 长轮询的工作原理: 1. 客户端向服务器发起一个请求。 2. 如果服务器上没有新数据,服务器不会立即响应这个请求,而是将请求挂起。 3. 一旦服务器上有新数据可供发送,或达到了预设的超时时间,服务器就会响应请求,并发送数据给客户端。 4. 客户端处理完数据后,再次向服务器发起一个新的请求,重复上述过程。 ![long-polling](/images/long-polling.png) 长轮询的应用场景: - **实时通知**:例如,在线聊天应用中,当用户收到新消息时,服务器可以通过长轮询及时推送消息给客户端。 - **在线协作工具**:如多人同时编辑文档的应用,长轮询可以用来实时同步用户的编辑操作。 - **游戏更新**:在网络游戏中,长轮询可用于实时更新游戏状态,比如玩家位置、得分等信息。 - **股票市场更新**:金融应用程序中使用长轮询来实时显示股票价格变动。 - **配置中心**:在微服务架构中,配置中心使用长轮询技术来确保各个服务能够即时接收到最新的配置变更。当配置发生更改时,配置中心可以迅速将更新推送到所有相关的服务实例,确保配置的一致性和时效性。 以下示例展示了如何使用长轮询请求: ```cs showLineNumbers {1-2,9-11} await httpRemoteService.LongPollingAsync("https://localhost:7044/HttpRemote/LongPolling" , async (responseMessage, token) => { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(token)); await Task.CompletedTask; }, cancellationToken: cancellationToken); // 使用构建器模式 await httpRemoteService.SendAsync(HttpRequestBuilder .LongPolling("https://localhost:7044/HttpRemote/LongPolling" , async (responseMessage, token) => { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(token)); await Task.CompletedTask; }), cancellationToken: cancellationToken); ``` 长轮询也支持以 `IAsyncEnumerable` 的方式消费数据,让你可以使用 `await foreach` 来迭代每个轮询响应: ```cs showLineNumbers {1,4,6,11,13,15} await foreach (var responseMessage in httpRemoteService.LongPollingAsAsyncEnumerable("https://localhost:7044/HttpRemote/LongPolling", cancellationToken: cancellationToken)) { // 注意:每个响应在使用后需要手动释放(或使用 using) using (responseMessage) { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(cancellationToken)); } } // 使用构建器模式 await foreach (var responseMessage in httpRemoteService.SendAsAsyncEnumerable(HttpRequestBuilder.LongPolling("https://localhost:7044/HttpRemote/LongPolling"), cancellationToken)) { using (responseMessage) { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(cancellationToken)); } } ``` 虽然长轮询在一定程度上解决了实时通信的需求,但它也有一些缺点,比如在高并发情况下可能会对服务器造成较大压力,以及长时间的连接可能会影响服务器的性能。随着 `Web` 技术的发展,`Server-Sent Events` 或 `WebSocket` 等更先进的技术逐渐成为实现实时双向通信的首选方案。然而,在某些受限环境中,长轮询仍然是一个可行的选择。 --- # 2.13 Server-Sent Events 单向通信 > 原文链接:https://http.furion.net/docs/quick-start/sse/ 随着人工智能聊天机器人 `ChatGPT` 的快速流行,其用户界面中模拟打字机效果的对话设计给人留下了深刻印象。这种生动逼真的交互体验,实际上是通过一种称为“服务器发送事件”(`Server-Sent Events`, `SSE`)的技术实现的。 `Server-Sent Events` 是一种允许服务器主动向客户端(通常是浏览器)发送实时更新数据的通信技术。**与传统的客户端请求-服务器响应模式不同,`SSE` 实现了服务器到客户端的单向、异步通信,从而无需客户端不断轮询服务器以获取最新数据。** 这种技术极大地减轻了服务器的负担,并提高了数据传输的效率和实时性。 `Server-Sent Events` 的应用场景: 1. **实时通知**:可以用来实现实时的消息提醒或通知系统,如社交网络上的新消息提示或邮件到达通知。 2. **数据流更新**:对于需要持续更新的数据,如股票价格、天气信息或体育比赛结果,`SSE` 能够提供即时的数据更新。 3. **进度报告**:在执行耗时较长的任务时,比如文件上传或复杂计算过程中,`SSE` 可以用来向客户端报告任务的进度。 4. **日志和监控**:在开发和运维领域,`SSE` 可用于实时显示日志文件的变化或监控系统的健康状态。 以下示例展示了如何使用 `Server-Sent Events` 向服务器获取数据: ```cs showLineNumbers {1,3,11,13} await httpRemoteService.ServerSentEventsAsync("https://localhost:7044/HttpRemote/Events" // 接收到数据时的操作 , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }, cancellationToken: cancellationToken); // 使用构建器模式 await httpRemoteService.SendAsync(HttpRequestBuilder .ServerSentEvents("https://localhost:7044/HttpRemote/Events" // 接收到数据时的操作 , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }), cancellationToken: cancellationToken); ``` `Server-Sent Events` 也支持以 `IAsyncEnumerable` 的方式消费数据,让你可以使用 `await foreach` 来迭代每个轮询响应: ```cs showLineNumbers {1,3,7,9} await foreach (var data in httpRemoteService.ServerSentEventsAsAsyncEnumerable("https://localhost:7044/HttpRemote/Events", cancellationToken: cancellationToken)) { Console.WriteLine(data.Data); } // 使用构建器模式 await foreach (var data in httpRemoteService.SendAsAsyncEnumerable(HttpRequestBuilder.ServerSentEvents("https://localhost:7044/HttpRemote/Events"), cancellationToken)) { Console.WriteLine(data.Data); } ``` `data` 参数的类型为 `ServerSentEventsData`,包含以下属性: - **属性**: - `Event`:事件类型(`string` 类型)。 - `Data`:消息(`string` 类型)。 - `RawLine`:原始消息行(`string` 类型)。 - `Id`:事件 `ID`(`string` 类型)。 - `Retry`:重新连接的时间(以毫秒为单位的 `int` 类型)。 - `CustomFields`:自定义的字段数据(`IReadOnlyCollection>` 类型)。 您还可以监听连接成功和发送异常时的事件: ```cs showLineNumbers {9,14,29,34} await httpRemoteService.ServerSentEventsAsync("https://localhost:7044/HttpRemote/Events" // 接收到数据时的操作 , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }, builder => builder // 连接打开时操作 .SetOnOpen(() => { Console.WriteLine("连接成功。"); }) // 连接未打开时操作 .SetOnError((ex) => { Console.WriteLine("连接错误。" + ex.Message); }), cancellationToken: cancellationToken); // 使用构建器模式 await httpRemoteService.SendAsync(HttpRequestBuilder .ServerSentEvents("https://localhost:7044/HttpRemote/Events" // 接收到数据时的操作 , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }) // 连接打开时操作 .SetOnOpen(() => { Console.WriteLine("连接成功。"); }) // 连接未打开时操作 .SetOnError((ex) => { Console.WriteLine("连接错误。" + ex.Message); }), cancellationToken: cancellationToken); ``` `Server-Sent Events` 特别适合那些需要服务器向客户端发送更新,但客户端不需要频繁向服务器发送请求的应用场景。无论是用于实时更新数据、提供进度报告还是实现简单的通知系统,`SSE` 都是一个值得考虑的选择。 > **禁用请求分析工具** 在发送 `Server-Sent Events`(服务器发送事件)时,由于它采用 `Stream` 流式返回数据,如果启用请求分析工具,会导致流式数据的每个部分被提前加载到内存中读取。这不仅会严重影响流式数据的实时显示效果,还可能在返回大量数据时引发内存过高的问题。 因此,建议在发送 `Server-Sent Events` 请求时关闭请求分析工具。 > **请求谓词说明** 标准化的 `Server-Sent Events (SSE)` 仅支持通过 `GET` 方法接收服务器推送的事件。但框架提供支持通过任意请求谓词(如示例中的 `POST`)来配置 `SSE`: ```cs showLineNumbers {} HttpRequestBuilder .ServerSentEvents(HttpMethod.Post, new Uri("https://localhost:7044/HttpRemote/Events")); ``` --- # 2.14 WebSocket 双工通信 > 原文链接:https://http.furion.net/docs/quick-start/websocket/ `WebSocket` 是一种在单个 `TCP` 连接上进行全双工通信的协议。`WebSocket `使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 `WebSocket API` 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。 `WebSocket` 的应用场景: - **实时聊天应用**:`WebSocket` 可以实现实时的消息传递,使得用户间的交流几乎无延迟。 - **在线游戏**:对于需要快速响应的游戏,`WebSocket` 能够提供低延迟的数据传输。 - **股票市场更新**:实时更新股票价格和其他金融信息。 - **协同编辑工具**:允许多个用户同时编辑同一个文档,并实时看到其他人的更改。 - **实时地图应用**:例如导航应用中实时交通状况的更新。 以下示例展示了如何使用 `WebSocketClient` 连接服务器: ```cs showLineNumbers {1,4,10,16,22,29,32,38,45,49} using var webSocketClient = new WebSocketClient("wss://ws.postman-echo.com/raw"); // 支持 ws:// 和 wss:// // 连接成功事件 webSocketClient.Connected += (sender, s) => { Console.WriteLine("连接成功"); return Task.CompletedTask; }; // 连接关闭事件 webSocketClient.Closed += (sender, args) => { Console.WriteLine("连接关闭"); return Task.CompletedTask; }; // 接收文本消息 webSocketClient.TextReceived += (sender, s) => { Console.WriteLine(s.Message); return Task.CompletedTask; }; // 接收二进制消息 webSocketClient.BinaryReceived += (sender, s) => { Console.WriteLine(s.Message); return Task.CompletedTask; }; // 连接服务器 await webSocketClient.ConnectAsync(); // 开启一个任务,循环发送消息 _ = Task.Run(async () => { var i = 0; while (i < 5) { // 发送文本消息 await webSocketClient.SendAsync("Hello, WebSocket!"); await Task.Delay(1000); i++; } // 关闭连接 await webSocketClient.CloseAsync(); }); // 等待接收消息和关闭事件(阻塞) await webSocketClient.WaitAsync(); ``` `WebSocket` 与 `Server-Sent Events (SSE)` 的区别: - **通信方向**:`WebSocket` 支持全双工双向通信,`SSE` 仅支持服务器向客户端单向推送数据。 - **协议**:`WebSocket` 使用独立的 `WebSocket` 协议 (`ws://` 或 `wss://`),`SSE` 基于 `HTTP` 协议。 - **握手过程**:`WebSocket` 需要特殊的 `HTTP` 升级头来转换协议,`SSE` 无需特殊握手,直接通过 `HTTP` 请求建立连接。 - **连接保持**:`WebSocket` 连接保持直到显式关闭,`SSE` 可能因网络问题断开,但浏览器会自动重连。 - **数据格式**:`WebSocket` 支持多种数据格式,包括二进制数据,`SSE` 数据格式较固定,通常是简单的文本消息。 - **跨域支持**:`WebSocket` 建立连接时检查跨域策略,连接后不受限,`SSE` 依赖于 `CORS` 策略。 选择使用 `WebSocket` 还是 `SSE` 主要取决于具体的应用需求: - 如果需要实现双向通信或处理大量数据流,`WebSocket` 是更好的选择; - 如果只是需要服务器向客户端推送更新,且对数据格式要求不高,`SSE` 可能更加轻量和易于实现。 --- # 2.15 HttpContext 转发和代理 > 原文链接:https://http.furion.net/docs/quick-start/httpcontext-forward/ `HttpContext` 转发是指在 `ASP.NET Core` 应用程序中,将一个 `HTTP` 请求的上下文信息(包括请求标头、请求内容、查询字符串、响应标头、响应内容等)从一个请求转发到另一个内部请求或服务的过程。这种技术允许开发者在不改变客户端请求的情况下,将请求重定向到另一个处理点,从而实现请求的代理或路由功能。 `HttpContext` 转发的应用场景: - **`API Gateway` 模式**:作为所有外部请求的入口点,将请求路由到正确的后端服务。 - **负载均衡和故障转移**:将请求转发到其他可用的服务实例,确保系统的稳定性和可靠性。 - **请求日志记录和审计**:将请求信息记录到日志系统或审计服务,便于监控和调试。 - **安全过滤和验证**:在转发过程中检查请求的认证信息和权限,确保请求的合法性。 - **A/B 测试和蓝绿部署**:将部分流量路由到新版本的服务,逐步验证新功能。 - **跨域请求处理**:处理跨域请求,确保请求能够成功执行。 在使用 `HttpContext` 进行转发操作之前,请确保已完成以下两个步骤: > **独立库说明** `Furion` 框架已内置该功能,无需额外安装 `NuGet` 包。若您使用 `HttpAgent` 独立库,请安装 `HttpAgent.AspNetCore` 以替代 `HttpAgent`。 1. 注册并启用 `IHttpContextAccessor` 服务。 在 `Startup.cs` 或 `Program.cs` 文件中注册并启用 `IHttpContextAccessor` 服务,并配置转发目标白名单。 ```cs showLineNumbers {1,4,7} services.AddHttpContextAccessor(); // 若使用 Furion 框架无需注入(已默认注入) // 全局配置 HttpContext 转发配置选项 services.Configure(options => { // 允许转发的目标主机白名单,必须显式配置;若未配置或为空,任何通过 X-Forward-To 头的转发都会被拒绝 options.AllowedHosts = ["*"]; // "*" 表示允许所有主机和协议(高风险,仅推荐在信任环境使用) }); ``` **`AllowedHosts` 白名单规则详解:** - `"furion.net"` — 仅主机名,匹配任意协议(`http/https`)的默认端口(`80/443`)。 - `"furion.net:8080"` — 主机+端口,匹配任意协议的指定端口。 - `"furion.net:*"` — 主机+端口通配符,匹配任意协议下的任意端口。 - `"https://furion.net"` — 协议+主机,只匹配指定协议的默认端口。 - `"http://furion.net:8080"` — 协议+主机+端口,精确匹配。 - `"https://furion.net:*"` — 协议+主机+端口通配符,只匹配指定协议的任意端口。 - `"[::1]"` — `IPv6` 主机(方括号包裹),匹配任意协议的默认端口。 - `"[::1]:8080"` — `IPv6` 主机+端口,匹配任意协议的指定端口。 - `"[::1]:*"` — `IPv6` 主机+端口通配符,匹配任意协议下的任意端口。 - `"http://[2001:db8::1]:8080"` — 协议+IPv6 主机+端口,精确匹配。 - `"*"` — 全局通配符,允许任意主机和协议(完全绕过所有主机验证)。 > **安全风险提示** - **务必显式配置 `AllowedHosts`**,留空或未配置将拒绝所有 `X-Forward-To` 转发请求,防止 `SSRF` 攻击。 - 使用全局通配符 `*` 会使应用完全暴露在 `SSRF` 风险之下,请仅在完全信任请求来源(如内网管理服务)且了解风险时启用。 - 建议尽可能使用最严格的规则(如指定协议和端口),并配合网络防火墙限制出站流量。 - 所有主机名和协议匹配均不区分大小写,防止大小写混淆绕过。 2. 启用请求正文缓存中间件,以支持请求内容的重复读取。 ```cs showLineNumbers app.UseEnableBuffering(); ``` 3. **(可选)** 若在转发过程中出现 `The SSL connection could not be established, see inner exception.` 的证书错误问题,您可以通过添加以下配置来忽略 `SSL` 证书验证: ```cs showLineNumbers {3,6-7,12,14,17-18} // 默认客户端配置 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { // 忽略 SSL 证书验证 ServerCertificateCustomValidationCallback = HttpRemoteUtility.IgnoreSslErrors, SslProtocols = HttpRemoteUtility.AllSslProtocols }); // 若使用 SocketsHttpHandler,可以通过以下配置来忽略 SSL 证书验证 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler() { SslOptions = new SslClientAuthenticationOptions { // 忽略 SSL 证书验证 RemoteCertificateValidationCallback = HttpRemoteUtility.IgnoreSocketSslErrors, EnabledSslProtocols = HttpRemoteUtility.AllSslProtocols }, }); ``` 以下是一个简单的示例,展示了如何在 `ASP.NET Core` 中实现 `HttpContext` 转发: ```cs showLineNumbers {3,10,18-19,27-28,35-35} [ApiController] [Route("[controller]/[action]")] public class GetStartController(IHttpRemoteService httpRemoteService, IHttpContextAccessor httpContextAccessor) : ControllerBase { // 转发代理到网站 [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // 禁用浏览器缓存 public Task ForwardToWebSite() { return httpContextAccessor.HttpContext.ForwardAsResultAsync("https://github.com"); } // 转发代理到图片 [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // 禁用浏览器缓存 public Task ForwardToImage() { return httpContextAccessor.HttpContext.ForwardAsResultAsync( "https://img-s-msn-com.akamaized.net/tenant/amp/entityid/AA1u7RJI.img?w=584&h=326&m=6"); } // 转发代理到下载 [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // 禁用浏览器缓存 public Task ForwardToDownload() { return httpContextAccessor.HttpContext.ForwardAsResultAsync( "https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe"); } // 转发代理到表单 [HttpPost] public Task ForwardToForm(int id, [FromForm] YourRemoteFormModel model) { return httpContextAccessor.HttpContext.ForwardAsAsync( "https://localhost:7044/HttpRemote/AddForm"); } } ``` > **`X-Forward-To` 请求标头** 除了可以手动配置转发目标地址外,系统还支持通过解析 `X-Forward-To` 请求标头来自动设置目标地址。**注意**:使用该标头时,目标主机必须在 `AllowedHosts` 白名单中,否则转发将被拒绝。 > **`GET` 请求转发失败的可能原因** 在某些特殊应用场景中,例如通过 `GET` 请求转发至特定文件或图片时,可能会出现转发失败的情况。这可能是由于 `TLS/SSL` 证书问题导致的。此时,请确保用于转发的目标应用通过 `HTTPS` 协议部署网站。 通过 `HttpContext` 转发,可以在 `ASP.NET Core` 应用程序中结合 `Middleware` 中间件技术实现灵活的请求路由和处理机制,适用于多种应用场景,如 `API Gateway`、负载均衡、请求日志记录、安全验证等。 --- # 2.16 WebService 接口请求(SOAP) > 原文链接:https://http.furion.net/docs/quick-start/webservice/ `WebService` 是一种基于 `SOA`(面向服务架构)的应用程序,具有语言和平台无关性。它通过 `XML` 描述实现不同语言间的相互调用,并利用 `HTTP` 协议在 `Internet` 上进行网络应用间的交互。框架支持对 `WebService` 接口的请求,以下为示例代码: ### **`SOAP 1.1`** ```cs showLineNumbers {1-3,15} var result = await httpRemoteService.PostAsStringAsync("http://您的主机地址/Share/DatabaseManager.asmx", builder => builder.SetSOAPAction("http://tempuri.org/GetDatabaseList") // 可配置自动追加双引号:addQuotes: true .SetXmlContent(""" """, Encoding.UTF8)); ``` ### **`SOAP 1.2`** ```cs showLineNumbers {1-2,14} var result = await httpRemoteService.PostAsStringAsync("http://您的主机地址/Share/DatabaseManager.asmx", builder => builder.SetXmlContent(""" """, Encoding.UTF8, "application/soap+xml")); ``` > **确保服务器端支持 `SOAP 1.2`。** 如果服务器仅支持 `SOAP 1.1`,请调整请求以符合 `SOAP 1.1` 规范。具体做法是将 `XML` 内容中的 `xmlns:soap12` 和 `soap12:` 分别替换为 `xmlns:soap` 和 `soap:`。 > **`SOAP 1.1` 和 `SOAP 1.2` 的区别** | 特性 | SOAP 1.1 | SOAP 1.2 | | ------------------- | ------------------------------------------- | ----------------------------------------- | | **命名空间** | `http://schemas.xmlsoap.org/soap/envelope/` | `http://www.w3.org/2003/05/soap-envelope` | | **`Content-Type`** | `text/xml` | `application/soap+xml` | | **`SOAPAction` 头** | 必须设置 `SOAPAction` 请求头 | 可选,可以使用 `action` 参数 | | **错误处理** | 使用 `SOAP Fault` | 使用 `SOAP Fault`,但结构更规范 | | **协议支持** | 较旧,广泛支持 | 较新,支持更多特性(如 `MTOM`) | 在某些 `WebService` 接口返回的 `XML` 中,`soap:Body` 节点可能经过 `Base64` 编码和 `GZip` 压缩。此时,可通过以下代码进行解码和解压: ```cs showLineNumbers {2,4,6,9,12-15,18} // 使用 XDocument 解析 XML var xDocument = XDocument.Parse(result!); // SOAP 1.1 var bodyContent = xDocument.Descendants(XName.Get("Body", "http://schemas.xmlsoap.org/soap/envelope/")).FirstOrDefault()?.Value!; // SOAP 1.2 // var bodyContent = xDocument.Descendants(XName.Get("Body", "http://www.w3.org/2003/05/soap-envelope")).FirstOrDefault()?.Value!; // Base64 解码 var data = Convert.FromBase64String(bodyContent); // GZip 解压缩 using var input = new MemoryStream(data); await using var gzip = new GZipStream(input, CompressionMode.Decompress); using var output = new MemoryStream(); await gzip.CopyToAsync(output); // 获取实际内容 var body = Encoding.UTF8.GetString(output.ToArray()); ``` --- # 2.17 从 cURL 命令发送 > 原文链接:https://http.furion.net/docs/quick-start/from-curl/ > **`cURL` 在线测试** 推荐使用 [ReqBin](https://reqbin.com/curl) 在线测试和学习 `cURL` 命令,该网站提供大量可直接运行的 `cURL` 示例,调试 `API` 非常方便。 在与第三方 API 对接或调试时,`cURL` 命令是最常见的请求描述方式。框架内置了 `cURL` 命令解析引擎,支持从原生 `cURL` 命令字符串一键发起 HTTP 请求,覆盖常用选项(如 `-X`、`-H`、`-d`、`-F`、`-u`、`--data-urlencode`、`--max-time`、`--http2` 等),并可自由扩展自定义标志。 使用方式非常简单:将 `cURL` 命令直接传入 `HttpRequestBuilder.FromCurl()`,然后通过 `IHttpRemoteService` 发送即可。 ```cs showLineNumbers {2} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl("curl https://furion.net")); ``` 下面通过多个场景示例展示具体用法。 ### 常规 `GET` 请求 ```cs showLineNumbers {2} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl("curl https://furion.net")); ``` ### 携带查询参数和 `JSON` 请求体 ```cs showLineNumbers {3-8} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl(""" curl -k -X POST 'https://localhost:7044/HttpRemote/AddModel?query1=10&query2=hello' \ -H 'Content-Type: application/json' \ -d '{ "id": 1, "name": "sample" }' """)); ``` ### 多部分表单(文件上传 + 普通字段) ```cs showLineNumbers {3-6} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl(""" curl -k -X POST 'https://localhost:7044/HttpRemote/AddForm?id=100' \ -F 'Id=100' \ -F 'Name=furion' \ -F 'File=@C:\Workspaces\httptest.jpg' """)); ``` 文件上传使用 `@` 前缀,**路径支持本地绝对路径**(如 `C:\...`)或**网络 `URL`**(如 `@https://example.com/avatar.png`)。 ### `URL` 编码表单(`application/x-www-form-urlencoded`) 使用 `-d` 发送 URL 编码数据: ```cs showLineNumbers {3-5} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl(""" curl -k -X POST 'https://localhost:7044/HttpRemote/AddUrlForm' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'id=200&name=furion' """)); ``` 使用 `--data-urlencode` 自动编码空格等特殊字符: ```cs showLineNumbers {3-5} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl(""" curl -k -X POST 'https://localhost:7044/HttpRemote/AddUrlForm' \ --data-urlencode 'id=200' \ --data-urlencode 'name=fu rion' """)); ``` ### 单个文件上传 ```cs showLineNumbers {3-4} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl(""" curl -k -X POST 'https://localhost:7044/HttpRemote/AddFile' \ -F 'file=@C:\Workspaces\httptest.jpg' """)); ``` ### 多个文件上传 ```cs showLineNumbers {3-5} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl(""" curl -k -X POST 'https://localhost:7044/HttpRemote/AddFiles' \ -F 'files=@C:\Workspaces\httptest.jpg' \ -F 'files=@C:\Workspaces\httptest.jpg' """)); ``` ### 发送原始字符串(如 `"This is a raw string"`) ```cs showLineNumbers {3-5} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl(""" curl -k -X POST 'https://localhost:7044/HttpRemote/RawString' \ -H 'Content-Type: application/json' \ -d '"This is a raw string"' """)); ``` ### 带有认证信息的请求 ```cs showLineNumbers {3-6} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl(""" curl -X POST https://jsonplaceholder.typicode.com/posts \ -H "Content-Type: application/json" \ -u testuser:testpass \ -d '{"title":"Test"}' """)); ``` ### 忽略输出选项(如 `-o`) 某些 `cURL` 选项(如 `-o`、`-v`、`-s`)属于输出控制,**不影响请求构建**,会被自动忽略。例如: ```cs showLineNumbers {2} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl("curl -o qr.png \"https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=Hello\"")); ``` 该命令会正常发送 `GET` 请求并获取响应内容,但不会将结果保存为文件(文件保存需自行处理)。 > **注意事项** - **`cURL` 命令必须以 `curl` 开头**,否则会抛出 `InvalidOperationException("The cURL command must start with 'curl'.")`。 - 命令中不能只包含 `curl`(无任何参数),否则同样会抛出异常。 - 框架支持常用选项的自动解析,未识别选项将被跳过,不会引发错误。 - 文件上传路径需确保实际存在,否则可能引发运行时异常。 ### 扩展自定义 `cURL` 选项 框架的 `cURL` 解析器采用**可插拔提取器架构**,每个 `cURL` 选项均由独立的 `IHttpCurlExtractor` 实现处理。您可以通过新增自定义提取器来支持私有 `cURL` 标志(如 `--my-flag`),而无需修改框架源码。 #### 实现自定义提取器 创建自定义提取器最便捷的方式是继承 `HttpCurlExtractorBase` 基类,它已封装好游标推进和参数消费逻辑: ```cs showLineNumbers {4,9,14,22,25,28} /// /// 自定义 --my-flag 提取器 /// internal sealed class CurlMyFlagExtractor : HttpCurlExtractorBase { /// /// 匹配的标志集合(大小写不敏感) /// protected override string[] Flags => ["--my-flag"]; /// /// 是否需要参数。默认为 true;如果标志无需参数则设为 false。 /// protected override bool RequiresArgument => true; /// /// 当匹配到标志时执行的具体操作 /// /// 请求构建器 /// 当前匹配的标志(已转为小写) /// 携带的参数值,若无参数则为 null protected override void Extract(HttpRequestBuilder httpRequestBuilder, string flag, string? argument) { // 此处根据标志设置构建器 if (!string.IsNullOrWhiteSpace(argument)) { // 示例:将参数值放入 X-My-Flag 请求头 httpRequestBuilder.WithHeader("X-My-Flag", argument); } } } ``` 对于更复杂的场景(如需要控制优先级、手动管理游标),可直接实现 `IHttpCurlExtractor` 接口;若需排序,则额外实现 `IOrderedHttpCurlExtractor` 接口(`Order` 越小优先级越高)。 #### 注册自定义提取器 自定义提取器在调用 `FromCurl` 时通过配置委托注入: ```cs showLineNumbers {2-3} var builder = HttpRequestBuilder.FromCurl( "curl --my-flag hello-world http://example.com", options => options.AddExtractor(new CurlMyFlagExtractor()) ); ``` 如需移除内置提取器,可使用 `options.RemoveExtractor()`。例如: ```cs showLineNumbers {3} var builder = HttpRequestBuilder.FromCurl( "curl http://example.com", options => options.RemoveExtractor() ); ``` #### 上下文对象说明 `HttpCurlParsingContext` 提供了丰富的游标控制方法: | 成员 | 说明 | | :--------------------------- | :-------------------------------------------------- | | `CurrentToken` | 获取当前指向的 `Token` | | `PeekNext()` | 预览下一个 `Token`(不移动指针) | | `Advance(count)` | 向前推进指定步数(默认 1) | | `CurrentTokenMatches(flags)` | 检查当前 `Token` 是否匹配给定标志集合(忽略大小写) | | `IsEndOfTokens` | 是否已到达 `Token` 列表末尾 | 在直接实现 `IHttpCurlExtractor` 时,您需要自行调用 `Advance` 来消费 `Token`,否则会导致解析死循环。 #### 参考实现 框架内置的所有提取器(如 `CurlMethodExtractor`、`CurlFormExtractor` 等)均基于相同的接口和基类构建,您可以在仓库中查看其源码作为参考:[查看内置提取器源码](https://gitee.com/dotnetchina/HttpAgent/tree/master/src/HttpAgent/src/Parsers/cURL/Extractors) --- # 2.18 从 JSON 发送 > 原文链接:https://http.furion.net/docs/quick-start/from-json/ 框架同样支持从 `JSON` 配置字符串一键发起 `HTTP` 请求,完全替代传统的链式调用。只需将请求参数组织为 `JSON` 格式,传入 `HttpRequestBuilder.FromJson()` 方,然后通过 `IHttpRemoteService` 发送即可。 ```cs showLineNumbers {3-6} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://furion.net", "method": "GET" } """)); ``` ### `JSON` 语法全览 下表列出了所有可用的 `JSON` 字段(属性名不区分大小写,且支持尾随逗号): | 字段 (主键) | 别名 (Aliases) | 类型 | 必填 | 说明 | | :------------ | :------------------------------------ | :-------- | :--- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `method` | `httpMethod` | `string` | 否 | 请求方式(`GET`、`POST`、`PUT`、`DELETE`、`PATCH` 等)。若未指定,则根据是否包含请求体自动推断:有请求体时为 `POST`,否则为 `GET`。 | | `url` | `requestUri` | `string` | 是 | 请求地址。支持绝对 `URI`(如 `https://api.furion.net`)或相对路径(如 `/api/data`)。相对路径可配合 `baseURL` 使用。 | | `baseURL` | `baseAddress`, `baseUrl` | `string` | 否 | 请求基地址。必须为绝对 `URI`。当 `url` 为相对路径时,两者将按规则拼接为完整地址。 | | `headers` | – | `object` | 否 | 请求标头字典。键为标头名称,值为标头值(字符串)。例如 `{"Accept": "application/json", "X-API-Key": "xxx"}`。 | | `params` | `queries`, `query`, `queryParameters` | `object` | 否 | `URL` 查询参数。将自动拼接到请求地址的 `?` 后面。例如 `{"page": 1, "size": 10}` → `?page=1&size=10`。 | | `cookies` | – | `object` | 否 | `Cookies` 字典。例如 `{"session": "abc", "user": "john"}`。 | | `timeout` | – | `number` | 否 | 超时时间(毫秒)。例如 `5000` 表示 `5` 秒。 | | `client` | `clientName`, `httpClientName` | `string` | 否 | `IHttpClientFactory` 中注册的客户端名称。用于选择特定的 `HttpClient` 实例。 | | `httpVersion` | `version` | `string` | 否 | `HTTP` 版本。支持 `"1.0"`、`"1.1"`、`"2.0"`、`"3.0"` 等。 | | `auth` | `authentication`, `authorization` | `object` | 否 | 身份认证配置。必须包含 `type` 字段(`"bearer"`、`"basic"` 或 `"digest"`)。 **Bearer 示例**:`{"type": "bearer", "token": "xxx"}`,可选 `"header"` 自定义标头名(默认 `Authorization`)。 **Basic 示例**:`{"type": "basic", "username": "user", "password": "pass"}`。 **Digest 示例**:`{"type": "digest", "username": "user", "password": "secret"}`。 | | `data` | – | `any` | 否 | 请求体内容。可以是 `JSON` 对象、字符串、数字等。框架会将 `JsonNode` 作为原始内容传递,最终由内容处理器推断 `Content-Type`。 | | `contentType` | – | `string` | 否 | 配合 `data` 使用,显式指定 `Content-Type`。若不指定,框架会根据 `data` 的实际类型自动推断(例如 `JSON` 对象推断为 `application/json`)。 | | `encoding` | – | `string` | 否 | 配合 `data` 使用,指定内容编码(如 `"utf-8"`)。若未指定,使用默认编码。 | | `multipart` | – | `object` | 否 | 多部分表单(`multipart/form-data`)内容。对象的每个属性代表一个表单项。 普通字段:`"name": "John"` → 文本字段。 文件字段:`"file": "@C:\\path\\to\\file.jpg"` 或 `"@https://example.com/file.png"`(网络文件)。 支持 `@file;type=mime/type` 和 `@file;filename=renamed.txt` 语法。 多文件上传:`"files": ["@file1.jpg", "@file2.jpg"]`(数组形式,同一字段名)。 | | `profiler` | `debugger` | `boolean` | 否 | 是否启用请求分析工具。`true` 启用,`false` 禁用。 | **注意**:当未指定 `method` 时,框架根据 `data` 或 `multipart` 是否存在自动推断:存在则为 `POST`,否则为 `GET`。 ### 常规 `GET` 请求 ```cs showLineNumbers {3-6} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://furion.net", "method": "GET" } """)); ``` ### 携带查询参数和 `JSON` 请求体 ```cs showLineNumbers {6-16} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://localhost:7044/HttpRemote/AddModel", "method": "POST", "queries": { "query1": 10, "query2": "hello" }, "headers": { "Content-Type": "application/json" }, "data": { "id": 1, "name": "sample" } } """)); ``` ### 多部分表单(文件上传 + 普通字段) ```cs showLineNumbers {9-13} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://localhost:7044/HttpRemote/AddForm", "method": "POST", "queries": { "id": 100 }, "multipart": { "Id": 100, "Name": "furion", "File": "@C:\\Workspaces\\httptest.jpg" } } """)); ``` 文件字段值以 `@` 开头,支持本地绝对路径或网络 URL(如 `"@https://example.com/avatar.png"`)。同时支持 `@path;type=image/png` 和 `@path;filename=photo.jpg` 等扩展语法。 ### `URL` 编码表单 ```cs showLineNumbers {6-9} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://localhost:7044/HttpRemote/AddUrlForm", "method": "POST", "headers": { "Content-Type": "application/x-www-form-urlencoded" }, "data": "id=200&name=furion" } """)); ``` 或者通过对象自动序列化(需显式指定 `contentType`): ```cs showLineNumbers {6-10} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://localhost:7044/HttpRemote/AddUrlForm", "method": "POST", "data": { "id": 200, "name": "fu rion" }, "contentType": "application/x-www-form-urlencoded" } """)); ``` ### 单个文件上传 ```cs showLineNumbers {6-8} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://localhost:7044/HttpRemote/AddFile", "method": "POST", "multipart": { "file": "@C:\\Workspaces\\httptest.jpg" } } """)); ``` ### 多个文件上传 ```cs showLineNumbers {6-8} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://localhost:7044/HttpRemote/AddFiles", "method": "POST", "multipart": { "files": ["@C:\\Workspaces\\file1.jpg", "@C:\\Workspaces\\file2.jpg"] } } """)); ``` ### 发送原始字符串 ```cs showLineNumbers {9} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://localhost:7044/HttpRemote/RawString", "method": "POST", "headers": { "Content-Type": "application/json" }, "data": "\"This is a raw string\"" } """)); ``` ### 带有认证信息的请求 ```cs showLineNumbers {9-13} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://jsonplaceholder.typicode.com/posts", "method": "POST", "headers": { "Content-Type": "application/json" }, "auth": { "type": "basic", "username": "testuser", "password": "testpass" }, "data": { "title": "Test" } } """)); ``` ### 指定超时和 `HTTP` 版本 ```cs showLineNumbers {6-7} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://httpbin.org/delay/3", "method": "GET", "timeout": 5000, "httpVersion": "2.0" } """)); ``` ### 启用请求分析工具 ```cs showLineNumbers {6} var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://furion.net", "method": "GET", "profiler": true } """)); ``` > **注意事项** - 传入的 `JSON` 必须是有效的 `JSON` 对象(以 `{` 开头且以 `}` 结尾)。若传入数组、字符串或空值,将抛出 `ArgumentException("The provided JSON must be a valid JSON object.")`。 - 属性名不区分大小写,且允许尾随逗号。 - 未显式指定 `method` 时,若包含 `data` 或 `multipart`,则自动推断为 `POST`,否则为 `GET`。 - 文件上传路径需确保实际存在(本地文件)或网络可达(远程文件)。 ### 扩展自定义 `JSON` 提取器 与 `cURL` 解析器类似,`JSON` 解析也采用**可插拔提取器架构**。每个 `JSON` 字段由一个 `IHttpJsonExtractor` 实现处理。您可以新增自定义提取器来支持私有字段(如 `"customField"`),而无需修改框架源码。 #### 实现自定义提取器 最便捷的方式是继承 `HttpJsonExtractorBase` 抽象基类,它已封装好属性名与别名匹配逻辑: ```cs showLineNumbers {6,11,16,25-28} using HttpAgent; /// /// 自定义 json.customField 提取器 /// internal sealed class JsonCustomFieldExtractor : HttpJsonExtractorBase { /// /// 主属性名 /// protected override string PropertyName => "customField"; /// /// 可选别名列表 /// protected override string[]? Aliases => ["custom", "custom_field"]; /// /// 当匹配到属性时执行的具体操作 /// protected override void Extract(HttpRequestBuilder httpRequestBuilder, JsonNode node, HttpJsonParsingContext context) { // 此处根据节点值设置构建器 if (node is JsonValue jsonValue && jsonValue.TryGetValue(out var value)) { httpRequestBuilder.WithHeader("X-Custom-Field", value); } } } ``` 对于更复杂的场景,可直接实现 `IHttpJsonExtractor` 接口,手动遍历根 `JsonObject`。 #### 注册自定义提取器 在调用 `FromJson` 时通过配置委托注入: ```cs showLineNumbers {5,8} var builder = HttpRequestBuilder.FromJson(""" { "url":"http://example.com", "method":"GET", "customField":"hello" } """, options => options.AddExtractor(new JsonCustomFieldExtractor()) ); ``` 如需移除内置提取器,可使用 `options.RemoveExtractor()`。例如: ```cs showLineNumbers {6} var builder = HttpRequestBuilder.FromJson(""" { "url":"http://example.com" } """, options => options.RemoveExtractor() ); ``` #### 上下文对象说明 `HttpJsonParsingContext` 提供了对根 `JsonObject` 的安全访问方法: | 成员 | 说明 | | :----------------------------------- | :---------------------------------------------------- | | `RootObject` | 获取根 `JsonObject` | | `TryGetNode(propertyName, out node)` | 安全获取指定属性名的 `JsonNode`,不存在时返回 `false` | | `GetNode(propertyName)` | 获取指定属性名的 `JsonNode`,不存在时返回 `null` | | `ContainsProperty(propertyName)` | 检查根对象是否包含指定属性 | #### 参考实现 框架内置的所有提取器(如 `JsonMethodExtractor`、`JsonMultipartExtractor` 等)均基于相同的基类和接口构建,您可以在仓库中查看其源码作为参考:[查看内置 `JSON` 提取器源码](https://gitee.com/dotnetchina/HttpAgent/tree/master/src/HttpAgent/src/Parsers/JSON/Extractors) --- # 2.19 OData 接口请求 > 原文链接:https://http.furion.net/docs/quick-start/odata/ `OData`(`Open Data Protocol`)是一个基于 `REST` 的 `Web` 协议,通过 `URL` 参数就能像操作数据库一样对 `API` 进行查询:筛选、排序、分页、选择字段等。比如: - `$filter=Country eq 'China'` 过滤 - `$select=CustomerID,CompanyName` 指定字段 - `$top=10` 只取前 10 条 很多微软系服务(`Dynamics 365`、`Microsoft Graph`)都支持 `OData`。框架支持对 `OData` 接口的请求,以下为示例代码: ```cs showLineNumbers {2-7,10-11} var result = await httpRemoteService.GetAsStringAsync("https://您的主机地址/odata/Customers", builder => builder.WithQueryParameters(new Dicitionary { {"$top", "2"}, {"$select", "CustomerID,CompanyName"}, {"$format", "json"} })); // 解析 JSON 内容(推荐使用流变对象) var node = JsonNode.Parse(result!); var customers = node.Deserialize>(); ``` --- # 2.20 HTTP 请求与响应断言(Assert) > 原文链接:https://http.furion.net/docs/quick-start/assertions/ 在开发、测试中,经常需要对请求内容和响应结果进行验证,即“断言”。 系统将断言分为两类: - **请求断言**:在构建完 `HttpRequestMessage` 后、发送前执行,用于校验请求的 `URI`、方法、标头、主体等。 - **响应断言**:在收到 `HttpResponseMessage` 后执行,用于校验状态码、响应头、响应体、耗时等。 两类断言均通过 `UseAssertions()` 启用,并在 `Asserts(configure)` 中统一配置。若断言失败,会抛出 `HttpAssertionException`。 ```cs showLineNumbers {2-3,5-6,8-9} HttpRequestBuilder.Get("https://furion.net") .UseAssertions() .Asserts(ast => ast // 请求断言:发送前立即检查,失败不会发出请求 .RequestMethod(HttpMethod.Get) .RequestUri("https://furion.net/") // 响应断言:收到响应后检查 .ResponseStatusCode(200) .ResponseHeaderExists("encoding") ); ``` 其中,`ast` 参数为 `HttpAssertionBuilder` 类型,内置了以下常用断言方法(支持自定义扩展): ### 请求断言方法(发送前执行) - **`RequestUri(expectedUri)`**:断言请求 `URI` 等于指定字符串 - 失败时抛出:`Expected request URI to be '{expectedUri}', but found '{actual}'.` - **`RequestMethod(expectedMethod)`**:断言 `HTTP` 方法等于指定的 `HttpMethod` - 失败时抛出:`Expected request method to be {expectedMethod}, but found {actual}.` - **`RequestHeaderExists(name)`**:断言指定的请求标头存在(包括内容标头) - 失败时抛出:`Expected request header '{name}' to exist, but it was not found.` - **`RequestHeaderEquals(name, expectedValue)`**:断言请求标头的第一个值严格等于指定字符串(区分大小写) - 失败时抛出:`Expected request header '{name}' to be '{expectedValue}', but found '{actual}'.` - **`RequestHeaderContains(name, expectedValue)`**:断言请求标头任意值包含指定子字符串(不区分大小写) - 失败时抛出:`Expected request header '{name}' to contain '{expectedValue}', but the header was not found.` 或 `Expected request header '{name}' to contain '{expectedValue}', but actual values were: [{...}].` - **`RequestContentContains(expectedSubstring)`**:断言请求内容包含指定子字符串(不区分大小写) - 失败时抛出:`Expected request content to contain '{expectedSubstring}', but it was not found.` - **`RequestContentEquals(expected)`**:断言请求内容完全等于指定字符串 - 失败时抛出:`Expected request content to be '{expected}', but found '{actual}'.` - **`RequestSatisfies(assertion)`**:自定义请求断言(同步或异步),直接操作 `HttpRequestMessage` - 异步重载接受 `Func`。 ### 响应断言方法(收到响应后执行) - **`AddAssertion(assertion)`**:添加自定义断言委托(默认视为响应断言),如 `ast.AddAssertion(async context => await ...)`。 - **`ResponseStatusCode(statusCode)`**:断言响应状态码等于指定值(整数或 `HttpStatusCode`) - 失败时抛出:`Expected response status code to be {expected}, but found {actual}.` - **`ResponseStatusCodeIn(allowedStatusCodes)`**:断言状态码在允许列表中 - 失败时抛出:`Expected response status code to be one of [{string.Join(", ", allowedStatusCodes)}], but found {actual}.` - **`ResponseIsSuccessStatusCode()`**:断言请求成功(状态码为 `2xx`) - 失败时抛出:`Expected response to be successful (2xx status code), but found status code {(int)context.StatusCode}.` - **`ResponseContentContains(expectedSubstring)`**:断言响应内容包含指定子字符串(不区分大小写) - 失败时抛出:`Expected response content to contain '{expectedSubstring}', but it was not found.` - **`ResponseContentEquals(expected)`**:断言响应内容完全等于指定的字符串 - 失败时抛出:`Expected response content to be '{expected}', but found '{content}'.` - **`ResponseContentMatches(pattern)`**:断言响应内容与指定的正则表达式匹配 - 失败时抛出:`Expected response content to match regex '{pattern}', but it did not.` - **`ResponseContentNotEmpty()`**:断言响应内容不为空 - 失败时抛出:`Expected response content not to be empty.` - **`ResponseHeaderExists(name)`**:断言指定响应头存在(包括内容头) - 失败时抛出:`Expected response header '{name}' to exist, but it was not found.` - **`ResponseHeaderEquals(name, expectedValue)`**:断言响应头的第一个值严格等于指定字符串(区分大小写) - 失败时抛出:`Expected response header '{name}' to be '{expectedValue}', but found '{actualValue}'.` - **`ResponseHeaderContains(name, expectedValue)`**:断言响应头任意值包含指定子字符串(不区分大小写) - 失败时抛出:`Expected response header '{name}' to contain '{expectedValue}', but the header was not found.` 或 `Expected response header '{name}' to contain '{expectedValue}', but actual values were: [{string.Join(", ", values)}].` - **`ResponseHeaderNotExists(name)`**:断言指定的响应标头不存在(包括内容头) - 失败时抛出:`Expected response header '{name}' not to exist, but it was found.` - **`ResponseDurationUnder(maxMilliseconds)`**:断言请求耗时低于指定毫秒数 - 失败时抛出:`Expected response duration to be under {maxDuration.TotalMilliseconds:F2}ms, but it took {actualDuration.TotalMilliseconds:F2}ms.` - **`ResponseSatisfies(assertion)`**:自定义响应断言(同步或异步),直接操作 `HttpResponseMessage` - 异步重载接受 `Func`。 ### 自定义断言方法 除了内置方法,你还可以通过扩展方法为 `HttpAssertionBuilder` 添加自定义断言逻辑,以减少重复代码并提升可读性。例如,实现一个 `ResponseIsJson` 方法,用于验证响应内容是否为 `application/json` 类型: ```cs showLineNumbers {1,3,5,11-16} public static class HttpAssertionBuilderExtensions { public static HttpAssertionBuilder ResponseIsJson(this HttpAssertionBuilder httpAssertionBuilder) { return httpAssertionBuilder.AddAssertion(async context => { var contentType = context.ResponseMessage?.Content?.Headers.ContentType?.MediaType; const string jsonMediaType = "application/json"; // 允许 "application/json" 或 "application/json; charset=utf-8" 等 if (string.IsNullOrEmpty(contentType) || !contentType.StartsWith(jsonMediaType, StringComparison.OrdinalIgnoreCase)) { await HttpAssertionException.ThrowAsync( $"Expected response Content-Type to be '{jsonMediaType}' (or a subtype with parameters), but found '{contentType}'."); } }); } } ``` 使用自定义方法示例: ```cs showLineNumbers {2-3} HttpRequestBuilder.Get("https://furion.net") .UseAssertions() .Asserts(ast => ast.ResponseIsJson().ResponseStatusCode(200)); // 支持链式调用 ``` 借助 `C#` 扩展方法,你可以灵活扩展 `HttpAssertionBuilder` 的功能,提升代码的可维护性和复用性。 --- # 2.21 JSON 响应反序列化包装器 > 原文链接:https://http.furion.net/docs/quick-start/json-wrapper/ 在与第三方 `API` 进行 `HTTP` 远程通信时,通常会返回统一结构的 `JSON` 响应,例如 `ApiResult` 类型,其中实际数据存放在 `Data` 属性中: ```cs showLineNumbers {1,4} public class ApiResult { public bool Success { get; set; } public T? Data { get; set; } // 实际返回数据 } ``` 在未启用 `JSON` 响应反序列化包装器功能时,每次调用都需要显式指定 `ApiResult` 类型: ```cs showLineNumbers {1} var content = await httpRemoteService.SendAsAsync>( HttpRequestBuilder.Get("https://furion.net")); ``` ### 启用方式 #### 1. 单次启用 为简化调用流程,可配置 `JSON` 响应反序列化包装器,使其自动提取 `Data` 属性内容: ```cs showLineNumbers {2-3,5} // 配置默认 HTTP 客户端 services.AddHttpClient(string.Empty) .ConfigureOptions(options => { options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data)); }); ``` 配置完成后,通过调用 `UseJsonResponseWrapper()` 启用该功能,之后只需指定目标数据类型,无需重复声明 `ApiResult`: ```cs showLineNumbers {1-2} var content = await httpRemoteService.SendAsAsync( HttpRequestBuilder.Get("https://furion.net").UseJsonResponseWrapper()); ``` 框架将在运行时自动创建 `ApiResult` 实例,并返回其 `Data` 属性的值。 #### 2. 全局启用(默认对所有请求生效) 也可全局启用 `JSON` 响应反序列化包装器功能,只需设置 `UseJsonResponseWrapper` 为 `true`: ```cs showLineNumbers {2-3,6} // 配置默认 HTTP 客户端 services.AddHttpClient(string.Empty) .ConfigureOptions(options => { options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data)); options.UseJsonResponseWrapper = true; }); ``` 全局启用后,所有请求默认使用包装功能: ```cs showLineNumbers {2} var content = await httpRemoteService.SendAsAsync( HttpRequestBuilder.Get("https://furion.net")); // 无需显式调用 UseJsonResponseWrapper() ``` #### 3. 单次禁用(覆盖全局设置) 若需对特定请求禁用该功能,可调用以下方法: ```cs showLineNumbers {1,2} var content = await httpRemoteService.SendAsAsync>( HttpRequestBuilder.Get("https://furion.net").UseJsonResponseWrapper()); ``` 默认情况下,未调用 `UseJsonResponseWrapper()` 表示未启用该功能,此时需传入完整的响应类型,除非全局配置了 `UseJsonResponseWrapper = true`。 ### 自定义结果处理(`ResultHandler`) 有时除了提取 `Data`,还需对响应做额外校验或转换。可通过 `ResultHandler` 回调实现: ```cs showLineNumbers {7,12,16,19} // 配置默认 HTTP 客户端 services.AddHttpClient(string.Empty) .ConfigureOptions(options => { options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data)) { ResultHandler = context => { if (context.Instance is { } instance) { // 可访问包装类型实例,获取其任意属性 var success = context.GetPropertyValue(nameof(ApiResult<>.Success)); } // 例如确保请求成功 context.ResponseMessage.EnsureSuccessStatusCode(); // 返回最终的目标结果(即 Data 的值) return context.Result; } }; }); ``` 通过 `ResultHandler`,您可以在返回最终数据前执行任何自定义逻辑(如校验、转换或异常处理),使请求处理更加灵活。 `context` 参数的类型为 `JsonResponseWrapperContext`,包含以下属性和方法: - **属性**: - `Instance`:包装类型的具体实例(如 `ApiResult`,`object?` 类型)。 - `Result`:目标结果(即 `Data` 的值,`object?` 类型)。 - `ResponseMessage`:响应消息(`HttpResponseMessage` 类型)。 - **方法**: - `GetPropertyValue(propertyName)`:获取包装类型的具体类型(即 `Instance`) 指定属性值。 --- # 2.22 Access Token 自动管理 > 原文链接:https://http.furion.net/docs/quick-start/access-token/ 在与第三方服务(如微信公众号、企业微信等)对接时,通常需要先获取 `Access Token`,并在后续请求中携带该 `Access Token` 才能正常调用接口。`Access Token` 具有有效期(通常为两小时),过期后需重新获取并更新。 为简化这一流程,框架内置了 `Access Token` 自动管理机制:当 `Access Token` 不存在或已过期时,自动获取新 `Access Token`,并按配置将其注入到请求的 `Header`、`Query`、`Cookie` 等位置,同时支持在请求因 `Access Token` 失效(如返回 `401`)时自动重试。 ### `HttpAccessToken` 模型 `HttpAccessToken` 表示一个 `Access Token` 信息,包含以下构造函数、属性和方法: - **构造函数**: - `new(value, expiresAt)`:传入 `Access Token` 和绝对过期时间(`UTC` 时间)。 - `new(jwtToken)`:传入 `JWT Token` 字符串。 - **属性**: - `Value`:`Access Token` 值(`string` 类型)。 - `ExpiresAt`:`Access Token` 的绝对过期时间(`DateTimeOffset` 类型)。 - `Scheme`:`HTTP` 认证方案(`string?` 类型)。 - `RefreshToken`:刷新令牌(`string?` 类型),内部基于 `Items["refresh_token"]` 提供便捷访问。 - `Items`:共享数据字典(`IDictionary` 类型),用于存储与 `Access Token` 相关的自定义数据(如 `refresh_token`、用户标识等)。 - **静态属性**: - `None`:表示没有可用的 `Access Token`(`HttpAccessToken?` 类型)。 - **方法**: - `IsExpired()` 检查 `Access Token` 是否过期。 - `SetExpiresAt(expiresAt)` 设置 `Access Token` 的绝对过期时间。 ### 启用步骤 #### 1. 实现 `IHttpAccessTokenProvider` 接口 该接口负责定义如何获取和刷新 `Access Token`。所有方法均接收 `HttpAccessTokenContext` 参数,可通过 `context.Items` 获取请求时传入的自定义数据(如用户名、密码)。示例: ```cs showLineNumbers {1,4,7-8,10} public sealed class WeiXinHttpAccessTokenProvider(IHttpRemoteService httpRemoteService) : IHttpAccessTokenProvider { /// public async Task GetAsync(HttpAccessTokenContext context, CancellationToken cancellationToken) { // 请求微信服务器获取 Access Token var weixinToken = await httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://weixin.qq.com/login") .WithoutTokenManagement(), cancellationToken); // 跳过 Token 管理,避免递归调用(声明式请求使用 [SuppressTokenManagement]) return new HttpAccessToken(weixinToken.Token, DateTimeOffset.Parse(weixinToken.ExpiresAt)); } } ``` > **默认刷新行为** 若未重写 `RefreshAsync`,框架将直接调用 `GetAsync` 完成刷新。如果需要区分首次获取与刷新(如使用不同的接口或基于 `refresh_token` 刷新),可重写 `RefreshAsync` 方法。 #### 2. 为指定 `HttpClient` 客户端启用 `Access Token` 自动管理: ```cs showLineNumbers {2-3,5,9-10,12} // 配置默认客户端 services.AddHttpClient(string.Empty) .ConfigureOptions((options, serviceProvider) => { options.AccessTokenProvider = ActivatorUtilities.CreateInstance(serviceProvider); }); // 配置特定客户端 services.AddHttpClient("weixin") .ConfigureOptions((options, serviceProvider) => { options.AccessTokenProvider = ActivatorUtilities.CreateInstance(serviceProvider); }); ``` 完成上述配置后,该客户端发出的所有请求都将自动管理 `Access Token`。 ### 配置注入位置(`Header`、`Query`、`Cookie` 等) 默认情况下,`Access Token` 会以 `Authorization` 请求头形式发送。开发者可通过设置 `HttpAccessToken.Scheme` 属性指定认证方案(如 `Bearer`): ```cs showLineNumbers {11} public sealed class WeiXinHttpAccessTokenProvider(IHttpRemoteService httpRemoteService) : IHttpAccessTokenProvider { /// public async Task GetAsync(HttpAccessTokenContext context, CancellationToken cancellationToken) { var weixinToken = await httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://weixin.qq.com/login") .WithoutTokenManagement(), cancellationToken); // 跳过 Token 管理,避免递归调用(声明式请求使用 [SuppressTokenManagement]) return new HttpAccessToken(weixinToken.Token, DateTimeOffset.Parse(weixinToken.ExpiresAt)) { Scheme = "Bearer" // 指定 Bearer 方案 }; } } ``` 若需更精细地控制 `Access Token` 的携带方式(如放入 `URL` 参数或 `Cookie`),可实现 `IHttpAccessTokenConfigurator` 接口。推荐直接在 `IHttpAccessTokenProvider` 实现类上继承该接口,既减少类型定义,又便于集中管理: ```cs showLineNumbers {2,14,17-20} public sealed class WeiXinHttpAccessTokenProvider(IHttpRemoteService httpRemoteService) : IHttpAccessTokenProvider, IHttpAccessTokenConfigurator { /// public async Task GetAsync(HttpAccessTokenContext context, CancellationToken cancellationToken) { var weixinToken = await httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://weixin.qq.com/login") .WithoutTokenManagement(), cancellationToken); // 跳过 Token 管理,避免递归调用(声明式请求使用 [SuppressTokenManagement]) return new HttpAccessToken(weixinToken.Token, DateTimeOffset.Parse(weixinToken.ExpiresAt)); } /// public void Configure(HttpRequestBuilder httpRequestBuilder, HttpAccessToken httpAccessToken) { // 自定义 Token 注入方式 httpRequestBuilder.AddBearerAuthentication(httpAccessToken.Value); // Bearer 认证 // httpRequestBuilder.WithQueryParameter("access_token", httpAccessToken.Value); // URL 参数 // httpRequestBuilder.WithCookie("access_token", httpAccessToken.Value); // Cookie // 其他方式... } } ``` 当然,也支持独立实现 `IHttpAccessTokenConfigurator`: ```cs showLineNumbers {1,4-8} public sealed class CustomHttpAccessTokenConfigurator : IHttpAccessTokenConfigurator { /// public void Configure(HttpRequestBuilder httpRequestBuilder, HttpAccessToken httpAccessToken) { // 将 Access Token 放入自定义请求头(建议添加 replace: true) httpRequestBuilder.WithHeader("X-Custom-Token", httpAccessToken.Value, replace: true); } } ``` 然后在服务容器中注册该实现: ```cs showLineNumbers services.TryAddSingleton(); ``` ### 自定义 `Access Token` 刷新触发条件 默认情况下,当接收到 `HTTP 401 Unauthorized` 响应时,框架会强制刷新 `Access Token` 并重试请求。若您的 `API` 通过其他状态码(如 `403`)或响应内容内容标识 `Access Token` 失效,可重写 `IHttpAccessTokenProvider` 接口的 `ShouldRefreshAsync` 方法: ```cs showLineNumbers {13,16-17,20-21} public sealed class WeiXinHttpAccessTokenProvider(IHttpRemoteService httpRemoteService) : IHttpAccessTokenProvider { /// public async Task GetAsync(HttpAccessTokenContext context, CancellationToken cancellationToken) { var weixinToken = await httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://weixin.qq.com/login") .WithoutTokenManagement(), cancellationToken); // 跳过 Token 管理,避免递归调用(声明式请求使用 [SuppressTokenManagement]) return new HttpAccessToken(weixinToken.Token, DateTimeOffset.Parse(weixinToken.ExpiresAt)); } /// public async Task ShouldRefreshAsync(HttpAccessTokenContext context, HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken) { // 示例 1:状态码为 401 或 403 时刷新 // return httpResponseMessage.StatusCode == HttpStatusCode.Unauthorized // || httpResponseMessage.StatusCode == HttpStatusCode.Forbidden; // 示例 2:解析响应内容 JSON 中的错误码 var content = await httpResponseMessage.Content.ReadAsStringAsync(cancellationToken); return content?.Contains("\"errorCode\":\"TOKEN_EXPIRED\"") == true; } } ``` 方法返回 `true` 时将强制刷新 `Access Token` 并重试请求。**注意,重试仅执行一次,避免无限循环。** ### 传递自定义数据到 `IHttpAccessTokenProvider` 如果需要在获取 `Access Token` 时传入动态参数(如用户名、密码等),可使用 `HttpRequestBuilder.WithAccessTokenData` 方法。这些数据会被自动复制到 `HttpAccessTokenContext.Items` 中,供 `GetAsync` 等方法使用。 ```cs showLineNumbers {4-5} var result = await httpRemoteService.SendAsync( HttpRequestBuilder.Get("https://api.furion.net/data") .SetHttpClientName("myapi") // 可选 .WithAccessTokenData("username", "admin") .WithAccessTokenData("password", "123456")); ``` ### 手动设置 `Access Token`(`SetAsync`) 除了让框架自动调用 `GetAsync` 获取 `Access Token`,还支持在登录成功后手动设置 `Access Token`,然后重写 `RefreshAsync` 实现基于 `RefreshToken` 的刷新逻辑。 通过注入 `IHttpAccessTokenManager` 接口,调用 `SetAsync` 方法将 Token 存入框架缓存: ```cs showLineNumbers {3} // 登录成功后,手动设置 Access Token var token = new HttpAccessToken(accessToken, expiresAt) { RefreshToken = refreshToken }; await httpAccessTokenManager.SetAsync("myapi", token); ``` 此后框架将从缓存中读取 `Access Token`,过期时自动调用您重写的 `RefreshAsync` 进行刷新。此时 `GetAsync` 可以返回 `null` 或抛出异常(不会被调用)。 一个典型的手动刷新实现如下: ```cs showLineNumbers {1,4,6,10-13,19,22} public sealed class ManualTokenProvider : IHttpAccessTokenProvider, IHttpAccessTokenConfigurator { public Task GetAsync(HttpAccessTokenContext context, CancellationToken cancellationToken) => Task.FromResult(HttpAccessToken.None); public async Task RefreshAsync(HttpAccessTokenContext context, HttpAccessToken? currentToken, CancellationToken cancellationToken) { // 使用当前 Token 中的 RefreshToken 去获取新 Token var refreshToken = currentToken?.RefreshToken; var response = await httpRemoteService.SendAsync( HttpRequestBuilder.Post("https://auth.furion.net/refresh") .WithHeader("X-Refresh-Token", refreshToken, replace: true) .WithoutTokenManagement(), cancellationToken); // 跳过 Token 管理,避免递归调用(声明式请求使用 [SuppressTokenManagement]) return new HttpAccessToken(response.Headers.GetValues("X-Access-Token").First(), DateTimeOffset.UtcNow.AddHours(1)) { RefreshToken = response.Headers.GetValues("X-Refresh-Token").First() }; } public void Configure(HttpRequestBuilder httpRequestBuilder, HttpAccessToken httpAccessToken) { // 将 Access Token 放入自定义请求头(建议添加 replace: true) httpRequestBuilder.WithHeader("Authorization", $"Bearer {httpAccessToken.Value}", replace: true); } } ``` --- ### 内置 `FurionAccessTokenProvider`(Furion 框架专用) 如果您的服务端使用 `Furion` 框架的 `JWT` 令牌机制,可直接使用内置的 `FurionAccessTokenProvider`。该提供器自动处理 `access-token` 和 `x-access-token` 响应头,实现无感滚动刷新。 **1. 注册提供器** ```cs showLineNumbers {4} services.AddHttpClient("furion_api") .ConfigureOptions((options, serviceProvider) => { options.AccessTokenProvider = ActivatorUtilities.CreateInstance(serviceProvider); }); ``` 如果希望手动控制实例化,也可以显式传入依赖: ```cs showLineNumbers {4} services.AddHttpClient("furion_api") .ConfigureOptions((options, serviceProvider) => { options.AccessTokenProvider = new FurionAccessTokenProvider(serviceProvider.GetRequiredService()); }); ``` **2. 登录成功后手动设置初始 `Access Token`** ```cs showLineNumbers {2} var token = new HttpAccessToken(initialAccessToken, expiresAt) { RefreshToken = initialRefreshToken }; await httpAccessTokenManager.SetAsync("furion_api", token); ``` 之后每次请求时,`FurionAccessTokenProvider` 会自动携带 `Authorization: Bearer {token}`,并在 `Access Token` 过期时附加 `X-Authorization: Bearer {refresh_token}`;服务端返回的新 `Access Token` 会通过 `access-token` 和 `x-access-token` 响应头自动更新缓存,无需任何额外代码。 > 注意:`FurionAccessTokenProvider` 不会根据 `HTTP 401` 触发刷新,因为它的刷新逻辑完全由响应头驱动。**首次使用前务必调用 `SetAsync` 设置初始 `Access Token`。** ### 内置 `WeChatAccessTokenProvider`(微信开放平台专用) 若您的项目需要调用微信公众号/小程序等微信服务端接口,可直接使用内置的 `WeChatAccessTokenProvider`。该提供器自动管理 `access_token` 的获取、缓存和刷新,并支持根据微信错误码自动重试。 **1. 注册提供器** 微信 `access_token` 的获取需要 `appId` 和 `appSecret`,使用 `ActivatorUtilities.CreateInstance` 传入: ```cs showLineNumbers {4-5} services.AddHttpClient("wechat_api") .ConfigureOptions((options, serviceProvider) => { options.AccessTokenProvider = ActivatorUtilities.CreateInstance( serviceProvider, "YourAppId", "YourAppSecret"); }); ``` **2. 自动管理流程** [https://developers.weixin.qq.com/miniprogram/dev/server/API/mp-access-token/api_getaccesstoken.html](https://developers.weixin.qq.com/miniprogram/dev/server/API/mp-access-token/api_getaccesstoken.html) - **首次请求**:提供器自动调用微信 `/cgi-bin/token` 接口获取 `access_token`,并缓存到内存(默认提前 `5` 秒过期,避免因网络延迟导致使用失效 `access_token`)。 - **自动注入**:`access_token` 会作为查询参数 `?access_token=xxx` 附加到请求 `URL`。 - **过期刷新**:当收到微信错误码 `40001`(无效凭证)、`40014`(无效 `access_token`)、`42001`(`access_token` 过期)时,框架会自动获取新 `access_token` 并重试请求。 - **无需手动操作**:整个生命周期由框架自动管理,无需调用 `SetAsync` 手动设置初始 `access_token`。 **3. 错误重试说明** `WeChatAccessTokenProvider` 已重写 `ShouldRefreshAsync`,会同时检查 `HTTP` 状态码(`401/403`)和响应 `JSON` 中的 `errcode`。只有与 `access_token` 失效相关的错误码才会触发刷新,避免因临时网络问题或微信系统繁忙(如 `-1`)导致无意义的重试。 --- ### 多节点集群部署 当服务部署在多节点集群环境中时,默认的内存缓存会导致各节点独立管理 `Access Token`。一个节点获取或刷新 `Token` 后,其他节点的 `Token` 将随之失效,从而引发反复获取和刷新,甚至触发 `API` 限流。 为了解决这一问题,可以将 `Access Token` 的存储从内存迁移到分布式缓存(如 `Redis`)。只需实现 `IHttpAccessTokenManager` 接口并替换默认服务即可: ```cs showLineNumbers {1} public class RedisAccessTokenManager : IHttpAccessTokenManager { // 实现接口方法,将 Access Token 存入 Redis 等分布式缓存 } ``` 然后在服务注册时替换默认实现: ```cs showLineNumbers services.Replace(ServiceDescriptor.Singleton()); ``` 替换后,所有节点将共享同一份 `Access Token`,彻底避免多节点间的 `Access Token` 冲突和重复刷新问题。 --- 通过上述配置,框架将自动处理 `Access Token` 的获取、刷新及注入,开发者无需关心 `Access Token` 过期时间、失效重试(如 `401` 自动重发)等细节,显著降低与第三方接口集成的复杂度。 --- # 2.23 接口调用配额限制 > 原文链接:https://http.furion.net/docs/quick-start/quota/ 在与第三方 `API`(如[微信](https://developers.weixin.qq.com/doc/service/guide/dev/api/limit.html)、支付网关等)集成时,通常需要遵守其每日/每月调用次数限制。为避免因超限导致业务中断或封禁,框架提供了灵活的**接口调用配额限制**功能,支持按天、按周、按月、永久总次数等策略,并允许自定义策略。 启用配额限制后,每次请求会根据配置的策略检查当前计数,若达到上限将**直接中断请求**并抛出 `InvalidOperationException` 异常(不会实际发送 `HTTP` 请求)。通过 `HttpRequestBuilder.SetQuotaKey(key)` 为每个请求指定配额键,并与 `HttpClientOptions` 中的配额配置关联。 ### 配置方式 #### 1. 注册默认配额策略 在 `Startup.cs` 或 `Program.cs` 文件中注册默认配额策略: ```cs showLineNumbers {3} services.AddHttpRemote(builder => { builder.AddDefaultQuotaStrategies(); // 注册 daily、weekly、monthly、lifetime 四种策略 }); ``` > **默认配额策略说明** 如果仅使用自定义配额策略,此步骤可跳过,但需手动注册自定义配额策略(见后文)。 #### 2. 为指定 `HttpClient` 客户端配置配额限制 在注册 `HttpClient` 时,通过 `ConfigureOptions` 设置 `QuotaLimits` 字典,将配额键与对应的限制策略关联: ```cs showLineNumbers {2-3,5-11,15-16,18-24} // 配置默认客户端 services.AddHttpClient(string.Empty) .ConfigureOptions(options => // 或使用重载:.ConfigureOptions((options, serviceProvider) => { options.QuotaLimits = new Dictionary { ["wechat/accesstoken"] = new HttpQuotaLimit("daily", 2000), // 每日限额 ["wechat/menu_create"] = new HttpQuotaLimit("weekly", 1000), // 每周限额 ["wechat/upload_media"] = new HttpQuotaLimit("monthly", 50000), // 每月限额 ["wechat/lifetime_stat"] = new HttpQuotaLimit("lifetime", 10000) // 生命周期总额 }; }); // 配置特定客户端 services.AddHttpClient("weixin") .ConfigureOptions(options => // 或使用重载:.ConfigureOptions((options, serviceProvider) => { options.QuotaLimits = new Dictionary { ["wechat/accesstoken"] = new HttpQuotaLimit("daily", 2000), // 每日限额 ["wechat/menu_create"] = new HttpQuotaLimit("weekly", 1000), // 每周限额 ["wechat/upload_media"] = new HttpQuotaLimit("monthly", 50000), // 每月限额 ["wechat/lifetime_stat"] = new HttpQuotaLimit("lifetime", 10000) // 生命周期总额 }; }); ``` > **推荐使用配置文件管理配额** 当配额条目较多时,建议将配额配置放在 `appsettings.json` 中,避免硬编码。 **`appsettings.json` 示例:** ```json showLineNumbers {4-6} { "HttpQuotas": { "weixin": { "wechat/accesstoken": { "MaxCount": 2000, "Strategy": "daily" }, "wechat/menu_create": { "MaxCount": 1000, "Strategy": "weekly" }, "wechat/upload_media": { "MaxCount": 50000, "Strategy": "monthly" } } } } ``` **加载并绑定:** ```cs showLineNumbers {2,5-6,8} services.AddHttpClient("weixin") .ConfigureOptions((options, serviceProvider) => { // 读取 HttpQuotas:weixin 配置节点并转换为 Dictionary 类型 var configuration = serviceProvider.GetRequiredService(); var quotas = configuration.GetSection("HttpQuotas:weixin").Get>(); options.QuotaLimits = quotas; }); ``` #### 3. 为请求指定配额键 在发送请求时,使用 `SetQuotaKey` 关联到对应的配额配置,使该请求受对应规则的约束: ```cs showLineNumbers {3-4} var response = await httpRemoteService.SendAsync( HttpRequestBuilder.Get("https://api.weixin.qq.com/cgi-bin/token") .SetHttpClientName("weixin") .SetQuotaKey("wechat/accesstoken")); // 此键限制为每日 2000 次 ``` > **配置键 `QuotaKey` 说明** - 如果未指定配额键,或指定的键在 `QuotaLimits` 中不存在,则不会进行配额检查,请求正常发送。 - 配额键可以是任意自定义字符串,推荐使用与接口路径相关的名称,便于识别和管理。 ### 内置配额策略 框架内置了四种常用策略,通过 `Strategy` 属性指定(大小写不敏感): | 策略名称 | 说明 | 窗口重置规则(基于 `UTC` 时间) | | ---------- | ---------------------------- | ------------------------------- | | `daily` | 每日限额 | 每天 `00:00:00` 重置 | | `weekly` | 每周限额 | 每周一 `00:00:00` 重置 | | `monthly` | 每月限额 | 每月第一天 `00:00:00` 重置 | | `lifetime` | 生命周期总额(不按时间重置) | 永不重置,达到上限后永久拒绝 | 例如,配置 `Strategy = "daily"`,`MaxCount = 2000` 表示每天最多调用 `2000` 次。配置 `Strategy = "lifetime"`,`MaxCount = 10000` 表示该配额键在整个应用生命周期内最多调用 `10000` 次,不会随时间重置。 ### 自定义配额策略 您可以实现 `IHttpQuotaStrategy` 接口来创建任意重置规则的策略(例如按小时、按自定义时间窗口、基于滑动窗口等)。 **1. 定义策略类** ```cs showLineNumbers {1,4,7,13-16,20-21,23} public sealed class HourlyQuotaStrategy : IHttpQuotaStrategy { /// public string Name => "hourly"; // 策略的唯一名称 /// public bool TryAcquire(HttpQuotaCounter quotaCounter, int maxCount, out int current) { // 以 UTC 当前小时作为窗口标识(格式:yyyy-MM-dd HH) var hourKey = DateTime.UtcNow.ToString("yyyy-MM-dd HH"); // 如果窗口标识改变,说明进入新的小时,重置计数 if (quotaCounter.WindowKey != hourKey) { quotaCounter.Count = 0; quotaCounter.WindowKey = hourKey; } // 递增计数 quotaCounter.Count++; current = quotaCounter.Count; return current <= maxCount; } } ``` **2. 注册自定义配额策略** 在 `Startup.cs` 或 `Program.cs` 文件中注册自定义配额策略: ```cs showLineNumbers {3} services.AddHttpRemote(builder => { builder.AddQuotaStrategy(); }); ``` 注册后,即可在 `QuotaLimits` 中使用 `Strategy = "hourly"`: ```cs showLineNumbers {3} options.QuotaLimits = new Dictionary { ["some/high_freq_api"] = new HttpQuotaLimit("hourly", 100) // 每小时最多调用 100 次 }; ``` ### 多节点集群部署 默认情况下,配额管理器 `HttpQuotaManager` 基于内存缓存维护计数器,适用于单节点或单实例部署。在多节点集群环境中,每个节点各自维护独立的计数状态,导致整体配额限制失效(例如某接口全局限额 `2000` 次/天,各节点可能分别调用 `2000` 次而未互相感知)。为使配额在所有节点间准确共享,可将计数器存储迁移至分布式缓存(如 `Redis`)。 只需实现 `IHttpQuotaManager` 接口,将计数和窗口检查逻辑依托于分布式原子操作,然后替换默认服务即可: ```cs showLineNumbers {1} public class RedisHttpQuotaManager : IHttpQuotaManager { // 实现接口方法,基于 Redis 实现原子递增、窗口重置和超限检查 } ``` 在服务注册时替换默认实现: ```cs showLineNumbers services.Replace(ServiceDescriptor.Singleton()); ``` 替换后,所有节点共享同一份配额计数器,确保集群全局的调用次数始终在配置的限制之内。自定义实现时,务必保证窗口重置与计数递增的原子性,避免并发导致超限。 --- 通过以上机制,您可以轻松地为不同接口配置差异化的调用限额,有效防止因过度调用而触发的第三方 `API` 限制或费用超额问题。 --- # 2.24 服务发现(ServiceDiscovery) > 原文链接:https://http.furion.net/docs/quick-start/service-discovery/ 服务发现是一种允许开发人员使用逻辑名称而非物理地址(如 `IP` 地址和端口)来引用外部服务的机制。例如,我们可以使用 `furion` 来代替 `https://furion.net`。**这种方式的好处在于,可以在运行时通过配置修改服务地址,而无需更改程序代码,同时还能实现自动选择服务终结点以实现负载均衡**。服务发现在微服务架构中尤为常见。 要在 `HTTP` 远程请求中使用服务发现,可以按照以下步骤进行配置: ### 1. 安装 `Microsoft.Extensions.ServiceDiscovery` 包 ```cs showLineNumbers dotnet add package Microsoft.Extensions.ServiceDiscovery ``` ### 2. 配置并启用 `ServiceDiscovery` 服务 在 `Startup.cs` 或 `Program.cs` 文件中注册并配置 `ServiceDiscovery` 服务: ```cs showLineNumbers {1,3,5} services.AddServiceDiscovery(); services.AddHttpRemote() .ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.AddServiceDiscovery(); }); ``` ### 3. 在配置文件中添加服务终结点 在 `appsettings.json` 文件中配置服务终结点。以下示例配置了 `furion` 和 `weixin` 两个服务,每个服务都包含多个终结点。每次发送请求时,系统会自动选择一个终结点。 ```json showLineNumbers {2,3,6} { "Services": { "furion": { "https": ["localhost:5001", "furion.net"] }, "weixin": { "https": ["localhost:8080", "weixin.qq.com"] } } } ``` ### 4. 配置 `HttpClient` 客户端的 `BaseAddress` 接下来,配置 `HttpClient` 客户端的 `BaseAddress`,以便在请求时使用逻辑名称而非具体的物理地址。 ```cs showLineNumbers {2,4,8,10} // 配置默认客户端 services.AddHttpClient(string.Empty, client => { client.BaseAddress = new Uri("https://furion"); }); // 配置特定客户端,如:"weixin" services.AddHttpClient("weixin", client => { client.BaseAddress = new Uri("https://weixin"); }); ``` ### 5. 发送 `HTTP` 远程请求 最后,使用配置好的 `HttpClient` 发送远程请求: ```cs showLineNumbers {2,5} // 发送默认客户端请求 await httpRemoteService.GetAsStringAsync("docs"); // 请求地址为:https://localhost:5001/docs 或 https://furion.net/docs // 发送 "weixin" 客户端请求 await httpRemoteService.GetAsStringAsync("userinfo", builder => builder.SetHttpClientName("weixin")); // 请求地址为:https://localhost:8080/userinfo 或 https://weixin.qq.com/userinfo ``` 通过以上步骤,您可以在 `.NET` 应用中轻松实现服务发现功能,从而简化服务调用并提高系统的灵活性和可扩展性。想了解更多关于 `.NET` 中的服务发现的内容,可以查阅 [Microsoft 官方文档](https://learn.microsoft.com/zh-cn/dotnet/core/extensions/service-discovery)。 --- # 2.25 HttpRemoteResult 返回类型 > 原文链接:https://http.furion.net/docs/quick-start/http-remote-result/ `HttpRemoteResult` 是一个泛型类型,专门用于 `HTTP` 远程请求模块中的响应内容。泛型参数 `TResult` 代表最终需要转换成的数据类型,除了支持常见的 `HTTP` 响应类型如 `string`、`byte[]`、`Stream`、`HttpResponseMessage`、`IAsyncEnumerable` 和 `IActionResult`,还支持自定义类型和框架内置的 `VoidContent` 类型。该类型封装了常用的 `HTTP` 响应信息和请求耗时等功能。 在 `HTTP` 远程请求模块中,所有默认的不包含 `As` 关键字的泛型请求方法返回值均为 `HttpRemoteResult` 类型。以下是通过不同方式获取 `HttpRemoteResult` 类型返回值的示例: ```cs showLineNumbers {2,5} // 请求谓词方式 using var httpResult = await httpRemoteService.GetAsync("https://furion.net/"); // 构建器方式 using var httpResult = await httpRemoteService.SendAsync(HttpRequestBuilder.Get("https://furion.net/")); ``` `HttpRemoteResult` 包含以下属性和方法: - **属性**: - `ResponseMessage`:响应消息(`HttpResponseMessage` 类型)。 - `ContentType`:内容类型(`string` 类型)。 - `CharSet`:字符集(`string` 类型)。 - `ContentEncoding`:内容编码(`ICollection` 类型)。 - `ContentLength`:内容大小(`long` 类型)。 - `Server`:原始响应标头 `Server`(`HttpHeaderValueCollection` 类型)。 - `RawSetCookies`:原始响应标头 `Set-Cookie` 集合(`List` 类型)。 - `SetCookies`:响应 `Cookie` 集合(`IList` 类型)。 - `StatusCode`:响应状态码(`HttpStatusCode` 类型)。 - `IsSuccessStatusCode`:是否请求成功(`bool` 类型)。 - `Result`:目标数据(`TResult` 泛型类型)。 - `RequestDuration`:请求耗时(毫秒)(`long` 类型)。 - `Headers`:响应标头(`HttpResponseHeaders` 类型)。 - `ContentHeaders`:响应内容标头(`HttpContentHeaders` 类型)。 - `Version`:`HTTP` 版本(`Version` 类型)。 - `HttpClientName`:`HttpClient` 实例的配置名称(`string?` 类型)。 - **方法**: - `ToString()`:输出带缩进的详细请求和响应信息字符串。 > **返回值类型说明** 默认情况下,当返回值类型不是 `string`、`byte[]`、`Stream`、`HttpResponseMessage`、`VoidContent`、`IAsyncEnumerable` 和 `IActionResult` 时,其他类型将使用 `System.Text.Json` 进行反序列化处理。 如果需要更改此行为,可以在后续章节中了解如何实现 `IHttpContentConverter` 内容转换器接口进行自定义。 在最新版本中,框架为 `HttpRemoteResult` 类型引入了对[解构函数](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/deconstruction)的支持,通过解构表达式简化对象解析过程,使得获取关键属性值变得更加便捷。以下是示例代码: ```cs showLineNumbers // 解构表达式用于提取必需的属性值 var (result, response) = await httpRemoteService.GetAsync("https://furion.net/"); // 可调用 ThrowIfNull()/OrDefault() 解决空引用警告问题 var (result, response, isSuccess) = await httpRemoteService.GetAsync("https://furion.net/"); // 可调用 ThrowIfNull()/OrDefault() 解决空引用警告问题 var (result, response, isSuccess, statusCode) = await httpRemoteService.GetAsync("https://furion.net/"); // 可调用 ThrowIfNull()/OrDefault() 解决空引用警告问题 ``` 在这几个例子中,`result` 是 `TResult` 类型,`response` 是 `HttpResponseMessage` 类型,`isSuccess` 是 `bool` 类型,而 `statusCode` 则是 `HttpStatusCode` 类型。 通过使用解构表达式,不仅提升了代码的可读性,也让开发过程更加高效。这种改进允许开发者直接访问所需的数据,减少了手动获取各个属性值的步骤,从而使代码更简洁、直观。 --- 此外,`HttpRemoteResult` 类型还内置了一个 `ToString()` 方法,该方法能够以缩进格式清晰地打印出请求标头和响应标头的详细信息,如下所示: ```cs showLineNumbers Console.WriteLine(httpResult.ToString()); // 或使用 Console.WriteLine(httpResult); ``` 终端控制台输出如下: ```bash showLineNumbers Request Headers: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0 traceparent: 00-602c9070b85da9bd73fc1eac36fdb3cb-14dded89e0f5266b-00 General: Request URL: https://furion.net/ Request Method: GET Status Code: 200 OK HTTP Version: 1.1 HTTP Content: Content Type: HttpClient Name: Request Duration (ms): 133.00 Response Headers: Server: nginx/1.22.1 Date: Mon, 18 Nov 2024 21:26:06 GMT Connection: keep-alive Vary: Accept-Encoding ETag: "67091697-f32f" Cache-Control: max-age=315360000 Accept-Ranges: bytes Content-Type: text/html Content-Length: 62255 Last-Modified: Fri, 11 Oct 2024 12:14:15 GMT Expires: Thu, 31 Dec 2037 23:55:55 GMT ``` --- # 2.26 DeepSeek 官方对接 > 原文链接:https://http.furion.net/docs/quick-start/deepseek/ `DeepSeek` 是由深度求索公司开发的多功能人工智能模型,具备聊天、写作、编程、数据分析、翻译及教育辅导等能力。其强大的理解能力和快速学习速度使其适用于多种场景,未来发展潜力巨大。 在对接 `DeepSeek` 人工智能模型之前,您需要先在 [`DeepSeek` 开发平台](https://platform.deepseek.com/) 注册账号并创建 `API key`。获取 `API key` 后,即可在项目中集成 `DeepSeek` 人工智能模型。框架提供了多种对接 `DeepSeek` 人工智能模型的方式: > **`DeepSeek` 接口文档** 如需了解更多 `DeepSeek` 开发文档,请访问:[https://api-docs.deepseek.com/zh-cn/](https://api-docs.deepseek.com/zh-cn/) **1. 标准输出(非流式)** 标准输出(非流式)是指将用户提示词一次性发送并返回最终结果。结果会一次性呈现,适合需要完整输出的场景: ```cs showLineNumbers {5-15,18-19} [HttpGet] public async Task DeepSeek(CancellationToken cancellationToken) { var result = await httpRemoteService.PostAsStringAsync("https://api.deepseek.com/chat/completions", HttpRequestBuilder.Setup .AddBearerAuthentication("您的 APIKEY") .SetJsonContent(""" { "model": "deepseek-v4-pro", "messages": [ {"role": "system", "content": "你是一个专业的 C# 领域人才。"}, {"role": "user", "content": "Furion 框架未来前景?"} ], "stream": false } """), cancellationToken); // 解析 JSON 内容(推荐使用流变对象) var node = JsonNode.Parse(result!); var content = node?["choices"]?[0]?["message"]?["content"]?.GetValue(); return content ?? string.Empty; } ``` > **小知识** 在上面的 `JSON` 数据中,`messages` 数组包含两个对象,每个对象都有一个 `role` 键,分别设置为 `system` 和 `user`,例如: ```json showLineNumbers {4-5} { "model": "deepseek-v4-pro", "messages": [ { "role": "system", "content": "你是一个专业的 C# 领域人才。" }, { "role": "user", "content": "Furion 框架未来前景如何?" } ], "stream": false } ``` - **`system` 角色**:用于定义大模型的初始身份或技能。例如,你可以将其设置为“全能型人才”、“IT 高手”、“医学专家”或“历史学家”等。这个角色帮助模型理解其任务背景。 - **`user` 角色**:代表用户的输入,即用户提出的问题或提示词。 **2. 流式输出(`Server-Sent Events`)** 流式输出能够模拟打字机的效果,非常适合那些需要逐步展示结果的场景。与标准输出模式的主要区别在于,您需要将 `stream` 参数设置为 `true`,并采用 `Server-Sent Events` 实现单向通信。框架已内置对 `Server-Sent Events` 的支持,可直接使用: ```cs showLineNumbers {5-15,17,20,27-28,32} [HttpGet] public async Task DeepSeek_Stream(CancellationToken cancellationToken) { var builder = HttpRequestBuilder.ServerSentEvents("https://api.deepseek.com/chat/completions") .AddBearerAuthentication("您的 APIKEY") .SetJsonContent(""" { "model": "deepseek-v4-pro", "messages": [ {"role": "system", "content": "你是一个专业的 C# 领域人才。"}, {"role": "user", "content": "Furion 框架的作者是谁?"} ], "stream": true } """); await foreach (var data in httpRemoteService.SendAsAsyncEnumerable(builder, cancellationToken)) { // 输出完成 if (data.IsDone) { Console.WriteLine("++++++++++++ 结束 ++++++++++++"); break; } // 解析 JSON 内容(推荐使用流变对象) var node = JsonNode.Parse(data.Data); var content = node?["choices"]?[0]?["delta"]?["content"]?.GetValue(); if (!string.IsNullOrEmpty(content)) { Console.WriteLine(content); } } return "OK"; } ``` **3. 浏览器 `URL` 地址(`Web`)的流式输出(`Server-Sent Events`)** 您还可以在浏览器中通过访问 `URL` 地址实现流式输出效果: ```cs showLineNumbers {7,10-20,22,25,28-29,32,35} [HttpGet] public async Task DeepSeekChat([FromServices] IHttpContextAccessor httpContextAccessor, [FromQuery] string message, CancellationToken cancellationToken) { var httpContext = httpContextAccessor.HttpContext!; // 配置 Server-Sent Events (SSE) 标准流式响应格式 httpContext.Response.EnableServerSentEvents(); var builder = HttpRequestBuilder.ServerSentEvents("https://api.deepseek.com/chat/completions") .AddBearerAuthentication("您的 APIKEY") .SetJsonContent($$""" { "model": "deepseek-v4-pro", "messages": [ {"role": "system", "content": "你是一个专业的 C# 领域人才。"}, {"role": "user", "content": "{{message}}"} ], "stream": true } """); await foreach (var data in httpRemoteService.SendAsAsyncEnumerable(builder, cancellationToken)) { // DeepSeek 输出完成标记 if (data.IsDone) return; // 解析 JSON 内容(推荐使用流变对象) var node = JsonNode.Parse(data.Data); var content = node?["choices"]?[0]?["delta"]?["content"]?.GetValue(); // 向客户端写入一条消息并立即刷新响应流 await httpContext.Response.WriteAndFlushAsync(content, cancellationToken); } await httpContext.Response.CompleteAsync(); } ``` 打开浏览器并访问以下地址即可体验流式输出效果:`https://localhost:7044/GetStart/DeepSeekChat?message=Furion框架怎么样`。如下图所示: ![sse-ai](/images/sse-ai.svg) --- # 2.27 抓取网页内容(爬虫) > 原文链接:https://http.furion.net/docs/quick-start/web-crawler/ 本节介绍三种抓取网页内容的方式:直接抓取配合 `HTML` 解析库,以及借助无头浏览器抓取 `JavaScript` 渲染的页面。 借助 `HTTP` 远程请求模块,您可以轻松抓取互联网上的任意网页内容(即常说的“爬虫”)。配合 `HTML` 解析库(如 [AngleSharp](https://github.com/AngleSharp/AngleSharp) 或 [HtmlAgilityPack](https://github.com/zzzprojects/html-agility-pack)),可进一步提取所需数据;对于依赖 `JavaScript` 渲染的页面,还可借助无头浏览器(如 Playwright)抓取渲染后的完整内容。 以下以抓取博客园(cnblogs)首页的博客标题为例,分别展示两种解析库与无头浏览器的使用方式。 ### 使用 `AngleSharp` **1. 安装 NuGet 包** ```bash showLineNumbers Install-Package AngleSharp ``` **2. 编写爬取代码** ```cs showLineNumbers {2-3,9,12,15} // 获取博客园首页 HTML 字符串 var cnblogs = await httpRemoteService.SendAsStringAsync(HttpRequestBuilder.Get("https://www.cnblogs.com/") .SetUserAgent(UserAgents.GetRandom())); // 随机浏览器 User-Agent // 使用默认配置创建一个用于解析网页的上下文 var context = BrowsingContext.New(Configuration.Default); // 将获取到的 HTML 字符串作为虚拟请求的内容,解析为可操作的文档对象 var document = await context.OpenAsync(req => req.Content(cnblogs)); // 使用 CSS 选择器查询文档中所有匹配的元素:id 为 post_list 下的 post-item-title 类元素 var elements = document.QuerySelectorAll("#post_list .post-item-title"); // 提取每个元素的文本内容(即博客标题) var titles = elements.Select(u => u.TextContent).ToList(); ``` ### 使用 `HtmlAgilityPack` **1. 安装 NuGet 包** ```bash showLineNumbers Install-Package HtmlAgilityPack ``` **2. 编写爬取代码** ```cs showLineNumbers {2-3,9,12,15} // 获取博客园首页 HTML 字符串 var cnblogs = await httpRemoteService.SendAsStringAsync(HttpRequestBuilder.Get("https://www.cnblogs.com/") .SetUserAgent(UserAgents.GetRandom())); // 随机浏览器 User-Agent // 创建 HtmlDocument 实例,用于解析 HTML var document = new HtmlDocument(); // 加载获取到的 HTML 字符串 document.LoadHtml(cnblogs); // 使用 XPath 获取节点集合:选择 id 为 post_list 的元素内,所有 class 包含 post-item-title 的子元素 var nodes = document.DocumentNode.SelectNodes("//*[@id='post_list']//*[contains(@class,'post-item-title')]"); // 提取每个节点的内部文本(即博客标题) var titles = nodes.Select(node => node.InnerText).ToList(); ``` ### 使用无头浏览器(`Playwright`) 部分网页依赖 `JavaScript` 动态渲染(如单页应用 `SPA`、异步加载的列表),直接抓取 `HTML` 拿不到最终内容。此时可借助**无头浏览器**在真实浏览器内核中执行页面脚本,再提取渲染后的数据。[Playwright for .NET](https://playwright.dev/dotnet/)(`Microsoft.Playwright` 包)是当前 `C#` 生态最主流的无头浏览器库(`NuGet` 累计下载量超 6000 万),由微软官方维护,默认以无头模式运行,支持 `Chromium`、`Firefox`、`WebKit` 三种内核。如仅需 `Chromium`,也可选用 [PuppeteerSharp](https://github.com/hardkoded/puppeteer-sharp)。 **1. 安装 NuGet 包** ```bash showLineNumbers Install-Package Microsoft.Playwright ``` **2. 下载浏览器内核** 首次使用前需下载浏览器内核(以下载 `Chromium` 为例): ```bash showLineNumbers dotnet tool install --global Microsoft.Playwright.CLI playwright install chromium ``` **3. 编写抓取代码** ```cs showLineNumbers {2-3,10,13,16} // 启动 Playwright(默认以无头模式运行 Chromium) using var playwright = await Playwright.CreateAsync(); await using var browser = await playwright.Chromium.LaunchAsync(); // 新建页面并设置窗口尺寸(模拟真实浏览器环境) var page = await browser.NewPageAsync(); await page.SetViewportSizeAsync(1920, 1080); // 访问页面并等待网络空闲,确保 JavaScript 执行完毕 await page.GotoAsync("https://www.cnblogs.com/", new() { WaitUntil = WaitUntilState.NetworkIdle }); // 等待目标元素出现(SPA 页面可能异步渲染) await page.WaitForSelectorAsync("#post_list .post-item-title"); // 提取所有匹配元素的文本(即博客标题);page.ContentAsync() 可获取渲染后的完整 HTML var titles = await page.Locator("#post_list .post-item-title").AllTextContentsAsync(); ``` 无头浏览器会真实执行页面脚本,能抓取到 `JavaScript` 渲染后的最终内容,是三种方式中能力最强的;其开销与使用建议见下文注意事项。 ### 注意事项 - **合法合规**:请务必遵守目标网站的 `robots.txt` 协议及相关法律法规,合理控制请求频率,避免对服务器造成压力。 - **反爬策略**:可适当设置 `User-Agent`、延时等待、代理 `IP` 等策略,以提高抓取稳定性。 - **解析方式**:`AngleSharp` 支持 `CSS` 选择器,语法更接近前端;`HtmlAgilityPack` 则基于 `XPath`,两者各有所长,可按需选用。 - **无头浏览器**:首次使用需下载浏览器内核(约数百 `MB`),运行开销大于直接抓取,建议仅在页面依赖 `JavaScript` 渲染时使用;常规静态页面优先使用前两节的直接抓取方式,更轻量高效。 通过 `HTTP` 远程请求、解析库与无头浏览器的结合,您可以快速实现各类网页数据采集需求。 --- # 2.28 httpbin.org 在线测试服务 > 原文链接:https://http.furion.net/docs/quick-start/httpbin/ [`httpbin.org`](https://httpbin.org/) 是一个免费、开源的在线 `HTTP` 请求与响应测试服务,由 `Python` 社区知名项目 `requests` 的作者 `Kenneth Reitz` 创建,现由 `Postman` 团队维护。它能够回显客户端发送的任何请求信息,并模拟各种 `HTTP` 场景,是开发 `HTTP` 客户端、调试请求逻辑的理想工具。 ### 主要功能 - 回显请求方法、路径、头、体、参数、来源 `IP` 等详情。 - 模拟各类 `HTTP` 状态码(如 `404`、`500`)。 - 测试重定向、延迟响应、文件上传、`Cookie`、`GZip` 等场景。 - 提供 **`Basic Auth`**、**`Bearer Auth`** 和 **`Digest Auth`** 端点,便于验证认证实现。 ### 使用建议 - 适合用于集成测试或开发阶段的手动验证,无需自行搭建服务端。 - 该服务为公共资源,**切勿发送真实敏感数据**。 - 受外网环境影响,偶尔可能出现限流或不可用,建议核心测试仍以本地 `Mock` 为主。 - 若需离线环境,可从 [GitHub 仓库](https://github.com/postmanlabs/httpbin) 自行部署。 > **替代选择** 若需更现代的在线测试服务,也可尝试 [`httpbun.com`](https://httpbun.com)。它兼容 `httpbin` 的接口。 --- # 3.1 HttpRequestBuilder 请求构建器 > 原文链接:https://http.furion.net/docs/request-builder/httprequestbuilder-request-builder/ `HttpRequestBuilder` 是一个构建器工具,专门用于在通过 `HttpClient` 发送请求时构建所需的 `HttpRequestMessage` 对象。可以说,`HttpRequestBuilder` 是整个 `HTTP` 远程请求模块的核心组件,负责在发送请求前准备所有必要的请求数据。如下图所示: ![httpagent](/images/httpagent.jpg) [**查看高清架构图**](https://gitee.com/dotnetchina/HttpAgent/blob/master/drawio/HttpAgent.drawio) --- # 3.2 创建构建器实例 > 原文链接:https://http.furion.net/docs/request-builder/creating-a-builder-instance/ `HttpRequestBuilder` 类型的构造函数被设计为私有,因此无法直接使用 `new` 关键字进行实例化。不过,它提供了多个静态方法来方便地创建 `HttpRequestBuilder` 的实例。 > **小贴士** 嫌 `HttpRequestBuilder` 太长?用 `HttpBuilder` 更清爽! **1. 使用请求谓词静态方法(推荐)** `HttpRequestBuilder` 提供了多种基于 `HTTP` 请求方法(如 `GET`、`POST` 等)的静态方法,用于快速创建实例,这些方法支持重载,以适应不同的参数需求。 ```cs showLineNumbers var httpRequestBuilder = HttpRequestBuilder.Get("https://furion.net/"); // GET 请求,支持多个重载 var httpRequestBuilder = HttpRequestBuilder.Put("https://furion.net/"); // PUT 请求,支持多个重载 var httpRequestBuilder = HttpRequestBuilder.Post("https://furion.net/"); // POST 请求,支持多个重载 var httpRequestBuilder = HttpRequestBuilder.Delete("https://furion.net/"); // DELETE 请求,支持多个重载 var httpRequestBuilder = HttpRequestBuilder.Options("https://furion.net/"); // OPTIONS 请求,支持多个重载 var httpRequestBuilder = HttpRequestBuilder.Trace("https://furion.net/"); // TRACE 请求,支持多个重载 var httpRequestBuilder = HttpRequestBuilder.Patch("https://furion.net/"); // PATCH 请求,支持多个重载 var httpRequestBuilder = HttpRequestBuilder.Query("https://furion.net/"); // QUERY 请求,支持多个重载 ``` **2. 使用 `Create` 静态方法** `Create` 方法允许通过更灵活的方式创建 `HttpRequestBuilder` 实例,支持直接指定请求方法和 `URL`,或使用自定义 `HttpMethod`。 ```cs showLineNumbers var httpRequestBuilder = HttpRequestBuilder.Create("GET", "https://furion.net/"); var httpRequestBuilder = HttpRequestBuilder.Create(HttpMethod.Get, "https://furion.net/"); // 自定义请求谓词,如 CONNECT 请求 var httpRequestBuilder = HttpRequestBuilder.Create("Connect", "https://furion.net/"); ``` **3. 使用 `Setup` 静态属性** `Setup` 返回一个空白 `HttpRequestBuilder` 构建器实例,专门用于配置 `HttpRequestBuilder` 并作为 `Action` 委托传入谓词快捷方法(如 `GetAsync`、`PostAsync` 等)。**它通过隐式转换自动将链式配置转换为委托,省去 `builder => builder` 的包裹**。 ```cs showLineNumbers {2,5} // 传统方式:builder => builder await httpRemoteService.GetAsync("https://furion.net/", builder => builder.UseETag().Profiler()); // ✅ 使用 Setup 替代 await httpRemoteService.GetAsync("https://furion.net/", HttpRequestBuilder.Setup.UseETag().Profiler()); ``` > **适用场景** `Setup` 主要用于`IHttpRemoteService` 接口的谓词快捷方法(如 `GetAsync`、`PostAsync`、`PutAsync` 等)的 `configure` 参数。它**不可**直接作为完整请求构建器使用。 **4. 使用 `FromCurl` 静态方法** 通过传入原生的 `cURL` 命令字符串,`FromCurl` 可以一键生成配置好的 `HttpRequestBuilder` 实例,省去手动调用各项配置方法的繁琐步骤。 ```cs showLineNumbers {2,5-6} // 解析最简 cURL 命令,自动识别 GET 方法 var httpRequestBuilder = HttpRequestBuilder.FromCurl("curl http://example.com"); // 解析带请求体(-d)的命令,自动推断为 POST var httpRequestBuilder = HttpRequestBuilder.FromCurl( "curl -X POST https://api.furion.net/data -H \"Content-Type: application/json\" -d '{\"name\":\"John\"}'"); ``` - **`curlCommand`**:完整的 `cURL` 命令字符串,必须以 `curl` 开头。 - **`configure`**:可选委托,用于注册自定义的标志提取器。 > **注意事项** - 命令必须以 `curl` 开头,否则将抛出 `InvalidOperationException`。 - 如果命令中只有 `curl` 而无任何参数,同样会抛出异常。 - 解析器会自动根据 `-d`、`-F` 等数据选项推断 `POST` 请求,否则默认为 `GET`。 > **小贴士** 有关 `FromCurl` 支持的所有 `cURL` 选项、用法示例以及如何扩展自定义提取器,请参阅 **2.17 从 cURL 命令发送** 章节。 **5. 使用 `FromJson` 静态方法** 通过传入 `JSON` 配置字符串,`FromJson` 可以一键生成配置好的 `HttpRequestBuilder` 实例,省去手动链式调用的繁琐步骤。 ```cs showLineNumbers {2,5-6} // 解析最简 JSON 配置,自动识别 GET 方法 var httpRequestBuilder = HttpRequestBuilder.FromJson(""" { "url":"http://example.com", "method":"GET" } """); // 解析带请求体的 JSON 配置,自动推断为 POST var httpRequestBuilder = HttpRequestBuilder.FromJson(""" { "url": "https://api.furion.net/data", "method": "POST", "headers": { "Content-Type": "application/json" }, "data": { "name": "John" } } """); ``` - **`json`**:完整的 `JSON` 配置字符串,**必须是 `JSON` 对象**(以 `{` 开头且以 `}` 结尾),属性名不区分大小写且允许尾随逗号。 - **`configure`**:可选委托,用于注册自定义的 `JSON` 字段提取器。 > **注意事项** - 配置必须是合法的 `JSON` 对象,若传入数组、字符串或空值,将抛出 `ArgumentException("The provided JSON must be a valid JSON object.")`。 - 若未显式指定 `method`,解析器会根据 `data` 或 `multipart` 字段是否存在自动推断:有请求体则为 `POST`,否则为 `GET`。 > **小贴士** 有关 `FromJson` 支持的所有 `JSON` 字段、用法示例以及如何扩展自定义提取器,请参阅 **2.18 从 `JSON` 发送** 章节。 --- # 3.3 设置请求地址 > 原文链接:https://http.furion.net/docs/request-builder/setting-the-request-address/ 在 `HttpRequestBuilder` 类型提供的静态方法中,您可以配置请求的地址。以下展示了如何使用 `HttpRequestBuilder` 类型静态方法来定义不同的请求地址: ```cs showLineNumbers {2,5,8,11,14} // 使用完整 URL 地址 HttpRequestBuilder.Get("https://furion.net/"); // 使用相对地址(不含前导斜杠) HttpRequestBuilder.Get("api/get/user"); // 使用相对地址(含前导斜杠) HttpRequestBuilder.Get("/api/get/user"); // 请求地址为空字符串 HttpRequestBuilder.Get(""); // 也可以使用 string.Empty 替代 // 请求地址为 null HttpRequestBuilder.Get(null); ``` - 当提供的请求地址为完整 `URL` 时,它将直接作为最终的请求地址。 - 若请求地址为相对地址(无论是否包含前导斜杠 `/`),框架将尝试将其与 `HttpClient` 配置的 `BaseAddress` 合并,以生成最终的请求地址(`RFC 3986`)。例如: ```cs showLineNumbers {3} services.AddHttpClient(string.Empty, client => { client.BaseAddress = new Uri("https://furion.net/"); }); ``` 在上述配置中,若请求地址为 `"api/get/user"` 或 `"/api/get/user"`,则最终的请求地址将为 `"https://furion.net/api/get/user"`。 - 若请求地址为空字符串或 `null`,则 `HttpClient` 配置的 `BaseAddress` 将直接作为最终的请求地址。这意味着,如果 `BaseAddress` 为 `"https://furion.net/"`,则最终请求地址也将是 `"https://furion.net/"`。 > **小提示** 值得一提的是,所有支持配置请求地址的方法,除了接受字符串类型的地址外,还兼容 `Uri` 类型的地址设置。 --- # 3.4 方法命名原则 > 原文链接:https://http.furion.net/docs/request-builder/method-naming-conventions/ 在设计 `HttpRequestBuilder` 对象的方法时,我们遵循了一套明确的命名规则,以确保方法的功能和行为直观易懂。**具体来说,所有只能进行操作的方法均以 `Set` 或 `Use` 开头,而所有支持重复调用、进行叠加操作的方法则以 `With` 或 `Add` 开头。** - **`Set` 或 `Use` 开头的方法**:这类方法用于设置某个属性或参数,若重复调用,则后一次调用会覆盖前一次的设置。例如,`SetTraceIdentifier(traceId)` 方法,在多次调用时,只有最后一次调用的 `traceId` 会生效。 - **`With` 或 `Add` 开头的方法**:这类方法用于添加或修改某些内容,且支持重复调用。在重复调用时,它们不会覆盖之前的设置,而是采用叠加的方式。例如,`WithHeader(key, value)` 方法,在多次调用时,会保留之前的所有头部信息,并添加新的头部信息。 这样的命名原则使得 `HttpRequestBuilder` 对象的方法更加清晰易懂,便于开发者在使用时快速理解每个方法的功能和行为。 --- # 3.5 设置跟踪标识 > 原文链接:https://http.furion.net/docs/request-builder/setting-the-trace-identifier/ 为请求指定一个唯一标识符,便于跟踪和调试。该标识符将被设置在 `X-Trace-ID` 请求标头中。 ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/") .SetTraceIdentifier("your-id"); ``` --- # 3.6 设置内容类型 > 原文链接:https://http.furion.net/docs/request-builder/setting-the-content-type/ 指定请求的内容类型。 ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net/") .SetContentType("text/plain"); HttpRequestBuilder.Get("https://furion.net/") .SetContentType("text/plain; charset=utf-8"); // 支持指定字符集 ``` --- # 3.7 设置内容编码 > 原文链接:https://http.furion.net/docs/request-builder/setting-the-content-encoding/ 设置请求的内容编码。 ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net/") .SetContentEncoding(Encoding.UTF8); HttpRequestBuilder.Get("https://furion.net/") .SetContentEncoding("utf-8"); // 支持编码字符串 ``` > **内容编码使用说明** • 当设置内容编码时,系统会自动在 `Content-Type` 后追加 `;charset=编码` 参数。例如: - 原始 `Content-Type`:`application/json`。 - 设置编码为 `utf-8` 后,最终变为:`application/json;charset=utf-8`。 • 注意事项: 1. 部分第三方服务器可能不支持带 `charset` 参数的 `Content-Type`,这会导致请求失败。 2. **若无特殊需求,建议保持默认不设置内容编码**。 --- # 3.8 设置 JSON 内容 > 原文链接:https://http.furion.net/docs/request-builder/setting-json-content/ 将请求的内容类型设置为 `application/json` 并发送 `JSON` 数据。 ```cs showLineNumbers {2,5,8,11,14,17} HttpRequestBuilder.Post("https://furion.net/") .SetJsonContent(new { id = 1, name = "Furion" }); // 支持匿名对象或类型对象 HttpRequestBuilder.Post("https://furion.net/") .SetJsonContent("{\"id\":1,\"name\":\"furion\"}"); // 直接发送 JSON 字符串 HttpRequestBuilder.Post("https://furion.net/") .SetJsonContent("{\"id\":1,\"name\":\"furion\"}", Encoding.UTF8); // 设置编码 HttpRequestBuilder.Post("https://furion.net/") .SetJsonContent("{\"id\":1,\"name\":\"furion\"}", Encoding.UTF8, "application/json-patch+json"); // 自定义 content-type HttpRequestBuilder.Post("https://furion.net/") .SetJsonContent(new { id = 1, name = "Furion" }, jsonSerializerOptions: new JsonSerializerOptions()); // 支持传入 JsonSerializerOptions 对象 HttpRequestBuilder.Post("https://furion.net/") .SetJsonContentWithoutValidation("{\"id\":1,\"name\":\"furion\"}"); // 直接发送 JSON 字符串(不会校验 JSON 格式有效性) ``` > **推荐使用【[原始字符串字面量](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string)】设置 `JSON`** 推荐使用[原始字符串字面量](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string)来设置 `JSON` 数据。在 `C# 11` 中,新增了这一特性,允许使用三个双引号(`"""`)包裹的字符串来包含多行文本,同时字符串内的转义字符(如 `\n`,`\t` 等)将作为普通字符处理,无需转义。例如: ```cs showLineNumbers {2-7} HttpRequestBuilder.Post("https://furion.net/") .SetJsonContent(""" { "id": 1, "name": "Furion" } """); ``` 若需在原始字符串中插入变量,只需在首个 `"""` 前添加 `$$`,并使用 `{{变量名}}` 模板来占位。例如: ```cs showLineNumbers {1,4,7} var val = "Furion"; HttpRequestBuilder.Post("https://furion.net/") .SetJsonContent($$""" { "id": 1, "name": "{{val}}" } """); ``` 使用原始字符串字面量设置 `JSON` 数据,可以简化代码,避免处理转义符的繁琐操作。 > **自定义 `JSON` 属性名称** 当对象序列化为 `JSON` 时,属性默认采用小驼峰命名(`CamelCase`)。若需自定义序列化后的属性名称,可在属性上添加 `[JsonPropertyName("自定义名称")]` 特性。 > **`JSON` 字符串说明** 若传入的 `JSON` 字符串格式无效,将抛出 `JsonException` 异常。若无需校验格式,可使用 `SetJsonContentWithoutValidation` 方法。 --- # 3.9 设置 HTML 内容 > 原文链接:https://http.furion.net/docs/request-builder/setting-html-content/ 将请求的内容类型设置为 `text/html` 并发送 `HTML` 数据。 ```cs showLineNumbers {2,5,8} HttpRequestBuilder.Post("https://furion.net/") .SetHtmlContent(""); HttpRequestBuilder.Post("https://furion.net/") .SetHtmlContent("", Encoding.UTF8); // 设置编码 HttpRequestBuilder.Post("https://furion.net/") .SetHtmlContent("", Encoding.UTF8, "application/html"); // 自定义 content-type ``` > **推荐使用【[原始字符串字面量](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string)】设置 `HTML`** 参考【3.8 设置 `JSON` 内容】。 --- # 3.10 设置 XML 内容 > 原文链接:https://http.furion.net/docs/request-builder/setting-xml-content/ 将请求的内容类型设置为 `text/xml` 并发送 `XML` 数据。 ```cs showLineNumbers {2,5,8} HttpRequestBuilder.Post("https://furion.net/") .SetXmlContent(""); HttpRequestBuilder.Post("https://furion.net/") .SetXmlContent("", Encoding.UTF8); // 设置编码 HttpRequestBuilder.Post("https://furion.net/") .SetXmlContent("", Encoding.UTF8, "application/soap+xml"); // 自定义 content-type ``` > **推荐使用【[原始字符串字面量](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string)】设置 `XML`** 参考【3.8 设置 `JSON` 内容】。 --- # 3.11 设置文本内容 > 原文链接:https://http.furion.net/docs/request-builder/setting-text-content/ 将请求的内容类型设置为 `text/plain` 并发送纯文本数据。 ```cs showLineNumbers {2,5,8} HttpRequestBuilder.Post("https://furion.net/") .SetTextContent("Furion"); HttpRequestBuilder.Post("https://furion.net/") .SetTextContent("Furion", Encoding.UTF8); // 设置编码 HttpRequestBuilder.Post("https://furion.net/") .SetTextContent("Furion", Encoding.UTF8, "text/plain"); // 自定义 content-type ``` > **推荐使用【[原始字符串字面量](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string)】设置文本** 参考【3.8 设置 `JSON` 内容】。 --- # 3.12 设置原始 raw 字符串内容 > 原文链接:https://http.furion.net/docs/request-builder/setting-raw-raw-string-content/ 在诸如 `Postman` 等现代 `API` 测试工具中,用户可以通过 `raw` 数据格式发送请求。在 `ASP.NET Core` 服务端应用程序中,这通常表现为接收一个标记有 `[FromBody]` 特性的字符串参数(例如 `str`): ```cs showLineNumbers {3} [HttpPost] // [Consumes("application/json")] public string AddBodyString([FromBody] string str) // 注意:默认情况下 ASP.NET Core 不支持 text/plain 内容类型的绑定 { return str; } ``` 为了设置原始字符串内容,可以采用如下方法: ```cs showLineNumbers {2,5,8} HttpRequestBuilder.Post("https://furion.net/") .SetRawStringContent("Furion"); // 默认内容类型 text/plain HttpRequestBuilder.Post("https://furion.net/") .SetRawStringContent("Furion", "application/json"); // 内容类型必填 HttpRequestBuilder.Post("https://furion.net/") .SetContent("\"Furion\"", "application/json"); // 等价 SetRawStringContent 方式调用 ``` > **原始字符串格式** 请注意,在调用 `SetRawStringContent(text)` 方法时,所传递的字符串内容会自动被双引号包裹后发送。举例来说,若输入字符串为 `Furion`,则实际发送的内容将是 `"Furion"`。 --- # 3.13 设置 URL 编码表单内容 > 原文链接:https://http.furion.net/docs/request-builder/setting-url-encoded-form-content/ 将请求的内容类型设置为 `application/x-www-form-urlencoded` 并发送表单数据。 ```cs showLineNumbers {2,5,9,12} HttpRequestBuilder.Post("https://furion.net/") .SetFormUrlEncodedContent(new { id = 1, name = "Furion" }); HttpRequestBuilder.Post("https://furion.net/") .SetFormUrlEncodedContent(new { id = 1, name = "Furion" }, useStringContent: true); // 使用 StringContent 解决 FormUrlEncodedContent 编码问题 // 支持 URL 编码字符串格式 HttpRequestBuilder.Post("https://furion.net/") .SetFormUrlEncodedContent("id=1&name=furion", useStringContent: true); HttpRequestBuilder.Post("https://furion.net/") .SetFormUrlEncodedContent(new { id = 1, name = "Furion" }, urlEncode: false); // 可配置不进行 URL 编码处理 ``` > **`URL` 编码表单内容说明** - **默认情况下,`URL` 编码表单通过 [`FormUrlEncodedContent`](https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Net.Http/src/System/Net/Http/FormUrlEncodedContent.cs#L44) 类型进行构建,但此类型不支持自定义请求内容编码,它默认使用 `Encoding.Latin1` 而不是 `UTF-8`。** 这可能在提交到某些接口时引发异常。 为解决此问题,可以通过设置参数 `useStringContent` 为 `true` 来采用 `StringContent` 方式构建表单数据,从而允许自定义编码为 `UTF-8`。 ```cs showLineNumbers {3} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddURLForm", builder => builder .SetFormUrlEncodedContent(new { id = 1, name = "furion" }, useStringContent: true)); ``` - 某些服务器要求显式声明字符集(`charset`),此时可通过 `contentEncoding` 参数指定编码方式,例如使用 `UTF-8`: ```cs showLineNumbers {3} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddURLForm", builder => builder .SetFormUrlEncodedContent(new { id = 1, name = "furion" }, Encoding.UTF8)); ``` 此设置在发送远程请求时会生成如下 `Content-Type` 请求头:`application/x-www-form-urlencoded; charset=UTF-8`。 --- # 3.14 设置文件内容 > 原文链接:https://http.furion.net/docs/request-builder/setting-file-content/ 从本地路径或互联网地址中设置文件内容,自动识别文件名和 `Content-Type`。 ```cs showLineNumbers {3,7,11,16-19} // 设置本地文件 HttpRequestBuilder.Post("https://furion.net/") .SetFileContent(@"C:\Users\Furion\test.png"); // 设置本地文件并指定文件名和内容类型 HttpRequestBuilder.Post("https://furion.net/") .SetFileContent(@"C:\Users\Furion\test.png", "avatar.png", "image/png"); // 设置互联网文件 HttpRequestBuilder.Post("https://furion.net/") .SetFileContent("https://furion.net/files/test.png"); // 设置互联网文件并指定自定义请求配置 HttpRequestBuilder.Post("https://furion.net/") .SetFileContent("https://furion.net/files/test.png", configure: request => { request.Headers.TryAddWithoutValidation("custom-key", "custom-value"); }); ``` > **文件内容说明** - 当传入 `filePath` 为互联网地址(`http/https`)时,框架会自动发起 `GET` 请求下载文件流。 - 若未指定 `fileName`,框架会自动从路径或 `Uri` 地址中解析文件名。 - 若未指定 `contentType`,框架会根据文件扩展名自动推断 `Content-Type`。 - 该方法内部会自动添加 `Content-Disposition` 请求头,格式为 `attachment; filename="文件名"`。 --- # 3.15 设置二进制流内容 > 原文链接:https://http.furion.net/docs/request-builder/setting-binary-stream-content/ 直接设置 `Stream` 作为请求内容,适用于文件流、内存流、网络流等场景。 ```cs showLineNumbers {3,7,10,12,16} // 设置文件流 HttpRequestBuilder.Post("https://furion.net/") .SetStreamContent(File.OpenRead(@"C:\Users\Furion\test.png")); // 设置文件流并指定文件名和内容类型 HttpRequestBuilder.Post("https://furion.net/") .SetStreamContent(File.OpenRead(@"C:\Users\Furion\test.png"), "avatar.png", "image/png"); // 设置内存流 var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes("Hello Furion")); HttpRequestBuilder.Post("https://furion.net/") .SetStreamContent(memoryStream, contentType: "text/plain"); // 设置流并指定编码 HttpRequestBuilder.Post("https://furion.net/") .SetStreamContent(memoryStream, contentEncoding: Encoding.UTF8); ``` > **流内容说明** - `disposeResourcesOnRequestCompletion` 参数默认为 `true`,即请求完成后自动释放流资源。若需手动管理流的生命周期,可将该参数设置为 `false`。 - 当提供 `fileName` 参数时,框架会自动添加 `Content-Disposition` 请求头,格式为 `attachment; filename="文件名"`。 - 若未指定 `contentType`,框架会根据文件名扩展名自动推断,若无法推断则根据流类型使用 `application/octet-stream`。 --- # 3.16 设置请求内容(Body) > 原文链接:https://http.furion.net/docs/request-builder/setting-request-content-body/ 支持设置任意类型的请求内容。 ```cs showLineNumbers {2,5,8,11,14,17,20,23,26,29} HttpRequestBuilder.Post("https://furion.net/") .SetContent(null); HttpRequestBuilder.Post("https://furion.net/") .SetContent("furion", "text/plain"); HttpRequestBuilder.Post("https://furion.net/") .SetContent(new { id = 1, name = "Furion"}); // 自动推断 Content-Type HttpRequestBuilder.Post("https://furion.net/") .SetContent(new { id = 1, name = "Furion"}, "application/json"); HttpRequestBuilder.Post("https://furion.net/") .SetContent(new MemoryStream(), "application/octet-stream"); HttpRequestBuilder.Post("https://furion.net/") .SetContent(new byte[]{}, "application/octet-stream"); HttpRequestBuilder.Post("https://furion.net/") .SetContent(new StringContent(...), "text/plain; charset=utf-8"); HttpRequestBuilder.Post("https://furion.net/") .SetContent(new ReadOnlyMemory(...), "application/octet-stream"); HttpRequestBuilder.Post("https://furion.net/") .SetContent(new MultipartContent(), "multipart/form-data"); HttpRequestBuilder.Post("https://furion.net/") .SetContent(new MemoryStream(), "multipart/form-data", disposeResourcesOnRequestCompletion: true); // 配置请求结束后自动释放资源 ``` > **未提供 `Content-Type` 时的默认行为** 当未指定 `Content-Type` 时,框架会根据以下优先级确定 `Content-Type`: 1. **请求内容标头**:如果通过 `WithHeader` 等方式设置了 `Content-Type`,则优先使用。 2. **内容类型自动推断**:如果未设置内容标头,框架会根据 `RawContent` 的具体类型,按以下规则自动推断: - **`JsonContent`**:`application/json` - **`JsonNode` 或 `JsonElement`** - 若表示 `JSON` 对象或数组,则为 `application/json` - 否则(如 `JSON` 标量值)回退为 `text/plain` - **`FormUrlEncodedContent`**:`application/x-www-form-urlencoded` - **`StringContent`**:`text/plain` - **`MultipartFormDataContent`**:`multipart/form-data` - **`MultipartContent`**(非 `FormData` 子类):`multipart/mixed` - **`ByteArrayContent`、`StreamContent`、`ReadOnlyMemoryContent`**:`application/octet-stream` - **`byte[]`、`Stream`、`ReadOnlyMemory`**:`application/octet-stream` - **其他自定义 `HttpContent` 子类**(未设置标头且未命中上述具体类型):`application/octet-stream` - **`MultipartFile`**:`application/octet-stream` - **`FileInfo`**:根据文件扩展名通过 `FileTypeMapper` 推断,若无法识别则默认为 `application/octet-stream` - **其他复杂对象**(非基础类型、非枚举、非集合):`application/json`(假定将其序列化为 `JSON`) 3. **全局默认回退值**:如果以上规则均未命中,则使用 `HttpClientOptions.DefaultContentType` 中配置的值。该值默认为 `text/plain`,如需修改此回退值,可通过以下代码配置: ```cs showLineNumbers {2,5} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // 设置默认的请求内容类型(最终回退值) options.DefaultContentType = "application/json"; }); ``` > **小提示** `SetJsonContent`、`SetHtmlContent`、`SetXmlContent`、`SetTextContent`、`SetRawStringContent`、`SetFormUrlEncodedContent`、`SetFileContent`、`SetStreamContent` 和 `SetMcpContent` 方法均在内部调用了 `SetContent` 方法。 --- # 3.17 设置 MCP/2.0 消息内容 > 原文链接:https://http.furion.net/docs/request-builder/content-mcp/ `SetMcpContent` 是专门为 **`MCP`(`Model Context Protocol`)2.0** 协议设计的扩展方法,用于快速构建符合 `JSON‑RPC 2.0` 格式的请求,并自动附加必需的 `MCP` 请求头。它支持发送**请求**(需要响应)和**通知**(无需响应)两种消息类型。 ```cs showLineNumbers {2,6,10-13,17} HttpRequestBuilder.Post("https://mcp.example.com/mcp") .SetMcpContent("MyClient", "tools/list"); // 携带参数 HttpRequestBuilder.Post("https://mcp.example.com/mcp") .SetMcpContent("MyClient", "tools/list", new { name = "get_weather" }); // 自定义 ID HttpRequestBuilder.Post("https://mcp.example.com/mcp") .SetMcpContent("MyClient", new McpMessageData("tools/list") { Id = "custom-001" }); // 与 Server Sent Events 流式响应结合使用 HttpRequestBuilder.ServerSentEvents("https://mcp.example.com/mcp") .SetMcpContent("MyClient", "tools/call", new { name = "get_weather" }); ``` > **`MCP/2.0` 内容说明** - 该方法会自动完成 `MCP` 请求封装:填充必需的请求头(`Mcp-Name`、`MCP-Protocol-Version`、`Mcp-Method`、`Accept`),并按 `JSON‑RPC 2.0` 规范序列化请求体。 - `Id` 未指定时自动生成递增整数;若需发送通知,请将 `Id` 设为 `null`。 - 若使用 `GET/HEAD` 且带请求内容,框架会自动将方法切换为 `POST`。 > **`Server Sent Events` 流式响应处理** 当服务器返回 `text/event-stream` 流式响应时,可通过 `ToMcpMessage()` 扩展方法将 `ServerSentEventsData` 解析为 `McpMessageData`,再使用 `GetResult()` 或 `GetData()` 获取返回的内容。 --- # 3.18 设置多部分表单内容 > 原文链接:https://http.furion.net/docs/request-builder/multipart/ 将请求的内容类型设置为 `multipart/form-data` 并发送多部分表单内容。 ```cs showLineNumbers {2-5,12} HttpRequestBuilder.Post("https://furion.net/") .SetMultipartContent(multipart => { multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"); // ... }); // 支持配置保留多部分内容默认的 Content-Type(默认不保留) HttpRequestBuilder.Post("https://furion.net/") .SetMultipartContent(multipart => { multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"); // ... }, false); ``` 除了在 `SetMultipartContent` 内部一次性构建内容外,还可以通过 `WithMultipart` 方法在外部多次追加内容,实现更灵活的链式调用。 ```cs showLineNumbers {2,5-6} HttpRequestBuilder.Post("https://furion.net/") .SetMultipartContent(multipart => { multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"); }) .WithMultipart(multipart => multipart.AddJson("{}", "name")) .WithMultipart(multipart => multipart.AddText("Hello", "name")); // 支持配置多个 ``` > **注意事项** - `WithMultipart` 方法仅当之前已调用 `SetMultipartContent` 设置了多部分内容时才有效,否则将跳过操作。 - `WithMultipart` 支持多次调用,每次追加的内容会合并到同一个多部分表单中。 - `SetMultipartContent` 的第二个参数 `omitContentType` 默认为 `true`,表示**移除**默认的 `Content-Type` 头(通常由 `HttpContent` 自动生成)。若需保留该头,请显式传递 `false`。 > **重要说明** 使用 `SetMultipartContent` 方法会覆盖其他内容设置方法(`SetJsonContent`、`SetHtmlContent`、`SetXmlContent`、`SetTextContent`、`SetRawStringContent`、`SetFormUrlEncodedContent` 和 `SetContent`)。 ```cs showLineNumbers {2-5} HttpRequestBuilder.Post("https://furion.net/") .SetMultipartContent(multipart => { multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"); }) .SetContent(new { id = 1, name = "Furion" }, "application/json"); // 将被覆盖 ``` 此外,在调用 `SetMultipartContent` 方法后,`HttpRequestBuilder` 实例的 `MultipartFormDataBuilder` 属性将被初始化(即不再为 `null`),此时可通过该属性实现一些复杂的业务逻辑处理。 --- # 3.19 设置请求标头 > 原文链接:https://http.furion.net/docs/request-builder/setting-request-headers/ 添加或修改请求标头。 ```cs showLineNumbers {2-6} HttpRequestBuilder.Get("https://furion.net/") .WithHeader("X-Header", "X-Value") // 添加单个标头 .WithHeader("date", DateTime.Now, format: "yyyyMMdd") // 支持 format 格式化 .WithHeaders(new Dictionary { }) // 添加多个标头 .WithHeaders(new { id = 1, name = "Furion" }) // 添加多个标头,支持多种键值类型 .WithHeaders("Content-Type: application/json"); // 支持使用冒号(:)配置 ``` 若存在重复的请求标头,它们将被合并,并用逗号加空格(`, `)分隔多个值。通过设置 `replace: true` 参数,可以覆盖先前的请求标头设置。 > **使用 `HeaderNames` 静态类设置 `HTTP` 标头** 在配置 `HTTP` 请求或响应的标准标头时,如 `Authorization`,**手写标头名称可能会因拼写错误而导致问题**。 为避免此类错误,并充分利用集成开发环境(`IDE`)的智能提示功能,推荐使用 `HeaderNames` 静态类。例如,使用 `HeaderNames.Authorization` 和 `HeaderNames.UserAgent` 可以确保标头名称的准确性,并提升代码的可读性和维护性。 > **配置参数支持** 请求标头支持配置参数,用于读取配置信息进行替换操作。配置参数使用 `[[key]]` 语法。 --- # 3.20 设置移除的请求标头 > 原文链接:https://http.furion.net/docs/request-builder/setting-request-headers-to-remove/ 移除指定的请求标头。 ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/") .RemoveHeaders("User-Agent", "Host", "Accept"); // 移除多个标头 ``` 在发送 `HTTP` 请求之前,将移除配置中指定的待移除请求标头集合。也就是说,`RemoveHeaders` 方法会在所有 `WithHeader[s]` 方法调用之后执行。 --- # 3.21 设置片段标识符 > 原文链接:https://http.furion.net/docs/request-builder/fragment/ 在 `URL` 中添加片段标识符。 ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net/") .SetFragment("About"); // 生成 URL: https://furion.net/#About HttpRequestBuilder.Get("https://furion.net/") .SetFragment("#About"); // 支持以 # 符号开头 ``` --- # 3.22 设置超时时间 > 原文链接:https://http.furion.net/docs/request-builder/setting-the-timeout/ 为单次请求设置超时时长。 ```cs showLineNumbers {2,5,8,11,14,20,26,29} HttpRequestBuilder.Get("https://furion.net/") .SetTimeout(TimeSpan.FromSeconds(2)); HttpRequestBuilder.Get("https://furion.net/") .SetTimeout(2000); // 毫秒单位 HttpRequestBuilder.Get("https://furion.net/") .SetTimeout(-1); // 永不超时,或使用 .WithoutTimeout(),或设置为 System.Threading.Timeout.InfiniteTimeSpan HttpRequestBuilder.Get("https://furion.net/") .SetTimeout(0); // 立即取消请求 HttpRequestBuilder.Get("https://furion.net/") .SetTimeout(TimeSpan.FromSeconds(2), () => // 支持超时回调 { // 超时发生时要执行的操作 }); HttpRequestBuilder.Get("https://furion.net/") .SetTimeout(2000, () => // 支持超时回调 { // 超时发生时要执行的操作 }); HttpRequestBuilder.Get("https://furion.net/") .SetTimeout(new HttpTimeoutOptions()); // 自定义超时选项 HttpRequestBuilder.Get("https://furion.net/") .SetTimeout(options => options.SetTimeout(2000)); // 自定义超时选项 ``` > **超时与重试机制** `SetTimeout` 设置的是**单次请求**的超时时间。若同时配置了 `SetRetry`,该超时时间将**独立应用于每一次重试**,而非所有重试的总耗时。 若需限制包含重试和等待时间在内的**最大总耗时**,请通过外部 `CancellationTokenSource` 实现: ```cs showLineNumbers {2,5-6,9} // 限制整个流程(含重试)总耗时不得超过 2 秒 using var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(2)); var builder = HttpRequestBuilder.Get("https://furion.net/") .SetTimeout(500) // 单次请求超时 500ms .SetRetry(5, TimeSpan.FromSeconds(1)); // 允许重试 5 次,每次间隔 1 秒 // 传入全局 Token,达到 2 秒时立即终止整个流程 var response = await httpRemoteService.SendAsync(builder, timeoutCancellationToken.Token); ``` > **`HttpClient` 超时时间说明** 在 `HttpClient` 中设置超时时间时,请确保单次请求的超时时间不超过 `HttpClient` 配置的超时时间。例如,如果 `HttpClient` 超时时间设置为 `10` 分钟,而单次请求的超时时间设为 `15` 分钟,那么单次请求在超过 `10` 分钟时仍会触发超时异常。示例代码如下: ```cs showLineNumbers {1,3} services.AddHttpClient(string.Empty, client => { client.Timeout = TimeSpan.FromMinutes(10); // 默认超时时间为 100 秒,需显式设置 // client.Timeout = System.Threading.Timeout.InfiniteTimeSpan; // 永不超时 }); ``` 因此,**若单次请求需要更长的超时时间,请确保 `HttpClient` 的超时时间设置得相应更长。** --- # 3.23 配置重试策略 > 原文链接:https://http.furion.net/docs/request-builder/configuring-retry-policies/ 为单次请求配置重试策略。默认情况下,若已配置重试策略,则当请求出现未被抑制的异常时,将自动触发重试机制。 ```cs showLineNumbers {2,5,8,11,14,17,20,23,27,31,34} HttpRequestBuilder.Get("https://furion.net/") .SetRetry(3); // 最大重试次数,0 表示不重试 HttpRequestBuilder.Get("https://furion.net/") .SetRetry(3, ctx => Console.WriteLine($"这是第 {ctx.Attempt} 次重试.")); // 配置每次重试前的回调委托 HttpRequestBuilder.Get("https://furion.net/") .SetRetry(3, TimeSpan.FromSeconds(1)); // 配置重试间隔时间 HttpRequestBuilder.Get("https://furion.net/") .SetRetry(3, TimeSpan.FromSeconds(1), ctx => Console.WriteLine($"这是第 {ctx.Attempt} 次重试.")); // 配置每次重试前的回调委托 HttpRequestBuilder.Get("https://furion.net/") .SetRetry(3, 1000); // 配置重试间隔时间(毫秒) HttpRequestBuilder.Get("https://furion.net/") .SetRetry(3, 1000, ctx => Console.WriteLine($"这是第 {ctx.Attempt} 次重试.")); // 配置每次重试前的回调委托 HttpRequestBuilder.Get("https://furion.net/") .SetRetry(new HttpRetryOptions()); // 自定义重试选项 HttpRequestBuilder.Get("https://furion.net/") .SetRetry(options => options.SetMaxRetries(3)); // 自定义重试选项 HttpRequestBuilder.Get("https://furion.net/") .SetRetry(options => options.SetMaxRetries(3) .AddRetryStatusCodes(401)); // 对特定 HTTP 状态码进行重试 HttpRequestBuilder.Get("https://furion.net/") .SetRetry(options => options.SetMaxRetries(3) .AddRetryExceptions(typeof(InvalidOperationException))); // 对特定异常类型进行重试 HttpRequestBuilder.Get("https://furion.net/") .SetRetryIndefinitely(); // 设置无限重试,直到成功 ``` > **`HttpClient` 超时时间对重试的影响说明** 重试的总耗时受 `HttpClient` 超时时间约束。例如,超时设为 `3` 秒,最大重试 `4` 次,每次间隔 `1` 秒,若在 `3` 秒内重试仍未成功,则后续重试将被取消。因此,如需确保重试不被中断,请合理配合超时设置,否则可能导致请求被提前终止或无限阻塞。 `HttpRetryOptions` 包含以下属性和方法: - **属性**: - `MaxRetries`:最大重试次数(`int` 类型)。默认值为 `0`,表示不重试。如果设置了 `RetryIntervals`,此值将自动被覆盖为数组长度。 - `RetryInterval`:重试间隔基准时间(`TimeSpan` 类型)。默认值为 `1` 秒。仅在未设置 `RetryIntervals` 时生效。 - `UseExponentialBackoff`:是否采用指数退避重试(`bool` 类型)。默认值为:`false`。当设置为 `true` 时,每次重试间隔 = `RetryInterval * 2^(retry-1)`。仅在未设置 `RetryIntervals` 时生效。 - `RetryIntervals`:自定义重试间隔数组(`IList?` 类型)。如果设置了此属性,则重试次数将等于数组长度,`MaxRetries` 和 `UseExponentialBackoff` 将被忽略。每次重试将按顺序使用数组中对应索引的间隔时间。 - `RetryStatusCodes`:内容大小(`HashSet?` 类型)。需要重试的 `HTTP` 状态码集合。若为空,则仅重试因异常引发的失败。 - `RetryExceptionTypes`:需要重试的异常类型集合(`HashSet?` 类型)。若为空,则对所有 `Exception` 进行重试(受 `MaxRetries` 限制)。 - `OnRetry`:`OnRetry`:每次重试前的回调委托(`Action?` 类型)。可用于记录日志、发送通知等。**若未设置此回调,框架将自动输出默认的重试警告日志。** 参数为 `HttpRetryContext`,包含以下属性: - `Attempt`:当前重试次数(`int` 类型)。从 `1` 开始。 - `Exception`:触发重试的异常(`Exception?` 类型)。 - `StatusCode`:触发重试的 `HTTP` 状态码(`HttpStatusCode?` 类型)。 - `MaxRetries`:最大重试次数(`int` 类型)。`-1` 表示无限(日志中显示为 `∞`)。 - `IsExceptionRetry`:是否因异常触发(`bool` 类型) - `IsStatusCodeRetry`:是否因状态码触发(`bool` 类型) - `RetryIndefinitely`:是否无限重试,直到成功(`bool` 类型)。默认值为:`false`。当设置为 `true` 时,`MaxRetries` 和 `RetryIntervals` 的长度将被忽略,一直重试直到成功或发生不可重试的异常。 - **方法**: - `SetMaxRetries(maxRetries)`:设置最大重试次数。 - `SetRetryInterval(interval)` 和 `SetRetryInterval(milliseconds)`:设置重试间隔基准时间。 - `SetUseExponentialBackoff(use)`:设置是否使用指数退避。 - `SetRetryIntervals(intervals)` 和 `SetRetryIntervals(milliseconds)`:设置自定义重试间隔数组。 - `AddRetryStatusCode(statusCode)`:添加一个需要重试的 `HTTP` 状态码。 - `AddRetryStatusCodes(statusCodes)`:添加多个需要重试的 `HTTP` 状态码。 - `AddRetryException()` 和 `AddRetryException(exceptionType)`:添加一个需要重试的异常类型。 - `AddRetryExceptions(exceptionTypes)`:添加多个需要重试的异常类型。 - `SetOnRetry(onRetry)`:设置每次重试前的回调委托。 - `SetRetryIndefinitely(retryIndefinitely)`:设置是否无限重试。 --- # 3.24 设置路径片段 > 原文链接:https://http.furion.net/docs/request-builder/setting-path-segments/ 添加 `URL` 路径片段。 ```cs showLineNumbers {2-7} HttpRequestBuilder.Get("https://furion.net/") .WithPathSegment("user") // 添加单个路径片段 .WithPathSegments(["detail", "edit"]) // 添加多个路径片段 ``` 生成的最终 `URL` 为:`https://furion.net/user/detail/edit`。 若存在重复的路径片段,它们将在后续追加中重复出现(如:`/docs/docs/users/docs/`)。 --- # 3.25 设置移除的路径片段 > 原文链接:https://http.furion.net/docs/request-builder/setting-path-segments-to-remove/ 移除指定的路径片段。 ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/docs/login/users") .RemovePathSegments("docs", "user"); // 移除多个路径片段 ``` 生成的最终 `URL` 为:`https://furion.net/login`。 在发送 `HTTP` 请求之前,将移除配置中指定的待移除路径片段集合。也就是说,`RemovePathSegments` 方法会在所有 `WithPathSegment[s]` 方法调用之后执行。 --- # 3.26 设置查询参数(URL 参数) > 原文链接:https://http.furion.net/docs/request-builder/setting-query-parameters-url-parameters/ 添加或修改 `URL` 查询参数。 ```cs showLineNumbers {2-7,9-12} HttpRequestBuilder.Get("https://furion.net/") .WithQueryParameter("id", 1) // 添加单个参数 .WithQueryParameter("date", DateTime.Now, format: "yyyyMMdd") // 支持 format 格式化 .WithQueryParameter("name", new[] { "furion", "monksoul" }) // 添加多个值,生成:name=furion&name=monksoul .WithQueryParameter("name", (object?)null) // 设置 null 值 .WithQueryParameter("r", () => DateTimeOffset.UtcNow.ToUnixTimeSeconds()) // 设置动态计算参数(用于防缓存) .WithQueryParameter("r", context => DateTimeOffset.UtcNow.ToUnixTimeSeconds()) // 设置动态计算参数(用于防缓存) .WithQueryParameters(new Dictionary { }) // 添加多个参数 .WithQueryParameters(new { id = 1, name = "Furion" }) // 添加多个参数,生成:id=1&name=Furion .WithQueryParameters(new { id = 1, name = "Furion" }, "user") // 添加带前缀的参数,生成:user.id=1&user.name=Furion .WithQueryParameters(new Dictionary { { "str1", null }, {"str2", "test" } }, ignoreNullValues: true); // 忽略空值 ``` 若存在重复的查询参数键,它们将合并成多个键值对(如 `key1=value1&key1=value2`)。通过设置 `replace: true` 参数,可以覆盖先前的查询参数和原始 `URL` 地址参数。**默认情况下,值为 `null` 的查询参数会被添加到 `URL` 中;若需忽略这些参数,可设置 `ignoreNullValues: true`。** ### `URL` 参数格式化程序 在设置 `HTTP` 请求的查询参数时,框架会将参数键和值传递给 `IUrlParameterFormatter` 进行格式化。默认实现 `UrlParameterFormatter` 会为每个值生成一个 `key=value` 形式的键值对。但某些类型(如 `DateTime`)可能需要特殊处理,或希望改变整个键值对的输出形态(例如将多个值输出为 `key[0]=val1&key[1]=val2` 这样的数组格式),此时可以通过自定义格式化程序实现。 以下示例展示如何重写 `Format` 方法,以便将 `DateTime` 类型的值格式化为 `yyyyMMdd` 格式,其余类型使用默认处理: ```csharp showLineNumbers {1,4,6-15} public class CustomUrlParameterFormatter : UrlParameterFormatter { /// public override IEnumerable>? Format(UrlFormattingContext context, string key, IEnumerable values) { foreach (var value in values) { if (value is DateTime dateTime) { yield return new(key, dateTime.ToString("yyyyMMdd")); // 格式化 continue; } yield return new(key, FormatValue(context, value)); } } } ``` 完成自定义格式化程序后,可以在配置 `HttpRemoteOptions` 时将其注册为默认的 `URL` 参数格式化器: ```csharp showLineNumbers {2,4} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { options.UrlParameterFormatter = new CustomUrlParameterFormatter(); }); ``` 如此一来,在构建 `URL` 查询参数时,若遇到 `DateTime` 类型的值,框架将自动将其格式化为 `yyyyMMdd` 格式的字符串,从而确保输出符合预期。 ### `URL` 参数排序 尽管对 `URL` 查询参数排序的需求相对少见,但在一些对安全性要求较高的系统中,往往需要验证参数的顺序。框架为此提供了排序支持,排序对象为最终的键值对集合: ```cs showLineNumbers {3} HttpRequestBuilder.Get("https://furion.net/") .WithQueryParameters(new { name = "furion", id = 1}) .SetQueryParametersSorter(pairs => pairs.OrderBy(kv => kv.Key)); ``` 通过 `.SetQueryParametersSorter()` 方法配置查询参数排序规则。该方法接收一个 `KeyValuePair` 序列,返回排序后的新序列。为 `null` 时不排序(原始添加顺序)。 --- # 3.27 设置移除的查询参数 > 原文链接:https://http.furion.net/docs/request-builder/setting-query-parameters-to-remove/ 移除指定的查询参数。 ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/") .RemoveQueryParameters("id", "name", "age"); // 移除多个参数 ``` 在发送 `HTTP` 请求之前,将移除配置中指定的待移除查询参数集合。也就是说,`RemoveQueryParameters` 方法会在所有 `WithQueryParameter[s]` 方法调用之后执行。 --- # 3.28 设置路径参数(模板/配置参数) > 原文链接:https://http.furion.net/docs/request-builder/path-params/ 在 `URL` 路径中替换对象模板字符串。 ```cs showLineNumbers {2-6,8-9,11-12} HttpRequestBuilder.Get("https://furion.net?id={id}&name={name}") .WithPathParameter("id", 1) // 添加单个参数,{id} 将被替换为 1 .WithPathParameter("name", new[] { "furion", "monksoul" }) // 添加单个参数,{name} 将被替换为 furion,monksoul .WithPathParameters(new Dictionary { }) // 添加多个路径参数 .WithPathParameters(new { id = 1, name = "Furion" }) // 添加多个路径参数,{id} 和 {name} 分表被替换为 1 和 Furion .WithPathParameters(new { id = 1, name = "Furion" }, "user") // 添加带前缀的参数,{user.id} 和 {user.name} 分别被替换为 1 和 Furion HttpRequestBuilder.Get("https://furion.net/{id}/{name?}") // "?" 结尾表示键不存在时替换为空字符串 .WithPathParameter(new { id = 1 }); HttpRequestBuilder.Get("https://furion.net/{**path}") // "**" 开头表示不对路径分割符 "/" 转义 .WithPathParameter(new { path = "files/images/photo.jpg" }); ``` 若路径参数键出现重复,则后设置的键值会覆盖先前的设置。 **模板路径语法** 模板路径除了直接使用 `{key}` 外,还支持通过 `.` 访问对象的属性、嵌套属性,以及通过 `[index]` 访问集合中的元素。此外,对象类型的属性在未找到同名属性时,会自动尝试将其视为字典,并以路径标识符作为键进行取值(等同于 `dict["key"]`)。 - `{key}`:直接替换对应的值。 - `{key.property}`:访问 `key` 对象的 `property` 属性,或当 `key` 为字典时访问键为 `"property"` 的值。 - `{key.property.nested}`:多级属性/键访问。 - `{list[0]}`:访问 `list` 集合(数组、`List` 等)中索引为 `0` 的元素。 - `{user.names[1]}`:先访问 `user` 对象的 `names` 属性,再取其索引 `1` 处的元素。 - `{dic.key}`:当 `dic` 是字典时(包括 `Dictionary` 和 `Hashtable` 等),`dic.key` 会作为 `dic["key"]` 取值。 - `{obj.dictProp.someKey[0].another}`:混合使用点号和索引,逐级深入取值。 > **字典值中的 `JSON` 嵌套取值** 当使用字典(`IDictionary`)作为数据源时,如果某个键的值本身就是合法的 `JSON` 字符串(例如对象或数组),框架会自动解析该 `JSON` 并通过 `.` 和 `[index]` 继续访问内部数据。 例如:字典中包含 `["user"] = "{\"name\":\"Monk\",\"tags\":[\"A\",\"B\"]}"`,则 `{user.name}` 将替换为 `Monk`,`{user.tags[0]}` 将替换为 `A`。这样只需要将复杂对象序列化为 `JSON` 存入字典,即可用统一占位符语法进行深层取值,极大简化模板拼接逻辑。 以上路径都支持在结尾添加 `?` 表示值不存在时替换为空字符串,以及添加 `**` 前缀表示不对路径分隔符 `/` 进行转义。 --- **配置参数** 除了通过 `{key}` 模板语法设置路径参数外,框架还提供了配置参数,用于读取配置信息进行替换操作。配置参数使用 `[[key]]` 语法,例如: ```cs showLineNumbers HttpRequestBuilder.Get("https://furion.net?id=[[id]]&name=[[name]]"); ``` **启用配置参数支持** 要在 `HttpRemote` 服务中启用配置参数支持,请按照以下步骤进行配置: ```cs showLineNumbers {2,5} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // 设置用于替换 URL 地址中配置模板参数的提供源 options.Configuration = builder.Configuration; // 若使用 Furion 框架可直接设置 App.Configuration }); ``` **配置参数的使用** 配置参数将从您的配置文件中读取并替换到 `URL` 中。例如,您的配置文件可能如下所示: ```json showLineNumbers title="appsettings.json" { "id": 1, "name": "Furion" } ``` 配置参数的键支持多种格式语法,以便更灵活地访问配置文件中的值: - `[[key]]`:直接访问 `key` 对应的值。 - `[[key:sub]]`:访问 `key` 下的 `sub` 子项的值。 - `[[key:sub:nest]]`:访问 `key` 下的 `sub` 子项中的 `nest` 子项的值。 - 备用值查找: - `[[notfound | bak]]`:如果 `notfound` 不存在,则查找 `bak`。 - `[[notfound | bak | other]]`:如果 `notfound` 和 `bak` 都不存在,则查找 `other`。 - `[[notfound | bak:sub | other:sub:nest]]`:支持更深层次的备用查找。 - 默认值: - `[[notfound || default]]`:如果 `notfound` 不存在,则使用 `default` 作为值。 - `[[notfound | bak | other || 默认值]]`:结合备用查找和默认值,确保总有值可用。 --- # 3.29 设置 Cookie > 原文链接:https://http.furion.net/docs/request-builder/setting-cookie/ 添加或修改 `Cookie`。 ```cs showLineNumbers {2-7} HttpRequestBuilder.Get("https://furion.net/") .WithCookie("id", 1) // 设置单个 Cookie,生成:id=1 .WithCookie("date", DateTime.Now, format: "yyyyMMdd") // 支持 format 格式化 .WithCookie("name", new[] { "furion", "monksoul" }) // 添加多个值,生成:name=furion,monksoul .WithCookie("DeviceId=; ASP.NET_SessionId=dr1kcfupurtqpk42dzhwvsvq; CookieLastUName=sh") // 支持 Cookie 标头值字符串 .WithCookies(new Dictionary { }) // 设置多个 Cookie .WithCookies(new { id = 1, name = "Furion" }); // 设置多个 Cookie,生成:id=1; name=Furion ``` 若 `Cookie` 键出现重复,则后设置的键值会覆盖先前的设置。 > **配置参数支持** `Cookie` 值支持配置参数,用于读取配置信息进行替换操作。配置参数使用 `[[key]]` 语法。 --- # 3.30 设置移除的 Cookies > 原文链接:https://http.furion.net/docs/request-builder/setting-cookies-to-remove/ 移除指定的 `Cookie`。 ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/") .RemoveCookies("id", "name", "age"); // 移除多个 Cookie ``` 在发送 `HTTP` 请求之前,将移除配置中指定的待移除 `Cookie` 键集合。也就是说,`RemoveCookies` 方法会在所有 `WithCookie[s]` 方法调用之后执行。 --- # 3.31 设置 HttpClient 实例的名称(多个基地址) > 原文链接:https://http.furion.net/docs/request-builder/setting-the-httpclient-instance-name-multiple-base-addresses/ 系统默认使用 `IHttpClientFactory` 创建 `HttpClient` 实例,并将默认客户端名称设为空字符串(`string.Empty`)。您可以通过指定方式设置创建 `HttpClient` 实例时的客户端名称。 ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net/") .SetHttpClientName(string.Empty); // 使用默认客户端(通常无需显式设置) HttpRequestBuilder.Get("https://furion.net/") .SetHttpClientName("weixin"); // 指定为名为 "weixin" 的客户端 ``` 您还可以在 `Startup.cs` 或 `Program.cs` 文件中为命名 `HttpClient` 客户端提供配置: ```cs showLineNumbers {2,5} // 配置默认客户端(名称为空字符串) services.AddHttpClient(string.Empty, client => { }); // 配置名为 "weixin" 的客户端 services.AddHttpClient("weixin", client => { }); ``` --- # 3.32 设置响应内容的最大缓存大小 > 原文链接:https://http.furion.net/docs/request-builder/setting-the-maximum-buffer-size-for-response-content/ 为单次请求配置响应内容的最大缓存字节数。 ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/") .SetMaxResponseContentBufferSize(10 * 1024); // 设置为 10KB ``` 如果响应内容的 `Content-Length` 超过了配置的 `MaxResponseContentBufferSize` 限制(例如,限制为 `10240` 字节),则会引发 `HttpRequestException` 异常。该异常的消息内容为:`Cannot write more bytes to the buffer than the configured maximum buffer size: '10240'.`。 > **`HttpClient` 响应内容的最大缓存字节数说明** 在 `HttpClient` 中设置响应内容的最大缓存字节数时,请确保单次请求的响应内容的最大缓存字节数不超过 `HttpClient` 配置的响应内容的最大缓存字节数。例如,如果 `HttpClient` 响应内容的最大缓存字节数设置为 `5120` 字节数,而单次请求的最大缓存字节数设为 `10240` 字节数,那么单次请求在超过 `5120` 字节数时仍会触发 `HttpRequestException` 异常。示例代码如下: ```cs showLineNumbers {1,3} services.AddHttpClient(string.Empty, client => { client.MaxResponseContentBufferSize = 5 * 1024; }); ``` 因此,**若单次请求需要更大的响应内容的最大缓存字节数,请确保 `HttpClient` 的响应内容的最大缓存字节数设置得相应更大。** --- # 3.33 设置 HttpClient 实例提供器 > 原文链接:https://http.furion.net/docs/request-builder/httpclient-provider/ 系统默认通过 `IHttpClientFactory` 来创建并自动管理 `HttpClient` 实例的生命周期。如果需要手动管理 `HttpClient` 的生命周期,可以针对单次请求单独配置一个 `HttpClient` 实例。 ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/") .SetHttpClientProvider(() => (new HttpClient(), client => client.Dispose())); ``` `SetHttpClientProvider` 方法说明: - 参数类型为 `Func<(HttpClient, Action?)>` 委托。 - 该委托返回一个元组,其中第一个元素是 `HttpClient` 实例,用于发起请求。 - 第二个元素(可选)是一个委托,用于在请求完成后释放 `HttpClient` 实例。 在请求发起前,系统会调用此委托(若存在)并获取 `HttpClient` 实例进行请求;请求完成后,如果提供了释放委托,则调用该委托以释放 `HttpClient` 实例。 --- # 3.34 添加请求内容处理器 > 原文链接:https://http.furion.net/docs/request-builder/adding-request-content-processors/ `IHttpContentProcessor` 接口定义了如何根据请求的内容类型或原始类型构建 `HttpContent` 实例。 ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net/") .AddHttpContentProcessors(() => [ new StringContentProcessor() ]); // 可添加多个处理器 ``` --- # 3.35 添加响应内容转换器 > 原文链接:https://http.furion.net/docs/request-builder/adding-response-content-converters/ `IHttpContentConverter` 接口指定了如何将响应内容 `HttpResponseMessage` 转换为目标类型的实例。 ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net/") .AddHttpContentConverters(() => [ new StringContentConverter() ]); // 可添加多个转换器 ``` --- # 3.36 启用 HttpClient 池化管理 > 原文链接:https://http.furion.net/docs/request-builder/enabling-httpclient-pooling-management/ 默认情况下,`HttpClient` 实例会在每次发送 `HTTP` 请求时被新建。但在需要频繁请求的场景中,这种做法可能引发性能瓶颈和内存占用过高的问题,特别是在压力测试期间。为了优化性能,我们可以启用 `HttpClient` 池化管理,以便在请求过程中复用 `HttpClient` 实例。 ```cs showLineNumbers {2,5-8,11} var httpRequestBuilder = HttpRequestBuilder.Get("https://furion.net/") .UseHttpClientPool(); // 启用 HttpClient 池化管理 // 循环发送请求 for (var i = 0; i < 10; i++) { await httpRemoteService.SendAsync(httpRequestBuilder); } // 释放资源以避免内存泄漏 httpRequestBuilder.ReleaseResources(); ``` > **注意事项** 在启用 `HttpClient` 池化管理后,`HttpClient` 实例将不会自动释放。因此,在完成所有请求后,必须手动调用 `httpRequestBuilder.ReleaseResources()` 方法来释放资源,以防止内存溢出。 --- # 3.37 添加请求结束时需释放的资源 > 原文链接:https://http.furion.net/docs/request-builder/adding-resources-to-release-when-the-request-ends/ 内存安全是每位程序开发者必须高度重视的问题。在发送 `HTTP` 请求的过程中,有时需要引入未托管的资源,例如,在发送文件时,需要从本地读取文件并以流的形式发送。这种情况下,若处理不当,可能会遇到流资源无法释放的问题。 为解决这一问题,我们可以添加在请求结束后,自动处理这些需要释放的资源: ```cs showLineNumbers {2,5-7,12} // 打开文件并读取文件流(没有 using) var fileStream = File.OpenRead(@"C:\Workspaces\httptest.jpg"); var httpRequestBuilder = HttpRequestBuilder.Post("https://furion.net/") .SetContent(fileStream); // 设置请求内容 .AddDisposable(fileStream) // 添加请求结束需要释放的资源 .AddDisposables(fileStream1, fileStream2); // 支持批量添加 // 发送请求 var responseMessage = await httpRemoteService.SendAsync(httpRequestBuilder); // 此时,fileStream,fileStream1 和 fileStream2 已自动释放。✅ ``` `AddDisposable` 方法可以接受任何实现了 `IDisposable` 接口的对象作为参数,并且支持重复调用,每次调用都会将新的 `IDisposable` 对象添加到集合中。 --- # 3.38 管理和释放资源 > 原文链接:https://http.furion.net/docs/request-builder/managing-and-releasing-resources/ 请参考第 `3.36` 和 `3.37` 章节内容,了解如何在请求结束时释放资源以避免内存泄漏。 ```cs showLineNumbers {2} // 释放资源以避免内存泄漏 httpRequestBuilder.ReleaseResources(); ``` 以下是 `ReleaseResources` 方法的底层实现代码,它负责管理和释放与 `HTTP` 请求相关的所有资源: ```cs showLineNumbers {1,11,14} public void ReleaseResources() { // 空检查 if (HttpClientPooling is not null) { HttpClientPooling.Release?.Invoke(HttpClientPooling.Instance); HttpClientPooling = null; } // 释放可释放的对象集合 ReleaseDisposables(); } internal void ReleaseDisposables() { // 空检查 if (Disposables.IsNullOrEmpty()) { return; } // 逐条遍历进行释放 foreach (var disposable in Disposables) { disposable.Dispose(); } // 清空集合 Disposables.Clear(); } ``` --- # 3.39 设置添加请求内容前的操作 > 原文链接:https://http.furion.net/docs/request-builder/setting-the-operation-before-adding-request-content/ 在将 `HttpContent` 实例设置给 `HttpRequestMessage` 对象的 `Content` 属性之前,您可以执行一些额外的预处理操作。 ```cs showLineNumbers {2,5-10} HttpRequestBuilder.Post("https://furion.net/") .SetOnPreSetContent(httpContent => { // 示例:为请求内容设置 Content-Disposition 请求标头 httpContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { Name = multipartFormDataItem.Name, FileName = multipartFormDataItem.FileName, Size = multipartFormDataItem.FileSize }; }); ``` **注意**:`SetOnPreSetContent` 方法支持多次调用,每次调用的结果会累积叠加。 --- # 3.40 设置发送请求前的操作 > 原文链接:https://http.furion.net/docs/request-builder/setting-the-operation-before-sending-the-request/ 在发送 `HTTP` 远程请求之前,您可以执行一些预处理操作。 ```cs showLineNumbers {2,5} HttpRequestBuilder.Post("https://furion.net/") .SetOnPreSendRequest(requestMessage => { // 示例:添加名为 "header1" 的请求标头 requestMessage.Headers.TryAddWithoutValidation("header1", "value1"); }); ``` **注意**:`SetOnPreSendRequest` 方法支持多次调用,每次调用的结果会累积叠加。 --- # 3.41 设置收到响应后的操作 > 原文链接:https://http.furion.net/docs/request-builder/setting-the-operation-after-receiving-the-response/ 在接收到 `HTTP` 响应之后,您可以执行一些后续处理操作。 ```cs showLineNumbers {2,5} HttpRequestBuilder.Post("https://furion.net/") .SetOnPostReceiveResponse((responseMessage, cancellationToken) => { // 示例:打印响应状态码 Console.WriteLine(responseMessage.StatusCode); return Task.CompletedTask; }); ``` **注意**:`SetOnPostReceiveResponse` 方法支持多次调用,每次调用的结果会累积叠加。 --- # 3.42 设置发送请求失败时的处理 > 原文链接:https://http.furion.net/docs/request-builder/setting-the-handling-when-sending-the-request-fails/ 当 `HTTP` 请求发送过程中发生异常时,您可以执行一些错误处理操作。 ```cs showLineNumbers {2,5} HttpRequestBuilder.Post("https://furion.net/") .SetOnRequestFailed((exception, responseMessage) => // 注意:responseMessage 可能为空 { // 示例:打印异常信息 Console.WriteLine(exception.Message); }); ``` **注意**:可将 `SetOnRequestFailed` 与异常抑制方法 `SuppressExceptions()` 配合使用,以便在不中断程序流程的前提下捕获并处理请求失败信息。 > **推荐使用 `WithStatusCodeHandler` 方式** 推荐使用第 3.51 章节的 `WithStatusCodeHandler` 设置响应状态码处理程序方法。当需要针对特定状态码(如服务器异常状态码,即大于 `500`)进行处理时,可以这样做: ```cs showLineNumbers {3} HttpRequestBuilder.Get("https://furion.net/") // 表示状态码大于等于 500 配置回调处理 .WithStatusCodeHandler(">=500", async (responseMessage, cancellationToken) => { Console.WriteLine("调用状态码处理程序"); }) ``` --- # 3.43 确保请求成功 > 原文链接:https://http.furion.net/docs/request-builder/ensuring-the-request-succeeds/ 启用该功能后,当 `HTTP` 响应的状态码不在 `200-299` 范围内时(即 `IsSuccessStatusCode` 属性为 `false`),将自动抛出异常。 ```cs showLineNumbers {2,5} HttpRequestBuilder.Post("https://furion.net/") .EnsureSuccessStatusCode(); HttpRequestBuilder.Post("https://furion.net/") .EnsureSuccessStatusCode(false); // 关闭验证 ``` --- # 3.44 设置 Basic 身份认证 > 原文链接:https://http.furion.net/docs/request-builder/setting-basic-authentication/ 向请求中添加 `Authorization` 标头,其值为 `Basic` 关键字后接由 `用户名:密码` 字符串的 `Base64` 编码组成。 ```cs showLineNumbers{2,5} HttpRequestBuilder.Post("https://furion.net/") .AddBasicAuthentication("username", "password"); HttpRequestBuilder.Post("https://furion.net/") .AddBasicAuthentication("username", null); // 支持密码设置空 ``` > **`Authorization` 标头值格式** `Authorization` 标头的值遵循 `Schema 值` 的格式,即 `Basic` 后紧跟一个空格,然后是经过 `Base64` 编码的 `用户名:密码` 字符串。 --- # 3.45 设置 Bearer 身份认证(JWT) > 原文链接:https://http.furion.net/docs/request-builder/setting-bearer-authentication-jwt/ 向请求中添加 `Authorization` 标头,格式为 `Bearer` 关键字后接 `Token` 令牌字符串组成。 ```cs showLineNumbers{2,5} HttpRequestBuilder.Post("https://furion.net/") .AddBearerAuthentication("your-token"); HttpRequestBuilder.Post("https://furion.net/") .AddBearerAuthentication("X-Authorization", "your-token"); // 支持自定义标头键 ``` > **`Authorization` 标头值格式** `Authorization` 标头的值遵循 `Schema 值` 的格式,即 `Bearer` 后紧跟一个空格,然后是 `Token` 令牌字符串。 --- # 3.46 设置 Digest 摘要身份认证 > 原文链接:https://http.furion.net/docs/request-builder/setting-digest-authentication/ 向请求中添加 `Authorization` 标头,格式为 `Digest` 关键字后接由用户名和密码生成的摘要字符串组成。 ```cs showLineNumbers{2} HttpRequestBuilder.Post("https://furion.net/") .AddDigestAuthentication("username", "password"); ``` > **`Authorization` 标头值格式** `Authorization` 标头的值遵循 `Schema 值` 的格式,即 `Digest ` 后紧跟一个空格,然后是用户名和密码生成的摘要字符串。 --- # 3.47 设置自定义身份认证 > 原文链接:https://http.furion.net/docs/request-builder/setting-custom-authentication/ 向请求中添加自定义的 `Authorization` 标头,遵循 `Schema 值` 的格式。 ```cs showLineNumbers{2,5} HttpRequestBuilder.Post("https://furion.net/") .AddAuthentication(new AuthenticationHeaderValue("your-schema", "your-secret")); HttpRequestBuilder.Post("https://furion.net/") .AddAuthentication("your-schema", "your-secret"); // 重载版本,简化 new AuthenticationHeaderValue 操作 ``` > **`Authorization` 标头值格式** `Authorization` 标头的值遵循 `Schema 值` 的格式,即 `your-schema` 后紧跟一个空格,然后是对应的身份凭证字符串(`your-secret`)。 --- # 3.48 禁用 HTTP 缓存 > 原文链接:https://http.furion.net/docs/request-builder/cache/ 在发送 `HTTP GET` 请求时,服务器可能会缓存该请求的结果以提高性能。为了取消其缓存行为,可以在添加以下操作: ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net/") .DisableCache(); HttpRequestBuilder.Get("https://furion.net/") .DisableCache(false); // 启用缓存(默认) ``` 在添加该操作之后,`HTTP` 请求将在发送前自动附带以下请求标头,以确保缓存控制: ```bash showLineNumbers Cache-Control: must-revalidate, no-cache, no-store Pragma: no-cache If-None-Match: "" ``` --- # 3.49 设置请求处理程序 > 原文链接:https://http.furion.net/docs/request-builder/message-handler/ `IHttpRequestEventHandler` 接口允许您定义 `HTTP` 请求的预处理操作。通过实现该接口,您可以创建自定义的请求处理程序,例如 `CustomRequestEventHandler` 类: ```cs showLineNumbers {1} public class CustomRequestEventHandler : IHttpRequestEventHandler { // 在发送 HTTP 请求之前的操作 public void OnPreSendRequest(HttpRequestMessage httpRequestMessage) {} // 在收到 HTTP 响应之后的操作 public Task OnPostReceiveResponseAsync(HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken) {} // 当发送 HTTP 请求发生异常时的操作 public void OnRequestFailed(Exception exception, HttpResponseMessage? httpResponseMessage = null) {} } ``` 要在应用程序中启用此处理程序,请在 `Startup.cs` 或 `Program.cs` 文件中注册 `CustomRequestEventHandler` 服务: ```cs showLineNumbers services.TryAddSingleton(); ``` 接下来,您可以在构建 `HTTP` 请求时指定此处理程序: ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net/") .SetEventHandler(); HttpRequestBuilder.Get("https://furion.net/") .SetEventHandler(typeof(CustomRequestEventHandler)); // 使用类型方式设置 ``` > **复用提示** 您可以创建自定义的 `IHttpRequestEventHandler` 接口实现类型,并在多个 `HttpRequestBuilder` 实例中复用该实现。 ### 全局事件处理器 除了在每个请求上单独配置,您还可以通过 `HttpClientOptions` 为特定 `HttpClient` 实例设置全局事件处理器。该处理器将对该客户端发出的所有请求生效。 ```cs showLineNumbers {5} // 配置默认客户端 services.AddHttpClient(string.Empty) .ConfigureOptions(options => // 或使用重载:.ConfigureOptions((options, serviceProvider) => { options.HttpRequestEventHandler = new CustomRequestEventHandler(); }); // 配置特定客户端 services.AddHttpClient("weixin") .ConfigureOptions(options => // 或使用重载:.ConfigureOptions((options, serviceProvider) => { options.HttpRequestEventHandler = new CustomRequestEventHandler(); }); ``` > **执行顺序说明** 事件处理器的执行遵循以下优先级(先全局,后特定,再内联): 1. **全局事件处理器**(`HttpClientOptions.HttpRequestEventHandler`) 2. **特定事件处理器**(通过 `SetEventHandler` 方法注册的 `IHttpRequestEventHandler` 实现类) 3. **构建器内联回调**(通过 `SetOnPreSendRequest`、`SetOnPostReceiveResponse`、`SetOnRequestFailed` 方法设置的回调) 即执行顺序为:**全局 → 特性 → 内联回调**。 --- # 3.50 模拟浏览器环境(爬虫检测) > 原文链接:https://http.furion.net/docs/request-builder/browser/ 在开发爬虫程序时,目标网站可能会根据用户代理(`User-Agent`)或其他因素提供不同的页面版本,如 `PC` 端和移动端。此外,一些网站还具备反爬虫机制,能够识别并阻止爬虫程序的访问。为应对这些问题,我们可以配置请求标头以模拟真实的浏览器环境进行请求。 ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://www.baidu.com/") .SimulateBrowser(); // 模拟 PC 浏览器环境 HttpRequestBuilder.Get("https://www.baidu.com/") .SimulateBrowser(isMobile: true); // 模拟移动端浏览器环境 ``` 在添加该操作之后,`HTTP` 请求将在发送前自动附带以下请求标头,以确保服务器能够准确识别并处理请求: ```bash showLineNumbers {2,5} # PC 浏览器代理 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0 # 移动端浏览器代理 Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Mobile Safari/537.36 Edg/142.0.0.0 ``` --- # 3.51 添加响应状态码处理程序 > 原文链接:https://http.furion.net/docs/request-builder/statuscode/ 在发送 `HTTP` 请求并接收响应时,我们经常需要根据不同的响应状态码执行特定的操作。为了实现这一需求,`HttpRequestBuilder` 提供了 `WithStatusCodeHandler` 方法,允许我们为特定的状态码配置回调处理逻辑。 以下是如何使用 `WithStatusCodeHandler` 方法的示例代码: ```cs showLineNumbers {3,8,13,18,23,28,33,38} HttpRequestBuilder.Get("https://furion.net/") // 为状态码 200 配置回调处理 .WithStatusCodeHandler(200, async (responseMessage, cancellationToken) => { Console.WriteLine("调用状态码处理程序"); }) // 为状态码 200 配置回调处理 .WithStatusCodeHandler(HttpStatusCode.OK, async (responseMessage, cancellationToken) => { Console.WriteLine("调用状态码处理程序"); }) // 为 200 ~ 299(含) 区间状态码配置回调处理 .WithStatusCodeHandler("200-299", async (responseMessage, cancellationToken) => // 等价于 "200~299" { Console.WriteLine("调用状态码处理程序"); }) // 支持比较符号的类型: >=200, <=300, <100, =100, >100 .WithStatusCodeHandler(">=200", async (responseMessage, cancellationToken) => { Console.WriteLine("调用状态码处理程序"); }) // 为状态码 200、204 和 500 配置统一的回调处理 .WithStatusCodeHandler([200, 204, 500], async (responseMessage, cancellationToken) => { Console.WriteLine("调用状态码处理程序"); }) // 为所有状态码配置统一的回调处理 .WithAnyStatusCodeHandler(async (responseMessage, cancellationToken) => { Console.WriteLine("调用状态码处理程序"); }) // 为 200~299 状态码(请求成功)配置统一的回调处理 .WithSuccessStatusCodeHandler(async (responseMessage, cancellationToken) => { Console.WriteLine("调用状态码处理程序"); }) // 支持多种状态码表示方式,包括 HttpStatusCode 枚举、字符串表示的状态码、状态码区间、比较符号的类型及通配符 .WithStatusCodeHandler([200, "204", HttpStatusCode.InternalServerError, "200-299", ">=200", "*"], async (responseMessage, cancellationToken) => { Console.WriteLine("调用状态码处理程序"); }); ``` > **状态码参数类型** `WithStatusCodeHandler` 方法的状态码参数支持多种类型: - 正整数类型,例如 `200`。 - 字符串类型,例如 `"200"`。 - `HttpStatusCode` 枚举类型,例如 `HttpStatusCode.OK`。 - 字符串区间类型,例如 `"200-500"` 或 `"200~500"`,表示该区间内的所有状态码。 - 包含比较符号的类型,如:`">=200"`(大于等于)、`"<=300"`(小于等于)、`"<100"`(小于)、`"=100"`(等于)和 `">100"`(大于)特定状态码。 - 特殊字符串 `"*"`,表示匹配所有状态码。 - 上述类型的集合,允许组合使用以匹配多个状态码。 这样,您可以根据实际需求灵活设置状态码参数。 通过 `WithStatusCodeHandler` 方法,我们可以灵活地根据响应状态码执行不同的操作,从而增强 `HTTP` 请求与响应的处理能力。 --- # 3.52 启用请求分析工具 > 原文链接:https://http.furion.net/docs/request-builder/profiler/ 在现代化的浏览器中,通常内置了开发者工具,这些工具能够捕获并直观展示用户访问网站时的所有请求与响应数据。类似地,我们也为 `HTTP` 远程请求模块提供了一套分析工具。 ```cs showLineNumbers {2,5,9-12,18} HttpRequestBuilder.Get("https://furion.net") .Profiler(); // 或使用 Debugger() HttpRequestBuilder.Get("https://furion.net") .Profiler(false); // 禁用请求分析工具,或调用任一函数:DisableProfiler()、DisableDebugger()、Debugger(false) // 获取请求分析工具的数据 HttpRequestBuilder.Get("https://furion.net") .Profiler(analyzer => { Console.WriteLine(analyzer.Data); }); HttpRequestBuilder.Get("https://furion.net") .Profiler(analyzer => { Console.WriteLine(analyzer.Data); }, false); // 禁用请求分析工具 ``` 启用后,当执行 `HTTP` 远程请求时,控制台将输出如下详细信息: ```bash showLineNumbers Request Headers: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0 X-Header: custom General: Request URL: https://furion.net/ Request Method: GET Status Code: 200 OK HTTP Version: 1.1 HTTP Content: Content Type: HttpClient Name: Request Duration (ms): 149.00 Response Headers: Server: nginx/1.22.1 Date: Thu, 14 Nov 2024 15:35:41 GMT Connection: keep-alive Vary: Accept-Encoding ETag: "67091697-f32f" Cache-Control: max-age=315360000 Accept-Ranges: bytes Content-Type: text/html Content-Length: 62255 Last-Modified: Fri, 11 Oct 2024 12:14:15 GMT Expires: Thu, 31 Dec 2037 23:55:55 GMT ``` > **关于 `Blazor WebAssembly` 项目的说明** 在 `Blazor WebAssembly` 应用中,请求分析工具的内容将在客户端(即浏览器)的开发者工具控制台中显示。请确保在开发过程中检查此控制台以获取相关分析信息。 此外,除了为单个请求启用分析工具,还可以全局注册以在 `HttpClient` 中启用: ```cs showLineNumbers {3,7,10,13-14,17-18,21-22} // 为默认客户端启用 services.AddHttpClient(string.Empty) .AddProfilerDelegatingHandler(); // 还可以提供条件禁用,例如生产环境中禁用 services.AddHttpClient(string.Empty) .AddProfilerDelegatingHandler(disableIn: () => builder.Environment.EnvironmentName == "Production"); services.AddHttpClient(string.Empty) .AddProfilerDelegatingHandler(disableInProduction: true); // 为特定客户端启用 //services.AddHttpClient("weixin") // .AddProfilerDelegatingHandler(); // 还可以一键为所有客户端配置启用 services.ConfigureHttpClientDefaults(clientBuilder => clientBuilder.AddProfilerDelegatingHandler()); // 或使用 IHttpRemoteBuilder 扩展方法进行一键配置 services.AddHttpRemote() .ConfigureHttpClientDefaults(clientBuilder => clientBuilder.AddProfilerDelegatingHandler()); ``` 通过启用请求分析工具,开发者能够更直观、便捷地观察和调试 `HTTP` 请求,从而提升开发效率与调试准确性。 > **生产环境禁用** 为了确保生产环境的最佳性能和安全性,建议在生产环境中**禁用**请求分析工具。 此外,打印请求内容时可能会导致 `Stream` 对象被重复读取或变得不可读,因为流会被提前读取到内存中,其 `Position` 随之移动到尾部。 **补充说明:** 请求分析工具默认仅展示请求或响应内容中最多 `5KB` 的内容数据。 --- # 3.53 设置客户端偏好的语言和区域 > 原文链接:https://http.furion.net/docs/request-builder/setting-the-client-preferred-language-and-region/ 全球化是互联网应用产品的发展趋势,因此,面向全球的应用产品应具备国际化功能。发送 `HTTP` 请求时,可通过添加 `Accept-Language` 标头来指定客户端偏好的自然语言和区域。 ```cs showLineNumbers {2,5,8} HttpRequestBuilder.Get("https://furion.net") .AcceptLanguage("en-US"); HttpRequestBuilder.Get("https://furion.net") .AcceptLanguage("zh-CN,en;q=0.5"); HttpRequestBuilder.Get("https://furion.net") .AcceptLanguage("fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5"); ``` --- # 3.54 设置 HttpRequestMessage 属性 > 原文链接:https://http.furion.net/docs/request-builder/setting-httprequestmessage-properties/ 在特定场景下,我们可能需要为 `HttpRequestMessage` 请求添加额外的属性,而非通过请求标头。这时,可以如下操作: ```cs showLineNumbers {2-4} HttpRequestBuilder.Get("https://furion.net") .WithProperty("key1", "vallue2") // 设置单个属性 .WithProperties(new Dictionary {}) // 设置多个属性 .WithProperties(new { id = 1, name = "Furion" }); // 设置多个属性 ``` 这些属性会被添加到 `HttpRequestMessage` 对象的 `Options` 属性中([参考文档](https://learn.microsoft.com/zh-cn/dotnet/api/system.net.http.httprequestmessage.options))。要获取这些值,可以这样做: ```cs showLineNumbers httpRequestMessage.Options.TryGetValue(new HttpRequestOptionsKey("key1"), out var value); ``` 若属性键出现重复,则后设置的键值会覆盖先前的设置。 > **小提示** 此功能常被集成在自定义的 `DelegatingHandler` 和 `IHttpRequestEventHandler` 组件中。 --- # 3.55 启用标准请求标头 > 原文链接:https://http.furion.net/docs/request-builder/enabling-standard-request-headers/ 为了提升应用通过 `HTTP` 客户端发送网络请求的兼容性并避免被 `WAF`(`Web` 应用防火墙)拦截,框架提供了一键式配置方法,方便快速统一设置标准的请求头部: ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net") .UseStandardRequestHeaders(); HttpRequestBuilder.Get("https://furion.net") .UseStandardRequestHeaders(false); // 关闭标准请求标头 ``` 此外,除了为单个请求启用标准标头配置,还可以全局注册以在 `HttpClient` 中启用: ```cs showLineNumbers {4} // 为默认客户端启用 services.AddHttpClient(string.Empty, client => { client.UseStandardRequestHeaders(); }); services.AddHttpRemote(); ``` 启用标准请求标头后,请求将自动添加以下头部: - **`Accept`**:`application/json`、`text/plain;q=0.9`、`*/*;q=0.8`(明确的媒体类型优先级,避免被 `WAF` 拦截) - **`Connection`**:启用持久连接(`Keep-Alive`),减少 `TCP` 连接建立和关闭的开销 --- # 3.56 设置自动 Host 标头 > 原文链接:https://http.furion.net/docs/request-builder/setting-the-automatic-host-header/ `Host` 标头是 `HTTP/1.1` 协议中的一个必需标头。`Host` 标头用于指定请求的目标服务器的主机名和端口号,确保服务器能正确区分同一 `IP` 地址上的不同域名并进行相应处理。框架提供了简便的方法进行设置: ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net") .AutoSetHostHeader(); // 启用 HttpRequestBuilder.Get("https://furion.net") .AutoSetHostHeader(false); // 关闭自动 Host 标头 ``` 启用后,发送 `HTTP` 远程请求时会自动添加 `Host: furion.net` 标头。 > **小提示** 当对接旧程序提供的 `API` 接口时,建议启用该配置以提升兼容性。 > **`HttpClient` 自动重定向导致的 `Host` 问题** 在发送 `HTTP` 远程请求时,如果目标服务器返回重定向响应(如 `301 Moved Permanently` 或 `302 Found`),框架默认会自动跟随重定向。然而,当启用自动 `Host` 标头时,可能会遇到无法更新 `Host` 标头的问题。此时,可以关闭 `AllowAutoRedirect` 选项,使框架能够正确处理重定向: ```cs showLineNumbers {3,5} // 配置默认客户端 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { AllowAutoRedirect = false }); ``` 另外,如需设置框架内置重定向行为的最大重定向次数,可以使用以下方式: ```cs showLineNumbers {2,4} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { MaximumAutomaticRedirections = 20; }); ``` 这样,可以确保重定向行为符合预期,同时避免 `Host` 标头设置错误的问题。 --- # 3.57 配置请求基地址 > 原文链接:https://http.furion.net/docs/request-builder/configuring-the-request-base-address/ 当需要对接多个第三方 `API` 时,我们通常会全局注册并配置多个 `HttpClient` 实例的 `BaseAddress`。例如: ```cs showLineNumbers {4,10} // 配置默认客户端的基地址 services.AddHttpClient(string.Empty, client => { client.BaseAddress = new Uri("https://furion.net/"); }); // 配置GitHub客户端的基地址 services.AddHttpClient("github", client => { client.BaseAddress = new Uri("https://github.com/"); }); ``` 随后可通过 `.SetHttpClientName(客户端名称)` 方法指定要使用的客户端。 除了全局配置,框架还支持在单次请求中局部设置基地址的方法,允许在构建请求时动态指定: ```cs showLineNumbers {3,7,11} // 使用字符串设置基地址 HttpRequestBuilder.Get("/api/test") .SetBaseAddress("https://furion.net"); // 使用 Uri 对象设置基地址 HttpRequestBuilder.Get("/api/test") .SetBaseAddress(new Uri("https://furion.net")); // 可充当前缀使用 HttpRequestBuilder.Get("/api/test") .SetBaseAddress("/furion"); ``` > **特别说明** 请确保设置的请求基地址是绝对路径,即以 `http://` 或 `https://` 开头。 **处理逻辑说明**: - 若请求地址为绝对地址,则直接使用该地址发送请求。 - 若请求地址为相对地址: - 未设置局部 `BaseAddress` 时,将其与全局 `HttpClient` 实例的 `BaseAddress` 拼接,作为最终请求地址。 - 已设置局部 `BaseAddress` 时: - 若局部 `BaseAddress` 为相对地址,则先将局部 `BaseAddress` 拼接到请求地址前,再与全局 `BaseAddress` 拼接。 - 若局部 `BaseAddress` 为绝对地址,则直接将该绝对地址与请求地址拼接,作为最终请求地址(此时忽略全局 `BaseAddress`)。 > **配置参数支持** 请求基地址支持配置参数,用于读取配置信息进行替换操作。配置参数使用 `[[key]]` 语法。 --- # 3.58 配置来源地址 Referer > 原文链接:https://http.furion.net/docs/request-builder/configuring-the-referer-referrer-address/ 当访问某些第三方服务器时,服务器可能会验证请求头中的 `Referer` 来源地址。例如在下载图片时,可能因触发防盗链机制导致获取的图片不符合预期。此时,可通过 `SetReferer` 方法来设置 `Referer` 请求头,模拟来源页面以绕过防盗链检测。 ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/logo.png") .SetReferer("https://furion.net/"); // 伪造是从首页发出的请求 ``` 为简化配置,框架提供了内置模板字符串 `"{BASE_ADDRESS}"`,可自动提取请求地址的基地址作为 `Referer`: ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/logo.png") .SetReferer("{BASE_ADDRESS}"); // 发送时自动替换 {BASE_ADDRESS} 为 https://furion.net/ ``` --- # 3.59 配置 User-Agent > 原文链接:https://http.furion.net/docs/request-builder/configuring-user-agent/ `User-Agent` 请求标头是一个特征字符串,使得服务器和对等网络能够识别发出请求的用户代理的应用程序、操作系统、供应商或版本信息。框架提供了 `SetUserAgent` 方法来设置 `User-Agent` 请求头。 ```cs showLineNumbers {2-6} HttpRequestBuilder.Get("https://furion.net/logo.png") .SetUserAgent(UserAgents.Chrome.PC) // 使用 UserAgents 静态类便捷设置 .SetUserAgent(UserAgents.Chrome.Mobile) // 设置为移动端 User-Agent .SetUserAgent(UserAgents.GetRandom()) // 随机获取一个浏览器类型的 User-Agent .SetUserAgent(UserAgents.GetByBrowser("Safari")) // 获取指定浏览器类型的 User-Agent .SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"); // 或直接设置 User-Agent 字符串 ``` > **默认 `User-Agent`** 若请求未显式设置 `User-Agent`,框架将默认使用 `UserAgents.Edge.PC` 作为其值。 `UserAgents` 是框架提供的静态类,用于便捷设置主流浏览器的 `User-Agent` 标识,包含以下成员: - **静态方法**: - `GetRandom(isMobile)`: 随机获取一个浏览器的 `User-Agent`。 - `GetByBrowser(browser, isMobile)`:获取指定浏览器类型的 `User-Agent`。 - **静态成员**: - `Chrome`:Google Chrome 浏览器 - `PC`:桌面端 `User-Agent` 标识 - `Mobile`:移动端 `User-Agent` 标识 - `Firefox`:Mozilla Firefox 浏览器 - `PC`:桌面端 `User-Agent` 标识 - `Mobile`:移动端 `User-Agent` 标识 - `Safari`:Apple Safari 浏览器 - `PC`:桌面端 `User-Agent` 标识 - `Mobile`:移动端 `User-Agent` 标识 - `Edge`:Microsoft Edge 浏览器 - `PC`:桌面端 `User-Agent` 标识 - `Mobile`:移动端 `User-Agent` 标识 - `Opera`:Opera 浏览器 - `PC`:桌面端 `User-Agent` 标识 - `Mobile`:移动端 `User-Agent` 标识 - `Generic`:通用浏览器 - `PC`:桌面端 `User-Agent` 标识 - `Mobile`:移动端 `User-Agent` 标识 --- # 3.60 配置 HTTP 版本 > 原文链接:https://http.furion.net/docs/request-builder/configuring-the-http-version/ 在发起 `HTTP` 远程请求时,默认采用的 `HTTP` 协议版本为 `1.1`。不过,在访问部分第三方服务器时,这些服务器可能会对 `HTTP` 版本进行校验(例如,要求使用 `2.0` 版本)。此时,可以通过以下两种方式进行设置: - **单次请求设置** ```cs showLineNumbers {2-4} HttpRequestBuilder.Post("https://furion.net/") .SetVersion(HttpVersion.Version20); // 推荐 .SetVersion("1.2"); // 重载方法 .SetVersion(new Version("1.2")); // 重载方法 ``` - **全局配置** ```cs showLineNumbers {2,4,8,10} // 配置默认客户端 services.AddHttpClient(string.Empty, client => { client.DefaultRequestVersion = HttpVersion.Version10; }); // 配置特定客户端 services.AddHttpClient("weixin", client => { client.DefaultRequestVersion = HttpVersion.Version10; }); ``` --- # 3.61 异常抑制机制(静默处理) > 原文链接:https://http.furion.net/docs/request-builder/suppression/ 在发起 `HTTP` 远程请求时,可能会遇到以下异常情况: - 目标主机不可达 - 请求被取消 - 请求超时 - 其他网络异常 默认情况下,这些异常会中断程序执行。虽然开发者通常使用 `try/catch` 进行异常处理,但在某些场景下,我们更希望异常发生时静默返回 `null` 而不中断流程。为此,框架提供了灵活的异常抑制功能。 - **抑制所有请求异常** ```cs showLineNumbers {2} var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/") .SuppressExceptions()); // 抑制所有异常 ``` 当请求发生异常时,代码不会中断,而是返回 `null`,即 `httpResponseMessage` 的值为 `null`。 在某些场景下,我们希望在抑制异常的同时,仍能捕获异常信息(例如记录到日志中),而不中断程序的正常执行。此时,可以通过 `SetOnRequestFailed` 回调来实现: ```cs showLineNumbers {2-3} HttpRequestBuilder.Post("https://furion.net/") .SuppressExceptions() .SetOnRequestFailed((exception, responseMessage) => // 注意:responseMessage 可能为空 { Console.WriteLine(exception.Message); }); ``` 该方法允许你在异常被抑制后,安全地处理错误信息,适用于日志记录、监控或其他错误响应逻辑。 - **仅抑制特定类型的异常** 框架还支持仅抑制特定类型的异常。例如,可以仅抑制超时异常和请求取消异常: ```cs showLineNumbers {2} var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/") .SuppressExceptions([typeof(TimeoutException), typeof(TaskCanceledException)])); // 抑制超时和取消异常 ``` - **禁用异常抑制配置** 若需恢复默认行为(即异常发生时中断程序),可显式禁用异常抑制: ```cs showLineNumbers {2} var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/") .SuppressExceptions(false); // 恢复缺省配置 ``` 此配置等效于未调用 `SuppressExceptions()`,当发生任何异常时,程序将中断执行。 > **注意事项** 当启用异常抑制功能时,请注意以下事项: 1. **覆盖规则** 多次调用 `SuppressExceptions()` 或相关配置时,**仅最后一次调用生效**。 2. **状态码检查与异常抑制的优先级** 即使已配置 `EnsureSuccessStatusCode()`,被抑制的异常仍会返回 `null`,不会触发状态码检查逻辑。 3. **异常抑制的优先级** 异常抑制功能的优先级高于状态码检查。如果同时启用状态码检查和异常抑制,异常抑制会优先生效。 4. **请求拦截器依旧可用** 若通过 `SetOnRequestFailed(ex, res)` 或其他请求处理机制捕获异常,即使异常被抑制,拦截器或回调方法仍会被调用。 5. **异常类型选择建议** 应根据具体业务场景谨慎选择需要抑制的异常类型,避免因过度抑制异常而掩盖潜在问题。 6. **自动输出抑制日志** 当异常被成功抑制时,框架会自动输出一条 `Warning` 级别日志(例如 `"An exception occurred but was suppressed by SuppressExceptionPipelineHandler."`),便于排查问题。 --- # 3.62 移除内容的默认 Content-Type > 原文链接:https://http.furion.net/docs/request-builder/removing-the-default-content-type-of-content/ 在与部分较老版本的 `HTTP` 服务进行对接时,如果发送请求内容时设置了 `Content-Type` 请求标头,可能会导致请求处理异常。而现代 `HTTP` 接口通常不会存在此类限制。 若在发送请求时需要移除请求内容对应的 `Content-Type` 请求标头,可以按照以下方式进行设置: ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net") .SetOmitContentType(true); // 移除内容默认的 Content-Type ``` 除了采用上述方法外,另一种实现方式是利用 `SetOnPreSetContent(Action)` 方法,在设置请求内容之前修改请求头,将 `Content-Type` 置为 `null`: ```cs showLineNumbers {2,4} HttpRequestBuilder.Post("https://furion.net") .SetOnPreSetContent(httpContent => { httpContent.Headers.ContentType = null; }); ``` --- # 3.63 条件化配置构建器 > 原文链接:https://http.furion.net/docs/request-builder/conditional-configuration-builder/ 在构建 `HTTP` 远程请求的构建器实例时,常需根据不同条件动态配置请求参数。例如,当用户执行搜索操作时,应将 `?search=关键字` 查询参数添加到请求中;反之,则无需添加。 针对此类场景,`HttpRequestBuilder` 提供了 `When` 方法,用于根据指定条件执行相应的配置操作。该方法支持链式调用,使代码更加简洁清晰。 ```cs showLineNumbers {2-3} HttpRequestBuilder.Post("https://furion.net") .When(!string.IsNullOrEmpty(token), b => b.AddBearerAuthentication(token)) .When(!string.IsNullOrEmpty(keyword), b => b.WithQueryParameter("search", keyword)); ``` **示例代码说明:** - 当 `token` 不为空或 `null` 时,自动添加 `Bearer` 认证。 - 当 `keyword` 不为空或 `null` 时,将 `search` 查询参数添加到请求中。 该方式可灵活应用于各种条件判断场景,提升代码的可维护性和可读性。 --- # 3.64 启用断言功能 > 原文链接:https://http.furion.net/docs/request-builder/enabling-assertions/ 在开发或编写单元测试、集成测试时,我们经常需要验证请求内容和响应结果是否符合预期,这一过程通常称为“断言”。断言分为两类: - **请求断言**:在构建完 `HttpRequestMessage` 后、发送前执行,用于校验请求的 `URI`、方法、标头、主体等。若失败,请求不会发出。 - **响应断言**:在收到 `HttpResponseMessage` 后执行,用于校验状态码、响应头、响应体、耗时等。 若断言失败,系统会抛出 `HttpAssertionException` 异常。 要启用断言功能,需调用 `UseAssertions()` 方法,并配合 `Asserts(configure)` 方法使用: ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net") .UseAssertions(); HttpRequestBuilder.Get("https://furion.net") .UseAssertions(false); // 禁用断言功能 ``` --- # 3.65 配置断言逻辑 > 原文链接:https://http.furion.net/docs/request-builder/configuring-assertion-logic/ 启用断言功能后,可通过 `Asserts(configure)` 方法统一配置请求断言与响应断言: ```cs showLineNumbers {2-7} HttpRequestBuilder.Get("https://furion.net") .UseAssertions() .Asserts(ast => ast .RequestMethod(HttpMethod.Get) // 请求断言 .ResponseStatusCode(200) // 响应断言 .ResponseHeaderExists("encoding") ); ``` 其中,`ast` 参数为 `HttpAssertionBuilder` 类型,内置了丰富的断言方法(支持自定义扩展): ### 请求断言方法(发送前执行) - **`RequestUri(expectedUri)`**:断言请求 `URI` 等于指定字符串 - 失败时抛出:`Expected request URI to be '{expectedUri}', but found '{actual}'.` - **`RequestMethod(expectedMethod)`**:断言 `HTTP` 方法等于指定的 `HttpMethod` - 失败时抛出:`Expected request method to be {expectedMethod}, but found {actual}.` - **`RequestHeaderExists(name)`**:断言指定的请求标头存在(包括内容标头) - 失败时抛出:`Expected request header '{name}' to exist, but it was not found.` - **`RequestHeaderEquals(name, expectedValue)`**:断言请求标头的第一个值严格等于指定字符串(区分大小写) - 失败时抛出:`Expected request header '{name}' to be '{expectedValue}', but found '{actual}'.` - **`RequestHeaderContains(name, expectedValue)`**:断言请求标头任意值包含指定子字符串(不区分大小写) - 失败时抛出:`Expected request header '{name}' to contain '{expectedValue}', but the header was not found.` 或 `Expected request header '{name}' to contain '{expectedValue}', but actual values were: [{...}].` - **`RequestContentContains(expectedSubstring)`**:断言请求内容包含指定子字符串(不区分大小写) - 失败时抛出:`Expected request content to contain '{expectedSubstring}', but it was not found.` - **`RequestContentEquals(expected)`**:断言请求内容完全等于指定字符串 - 失败时抛出:`Expected request content to be '{expected}', but found '{actual}'.` - **`RequestSatisfies(assertion)`**:自定义请求断言(同步或异步),直接操作 `HttpRequestMessage` - 异步重载接受 `Func`。 ### 响应断言方法(收到响应后执行) - **`AddAssertion(assertion)`**:添加自定义断言委托(默认视为响应断言),如 `ast.AddAssertion(async context => await ...)`。 - **`ResponseStatusCode(statusCode)`**:断言响应状态码等于指定值(整数或 `HttpStatusCode`) - 失败时抛出:`Expected response status code to be {expected}, but found {actual}.` - **`ResponseStatusCodeIn(allowedStatusCodes)`**:断言状态码在允许列表中 - 失败时抛出:`Expected response status code to be one of [{string.Join(", ", allowedStatusCodes)}], but found {actual}.` - **`ResponseIsSuccessStatusCode()`**:断言请求成功(状态码为 `2xx`) - 失败时抛出:`Expected response to be successful (2xx status code), but found status code {(int)context.StatusCode}.` - **`ResponseContentContains(expectedSubstring)`**:断言响应内容包含指定子字符串(不区分大小写) - 失败时抛出:`Expected response content to contain '{expectedSubstring}', but it was not found.` - **`ResponseContentEquals(expected)`**:断言响应内容完全等于指定的字符串 - 失败时抛出:`Expected response content to be '{expected}', but found '{content}'.` - **`ResponseContentMatches(pattern)`**:断言响应内容与指定的正则表达式匹配 - 失败时抛出:`Expected response content to match regex '{pattern}', but it did not.` - **`ResponseContentNotEmpty()`**:断言响应内容不为空 - 失败时抛出:`Expected response content not to be empty.` - **`ResponseHeaderExists(name)`**:断言指定响应头存在(包括内容头) - 失败时抛出:`Expected response header '{name}' to exist, but it was not found.` - **`ResponseHeaderEquals(name, expectedValue)`**:断言响应头的第一个值严格等于指定字符串(区分大小写) - 失败时抛出:`Expected response header '{name}' to be '{expectedValue}', but found '{actualValue}'.` - **`ResponseHeaderContains(name, expectedValue)`**:断言响应头任意值包含指定子字符串(不区分大小写) - 失败时抛出:`Expected response header '{name}' to contain '{expectedValue}', but the header was not found.` 或 `Expected response header '{name}' to contain '{expectedValue}', but actual values were: [{string.Join(", ", values)}].` - **`ResponseHeaderNotExists(name)`**:断言指定的响应标头不存在(包括内容头) - 失败时抛出:`Expected response header '{name}' not to exist, but it was found.` - **`ResponseDurationUnder(maxMilliseconds)`**:断言请求耗时低于指定毫秒数 - 失败时抛出:`Expected response duration to be under {maxDuration.TotalMilliseconds:F2}ms, but it took {actualDuration.TotalMilliseconds:F2}ms.` - **`ResponseSatisfies(assertion)`**:自定义响应断言(同步或异步),直接操作 `HttpResponseMessage` - 异步重载接受 `Func`。 ### 自定义断言方法 除了内置方法,你还可以通过扩展方法为 `HttpAssertionBuilder` 添加自定义断言逻辑。例如,实现一个 `ResponseIsJson` 方法,用于验证响应内容是否为 `application/json` 类型: ```cs showLineNumbers {1,3,5,11-16} public static class HttpAssertionBuilderExtensions { public static HttpAssertionBuilder ResponseIsJson(this HttpAssertionBuilder httpAssertionBuilder) { return httpAssertionBuilder.AddAssertion(async context => { var contentType = context.ResponseMessage?.Content?.Headers.ContentType?.MediaType; const string jsonMediaType = "application/json"; // 允许 "application/json" 或 "application/json; charset=utf-8" 等 if (string.IsNullOrEmpty(contentType) || !contentType.StartsWith(jsonMediaType, StringComparison.OrdinalIgnoreCase)) { await HttpAssertionException.ThrowAsync( $"Expected response Content-Type to be '{jsonMediaType}' (or a subtype with parameters), but found '{contentType}'."); } }); } } ``` 其中,`context` 参数类型为 `HttpAssertionContext`,包含以下属性和方法: - **属性**: - `RequestMessage`:发送的请求消息(`HttpRequestMessage?`),在请求断言阶段可用 - `ResponseMessage`:响应消息(`HttpResponseMessage?`),在响应断言阶段可用 - `StatusCode`:响应状态码(`HttpStatusCode` 类型) - `IsSuccessStatusCode`:是否请求成功(`bool` 类型) - `RequestDuration`:请求耗时(毫秒,`long` 类型) - `ServiceProvider`:服务提供器(`IServiceProvider` 类型) - **方法**: - `ReadResponseAsStringAsync()`:读取响应内容字符串(自动缓存,可多次读取) - `ReadRequestAsStringAsync()`:读取请求内容字符串(自动缓存,可多次读取) 使用自定义方法示例: ```cs showLineNumbers {2-3} HttpRequestBuilder.Get("https://furion.net") .UseAssertions() .Asserts(ast => ast.ResponseIsJson().ResponseStatusCode(200)); // 支持链式调用 ``` 借助 `C#` 扩展方法,你可以灵活扩展 `HttpAssertionBuilder` 的功能,提升代码的可维护性和复用性。 --- # 3.66 启用 JSON 响应反序列化包装器 > 原文链接:https://http.furion.net/docs/request-builder/enabling-the-json-response-deserialization-wrapper/ 在与第三方 `API` 进行 `HTTP` 远程通信时,通常会返回统一结构的 `JSON` 响应,例如 `ApiResult` 类型,其中实际数据存放在 `Data` 属性中: ```cs showLineNumbers {1,4} public class ApiResult { public bool Success { get; set; } public T? Data { get; set; } // 实际返回数据 } ``` 在未启用 `JSON` 响应反序列化包装器功能时,每次调用都需要显式指定 `ApiResult` 类型: ```cs showLineNumbers {1} var content = await httpRemoteService.SendAsAsync>( HttpRequestBuilder.Get("https://furion.net")); ``` ### 启用方式 #### 1. 单次启用 为简化调用流程,可配置 `JSON` 响应反序列化包装器,使其自动提取 `Data` 属性内容: ```cs showLineNumbers {2-3,5} // 配置默认 HTTP 客户端 services.AddHttpClient(string.Empty) .ConfigureOptions(options => { options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data)); }); ``` 配置完成后,通过调用 `UseJsonResponseWrapper()` 启用该功能,之后只需指定目标数据类型,无需重复声明 `ApiResult`: ```cs showLineNumbers {1-2} var content = await httpRemoteService.SendAsAsync( HttpRequestBuilder.Get("https://furion.net").UseJsonResponseWrapper()); ``` 框架将在运行时自动创建 `ApiResult` 实例,并返回其 `Data` 属性的值。 #### 2. 全局启用(默认对所有请求生效) 也可全局启用 `JSON` 响应反序列化包装器功能,只需设置 `UseJsonResponseWrapper` 为 `true`: ```cs showLineNumbers {2-3,6} // 配置默认 HTTP 客户端 services.AddHttpClient(string.Empty) .ConfigureOptions(options => { options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data)); options.UseJsonResponseWrapper = true; }); ``` 全局启用后,所有请求默认使用包装功能: ```cs showLineNumbers {2} var content = await httpRemoteService.SendAsAsync( HttpRequestBuilder.Get("https://furion.net")); // 无需显式调用 UseJsonResponseWrapper() ``` #### 3. 单次禁用(覆盖全局设置) 若需对特定请求禁用该功能,可调用以下方法: ```cs showLineNumbers {1,2} var content = await httpRemoteService.SendAsAsync>( HttpRequestBuilder.Get("https://furion.net").UseJsonResponseWrapper(false)); ``` 默认情况下,未调用 `UseJsonResponseWrapper()` 表示未启用该功能,此时需传入完整的响应类型,除非全局配置了 `UseJsonResponseWrapper = true`。 ### 自定义结果处理(`ResultHandler`) 有时除了提取 `Data`,还需对响应做额外校验或转换。可通过 `ResultHandler` 回调实现: ```cs showLineNumbers {7,12,16,19} // 配置默认 HTTP 客户端 services.AddHttpClient(string.Empty) .ConfigureOptions(options => { options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data)) { ResultHandler = context => { if (context.Instance is { } instance) { // 可访问包装类型实例,获取其任意属性 var success = context.GetPropertyValue(nameof(ApiResult<>.Success)); } // 例如确保请求成功 context.ResponseMessage.EnsureSuccessStatusCode(); // 返回最终的目标结果(即 Data 的值) return context.Result; } }; }); ``` 通过 `ResultHandler`,您可以在返回最终数据前执行任何自定义逻辑(如校验、转换或异常处理),使请求处理更加灵活。 `context` 参数的类型为 `JsonResponseWrapperContext`,包含以下属性和方法: - **属性**: - `Instance`:包装类型的具体实例(如 `ApiResult`,`object?` 类型)。 - `Result`:目标结果(即 `Data` 的值,`object?` 类型)。 - `ResponseMessage`:响应消息(`HttpResponseMessage` 类型)。 - **方法**: - `GetPropertyValue(propertyName)`:获取包装类型的具体类型(即 `Instance`) 指定属性值。 --- # 3.67 响应 JSON 双重序列化处理 > 原文链接:https://http.furion.net/docs/request-builder/handling-double-serialization-of-response-json/ 在与第三方 `API` 进行 `HTTP` 远程通信时,极少数情况下可能会遇到服务器返回的 `JSON` 数据被意外进行了双重序列化(有时也可能是刻意为之)。例如,本应返回 `"{\"id\":1,\"name\":\"furion\"}"`,却因双重序列化变成了 `"\"{\\\"id\\\":10, \\\"name\\\":\\\"furion\\\"}\""`。针对这类情况,框架提供了解包支持: ```cs showLineNumbers {2} var content = await httpRemoteService.SendAsAsync( HttpRequestBuilder.Get("https://furion.net").UseJsonResponseStringUnwrap()); ``` 通过调用 `UseJsonResponseStringUnwrap()` 方法启用对 `JSON` 响应内容的解包处理,这样便能正确地将双重序列化后的 `JSON` 字符串转换为目标类型(`YourModel`)。 --- # 3.68 设置在构建最终请求 URL 的操作 > 原文链接:https://http.furion.net/docs/request-builder/setting-an-operation-for-building-the-final-request-url/ 在某些特殊场景下(例如需要动态拼接服务路径或查询参数),可以通过自定义逻辑来修改请求的 `URL`。使用 `SetOnUriBuilding` 方法即可调整 `UriBuilder` 对象的各个组成部分,从而构建出最终的请求地址。 ```cs showLineNumbers {2,4} HttpRequestBuilder.Post("https://furion.net/") .SetOnUriBuilding(uriBuilder => { uriBuilder.Query = "?id=10"; }); ``` **注意**:`SetOnUriBuilding` 方法支持多次调用,每次调用的结果会累积叠加。 --- # 3.69 设置重定向时进行额外配置 > 原文链接:https://http.furion.net/docs/request-builder/setting-additional-configuration-on-redirect/ 当请求发生自动重定向(例如 `301`、`302`、`307`、`308` 等状态码)时,框架会基于当前构建器克隆一个新的构建器用于重定向请求。如果你需要在重定向时对新的构建器进行额外处理(例如移除跨域的 `Authorization` 头、调整请求头、修改超时时间等),可以使用 `SetOnRedirect` 方法注册自定义回调。 ```cs showLineNumbers {2,5} HttpRequestBuilder.Post("https://furion.net/") .SetOnRedirect((originalBuilder, redirectBuilder) => { // 例如重定向时移除 Authorization 标头 redirectBuilder.Headers?.Remove("Authorization"); }); ``` **参数说明**: - `originalBuilder`:重定向之前的原始构建器(或上一次重定向的构建器),可从中读取原有的配置。 - `redirectBuilder`:即将用于重定向的克隆构建器,你可以修改它的任何属性。 **注意**:`SetOnRedirect` 方法支持多次调用,每次注册的回调都会在重定向时依次执行。 --- # 3.70 清除请求内容 > 原文链接:https://http.furion.net/docs/request-builder/clearing-request-content/ 在编写 `HTTP` 代理或单元测试时,有时需要一次性清空已设置的请求内容及其相关配置。框架提供了 `RemoveContent()` 方法来实现这一点。 ```cs showLineNumbers {3,6-7} HttpRequestBuilder.Post("https://furion.net/") .SetContent(new {}, "application/json", Encoding.UTF8) .RemoveContent(); HttpRequestBuilder.Post("https://furion.net/") .SetContent(new MemoryStream(), "application/json", Encoding.UTF8) .RemoveContent(disposeRawContent: true); // 配置移除时释放原始内容 ``` 调用 `RemoveContent()` 后,`ContentType`、`ContentEncoding`、`RawContent`、`MultipartFormDataBuilder` 和 `OmitContentType` 都会重置为初始状态;如果 `RawContent` 实现了 `IDisposable` 且之前已通过 `AddDisposable(rawContent)` 加入可释放列表,该方法也会自动将其移除。 根据新增的合并逻辑,文档做如下补充调整: --- --- # 3.71 追加请求内容 > 原文链接:https://http.furion.net/docs/request-builder/appending-request-content/ 当需要向已存在的请求内容中动态添加更多数据,而不影响已设置的 `ContentType` 和 `ContentEncoding` 时,可以使用 `AppendContent()` 方法。该方法会依据现有内容与传入内容的类型进行智能合并: | 现有内容类型 | 传入内容类型 | 合并行为 | | :----------------------------- | :------------------------------------------------------ | :-------------------------------------------------------------- | | `string` | `string` | 使用 `&` 符号拼接(常用于 `application/x-www-form-urlencoded`) | | `StringBuilder` | `string` | 将传入字符串追加到 `StringBuilder` 末尾 | | `IDictionary` | `IDictionary` | 合并两个字典,对已存在的键更新值 | | `NameValueCollection` | `NameValueCollection` 或 `IDictionary` | 合并键值对,同名键可保留多个值;字典值将转换为字符串后添加 | | `IList` | `IEnumerable` (非字符串) | 将传入集合的元素依次追加到列表中 | | 其他任意类型 | 任意类型 | 直接覆盖原有内容 | ```cs showLineNumbers {3-4,9-10,15-16,21-22,27-28,32-33} // 字符串拼接:一次调用追加,多次调用可连续追加 HttpRequestBuilder.Post("https://furion.net/") .SetContent("a=1", "application/x-www-form-urlencoded") .AppendContent("b=2"); // 结果:a=1&b=2 // StringBuilder 追加字符串 var sb = new StringBuilder("a=1"); HttpRequestBuilder.Post("https://furion.net/") .SetContent(sb, "application/x-www-form-urlencoded") .AppendContent("&b=2"); // sb 内容变为 "a=1&b=2" // 字典合并:已存在的键值会被更新 var dict = new Dictionary { ["key1"] = "val1" }; HttpRequestBuilder.Post("https://furion.net/") .SetContent(dict, "application/x-www-form-urlencoded") .AppendContent(new Dictionary { ["key2"] = "val2" }); // 合并为 { key1: val1, key2: val2 } // NameValueCollection 合并(同名键保留多个值) var nvc = new NameValueCollection { ["id"] = "1" }; HttpRequestBuilder.Post("https://furion.net/") .SetContent(nvc, "application/x-www-form-urlencoded") .AppendContent(new NameValueCollection { ["name"] = "furion", ["name"] = "dotnet" }); // "name" 拥有两个值 // 列表追加:将新元素添加到已有列表中 var list = new List { "item1" }; HttpRequestBuilder.Post("https://furion.net/") .SetContent(list, "application/json") .AppendContent(new[] { "item2" }); // 结果:["item1", "item2"] // 其他类型覆盖:使用新内容替换旧内容 HttpRequestBuilder.Post("https://furion.net/") .SetContent("original") .AppendContent(new { id = 1 }); // RawContent 变为 { id = 1 } ``` > **注意事项** - 使用 `AppendContent` 方法前,要求已通过 `SetContent` 等方法设置了请求内容,否则追加操作不生效。 - 该方法支持多次调用,可连续追加不同内容,每次追加均遵循相同的合并规则。 - 若传入 `null` 值,同样会被忽略,原有请求内容保持不变。 - 该方法**不会**修改已设置的 `ContentType` 和 `ContentEncoding`。 --- # 3.72 禁用 Access Token 自动管理 > 原文链接:https://http.furion.net/docs/request-builder/disabling-automatic-access-token-management/ 框架内置了 `Access Token` 自动管理功能,只需实现 `IHttpAccessTokenProvider` 接口,并在 `GetAsync` 方法中编写获取 `Access Token` 的逻辑即可。通常情况下,获取 `Access Token` 需要借助独立的 `HTTP` 请求,而若在 `GetAsync` 内部直接使用 `IHttpRemoteService` 发送请求,则会因触发自动管理机制而陷入递归死循环。此时,需通过 `.WithoutTokenManagement()` 显式禁用当前请求的 `Access Token` 自动管理,避免循环调用。 ```cs showLineNumbers {1,6,8} public class CustomHttpAccessTokenProvider(IHttpRemoteService httpRemoteService): IHttpAccessTokenProvider { /// public async Task GetAsync(CancellationToken cancellationToken) { var serverToken = await httpRemoteService.SendAsAsync(HttpRequestBuilder.Post("https://furion.net") .SetJsonContent(new { username = "furion", password = "your-password"}) .WithoutTokenManagement()); // 跳过 Token 管理,避免递归调用(声明式请求使用 [SuppressTokenManagement]) return new HttpAccessToken(serverToken.Token, serverToken.ExpiresAt) }; } ``` --- # 3.73 设置 Access Token 请求的自定义数据 > 原文链接:https://http.furion.net/docs/request-builder/setting-custom-data-for-the-access-token-request/ 当使用 `IHttpAccessTokenProvider` 自动获取 `Access Token` 时,有时需要向 `GetAsync` 方法传递额外的动态参数(如用户名、密码、客户端密钥等)。通过 `WithAccessTokenData` 方法,可以在构建请求时预设这些参数,框架会自动将它们复制到 `HttpAccessTokenContext.Items` 中,供 `GetAsync` 使用。 ```cs showLineNumbers {3-4} HttpRequestBuilder.Post("https://furion.net/") .SetHttpClientName("myapi") // 可选 .WithAccessTokenData("username", "admin") .WithAccessTokenData("password", "123456") ``` 然后,在 `IHttpAccessTokenProvider.GetAsync` 方法中,可以从 `context.Items` 中取出这些值: ```cs showLineNumbers {3-4} public async Task GetAsync(HttpAccessTokenContext context, CancellationToken cancellationToken) { context.Items.TryGetValue("username", out var username); context.Items.TryGetValue("password", out var password); // 使用 username 和 password 获取 Token... } ``` **注意**:该方法支持多次调用,重复的键会用最新的值覆盖。 --- # 3.74 移除 URL 地址末尾的 / > 原文链接:https://http.furion.net/docs/request-builder/removing-the-trailing--from-the-url/ 部分服务端对路径末尾的 `/` 敏感(如 `/api/` 与 `/api`),可能造成 `301` 重定向或路由匹配失败。开启该功能后,框架会在构造最终请求地址时自动去除路径末尾的 `/`。 ```cs showLineNumbers {2,5} HttpRequestBuilder.Post("https://furion.net/") .RemoveTrailingSlash(); HttpRequestBuilder.Post("https://furion.net/") .RemoveTrailingSlash(false); // 禁用该功能(默认值) ``` 请求地址将变为 `https://furion.net`。 --- # 3.75 设置请求接口配额键 > 原文链接:https://http.furion.net/docs/request-builder/setting-the-request-interface-quota-key/ 为当前请求指定配额键,用于关联在 `HttpClientOptions` 中配置的配额限制规则。 ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net/") .SetQuotaKey("wechat/accesstoken"); ``` > **配置键 `QuotaKey` 说明** - 如果未指定配额键,或指定的键在 `QuotaLimits` 中不存在,则不会进行配额检查,请求正常发送。 - 配额键可以是任意自定义字符串,推荐使用与接口路径相关的名称,便于识别和管理。 --- # 3.76 启用 ETag 响应缓存 > 原文链接:https://http.furion.net/docs/request-builder/enabling-etag-response-caching/ `ETag`(实体标签)是 `HTTP` 协议中用于标识资源版本的一种机制。服务器在响应头中返回资源的 `ETag` 值(例如 `"abc123"`),客户端在后续请求中可通过 `If-None-Match` 头携带该值。若资源未发生变化,服务器返回 `304 Not Modified`,无需重新传输内容;否则返回新内容及新的 `ETag`。 启用 `ETag` 缓存后,框架会自动处理这一过程:首次请求缓存响应及 `ETag`,后续请求自动附加 `If-None-Match`;收到 `304` 状态码时直接复用缓存内容,减少数据传输并提升请求效率。在弱网环境或移动端流量计费场景下,这一机制能显著降低带宽消耗,同时加快响应速度。 ```cs showLineNumbers {2,5} HttpRequestBuilder.Get("https://furion.net/") .UseETag(); HttpRequestBuilder.Get("https://furion.net/") .UseETag(false); // 禁用该功能(默认值) ``` > **`ETag` 响应缓存说明** - 仅对 `GET` 和 `HEAD` 请求生效。 - 若请求显式调用了 `DisableCache()`,`ETag` 功能将自动跳过。 - 缓存默认存储在内存中,支持通过实现 `IHttpETagCache` 接口替换为分布式缓存(如 `Redis`)。 - 默认内存缓存未限制缓存项数量和单条响应内容大小,大量唯一 `URL` 或大响应可能导致内存持续增长。为避免此问题,建议实现 `IHttpETagCache` 接口并替换默认实现,例如: ```cs showLineNumbers services.Replace(ServiceDescriptor.Singleton()); ``` - 若同时启用了全局请求分析工具(`AddProfilerDelegatingHandler`)且发现响应内容未打印,请在请求上显式调用 `Profiler()` 方法即可解决。 --- # 3.77 设置 SOAP 请求头(WebService) > 原文链接:https://http.furion.net/docs/request-builder/setting-the-soap-request-header-webservice/ 当调用基于 `SOAP` 协议的 `Web` 服务时,通常需要在请求头中指定 `SOAPAction`,以便服务端识别要执行的操作。使用 `SetSOAPAction` 方法可快速设置该头。 ```cs showLineNumbers {2,6} HttpRequestBuilder.Get("http://您的主机地址/Share/DatabaseManager.asmx") .SetSOAPAction("http://tempuri.org/GetDatabaseList"); // 自动为 SOAPAction 值添加双引号包裹(符合 SOAP 1.1 规范建议) HttpRequestBuilder.Get("http://您的主机地址/Share/DatabaseManager.asmx") .SetSOAPAction("http://tempuri.org/GetDatabaseList", addQuotes: true); ``` --- # 3.78 模拟请求响应与异常(Mock) > 原文链接:https://http.furion.net/docs/request-builder/simulating-request-responses-and-exceptions-mock/ `MockResponse` 和 `MockException` 专为单元测试设计,可在不实际发送 `HTTP` 请求的情况下,直接返回预设响应或抛出预设异常,对业务代码零侵入。 ```cs showLineNumbers {3,7,10-15,19,23-24} // 模拟 JSON 响应(自动序列化) HttpRequestBuilder.Get("https://api.furion.net/weather") .MockResponse(new { Temperature = 25, Condition = "Sunny" }); // 模拟自定义状态码和内容类型 HttpRequestBuilder.Post("https://api.furion.net/upload") .MockResponse(new { Id = 123 }, HttpStatusCode.Created, "application/json"); // 模拟完整的 HttpResponseMessage(适用于文件流等复杂场景) var content = new StreamContent(File.OpenRead("test.pdf")); content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf"); var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = content }; HttpRequestBuilder.Get("https://example.com/download") .MockResponse(response); // 模拟异常(超时、网络中断等) HttpRequestBuilder.Get("https://api.furion.net/data") .MockException(new HttpRequestException("Connection timed out")); // 清除所有模拟设置 HttpRequestBuilder.Get("https://api.furion.net/data") .MockResponse(new { }) .ClearMock(); // 清除后请求将正常发送 ``` > **模拟功能说明** - `MockResponse(T content, ...)`:将 `content` 序列化为 `JSON` 并构造 `HttpResponseMessage`,适用于绝大多数 `REST API` 场景。 - `MockResponse(HttpResponseMessage)`:允许完全自定义 `HttpResponseMessage`,支持文件流、二进制数据等任意响应内容。 - `MockException(Exception)`:设置模拟异常,优先级高于 `MockResponse`。若同时设置两者,异常优先抛出。 - `ClearMock()`:清除所有模拟设置,释放已占用的 `HttpResponseMessage` 资源。 - `IsMocked()`:检查当前构建器是否处于模拟状态。 > **注意事项** - **仅用于测试环境**:模拟功能应仅限单元测试中使用,避免在生产代码中误用。 - **资源释放**:设置的 `HttpResponseMessage` 会在 `ClearMock()` 或再次调用 `MockResponse` 时自动释放,无需手动 `Dispose`。 - **互斥性**:`MockResponse` 与 `MockException` 互斥,设置一方会自动清除另一方。 该功能与 `HttpRemoteService` 结合使用时,可完全替代真实网络请求,极大提升单元测试的隔离性和执行速度。 --- # 3.79 克隆和复制 > 原文链接:https://http.furion.net/docs/request-builder/cloning-and-copying/ 框架提供了 `Clone()` 和 `CopyTo()` 方法,用于复制或迁移 `HttpRequestBuilder` 的配置。这在需要复用一组公共配置(如认证头、超时时间等)并应用到多个不同请求时尤为有用。 ```cs showLineNumbers {3,7} var httpRequestBuilder = HttpRequestBuilder.Get("https://furion.net/").Profiler(); // 克隆操作:返回一个全新的构建器 var newBuilder = httpRequestBuilder.Clone(); // 完整克隆 var newBuilder = httpRequestBuilder.Clone("RequestUri", "HttpMethod"); // 排除特定属性 // 复制操作:将源构建器的配置复制到目标构建器 var sourceBuilder = HttpRequestBuilder.Get("https://furion.net/api/user").UseETag().SetQuotaKey("api/user"); sourceBuilder.CopyTo(httpRequestBuilder); // 完整复制 sourceBuilder.CopyTo(httpRequestBuilder, "RequestUri", "HttpMethod"); // 排除特定属性 ``` --- # 3.80 设置自定义数据 > 原文链接:https://http.furion.net/docs/request-builder/setting-custom-data/ 在某些特殊场景下,你可能需要向 `HttpRequestBuilder` 对象中注入自定义数据,以便后续的内部组件或拦截器能够访问这些信息。此时可以使用 `WithData` 方法来实现: ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net/") .WithData("somekey", "somedata"); ``` 存入的数据会保存在 `httpRequestBuilder.Items` 字典中,后续可通过键名取出: ```cs showLineNumbers {3-4} httpRequestBuilder.Items.TryGetValue("somekey", out var somedata); ``` **注意**:该方法支持多次调用,重复的键会用最新的值覆盖。 --- # 3.81 获取请求构建器或请求消息(预检请求) > 原文链接:https://http.furion.net/docs/request-builder/getting-the-request-builder-or-request-message-pre-flight-request/ 在某些情况下,您可能希望**只获取 `HTTP` 请求对象本身,而不实际发送请求**。例如:在单元测试中验证生成的请求是否符合预期,或拿到请求构建器后进一步手动修改,亦或是将请求消息传递给另一个系统执行。 为此,当发送 `HTTP` 远程请求的方法的返回类型为 `HttpRequestBuilder` 或 `HttpRequestMessage` 时,框架将直接构建并返回该对象,**跳过实际的网络传输**。示例如下: ```cs showLineNumbers {2,4,7-8} // 获取构建器,可继续链式配置后手动发送 var builder = await httpRemoteService.GetAsAsync("https://furion.net"); // 不发送请求 builder.WithHeader("X-Custom", "value"); var httpResponseMessage = await httpRemoteService.SendAsync(builder); // 发起网络请求 // 获取 HttpRequestMessage,用于断言或外部传递 var httpRequestMessage = await httpRemoteService.GetAsAsync("https://furion.net"); // 不发送请求 Assert.Equal("https://furion.net/", httpRequestMessage.RequestUri?.ToString()); ``` ### 应用场景 - **预检请求(Pre-flight Check)**:在正式发送前,检查生成的请求对象是否符合预期。确认 `URL`、请求头、`Token` 注入等都正确无误后,再手动发送或继续处理。 - **单元测试**:无需模拟网络环境,直接验证生成的 `HttpRequestMessage` 是否包含了正确的参数、头和认证信息。 - **请求对象传递**:将构造好的 `HttpRequestMessage` 传递给其他服务、库或进程执行,实现请求构造与请求执行的分离。 - **混合编程**:先通过构建器或声明式完成大部分配置(参数映射、`Token` 注入等),再拿到构建器进行少量动态修改后手动发送,兼顾声明式的简洁与命令式的灵活。 > **注意** - 返回 `HttpRequestBuilder` 或 `HttpRequestMessage` 的方法**不会发起网络请求**,框架仅在内存中完成构建。 - 如果方法返回其他类型(如 `string`、`HttpResponseMessage` 等),框架会正常发送请求并返回相应结果。 - 此特性与[冻结参数类型](/docs/declarative/frozen-parameter-types/)(如 `Action`)互补,后者在发送前注入配置,但无法阻止发送。 --- # 3.82 HttpRequestBuilder 统一配置器 > 原文链接:https://http.furion.net/docs/request-builder/httprequestbuilder-unified-configurator/ 在通过 `HttpRequestBuilder` 类构建 `HttpRequestMessage` 对象时,若需对所有请求进行全局配置,框架提供了统一的配置机制。 开发者可通过实现 `IHttpRequestBuilderConfigurator` 接口,对 `HttpRequestBuilder` 实例进行统一设置。例如,以下 `RequestBuilderConfigurator` 类在 `Configure` 方法中为所有请求添加了一个公共请求头: ```cs showLineNumbers {1,4} public class HttpRequestBuilderConfigurator : IHttpRequestBuilderConfigurator { /// public void Configure(HttpRequestBuilder httpRequestBuilder) { httpRequestBuilder.WithHeader("global", "form_furion"); } } ``` 实现自定义配置器后,需在配置 `HttpRemoteOptions` 时将其赋值给 `RequestBuilderConfigurator` 属性: ```cs showLineNumbers {2,4} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { options.RequestBuilderConfigurator = new HttpRequestBuilderConfigurator(); }); ``` 配置生效后,所有 `HttpRequestBuilder` 实例在调用 `Build()` 方法构建 `HttpRequestMessage` 之前,均会执行此统一配置逻辑。 --- # 3.83 自定义 HttpRequestBuilder 扩展方法 > 原文链接:https://http.furion.net/docs/request-builder/custom-httprequestbuilder-extension-methods/ 除了框架自带的 `HttpRequestBuilder` 方法外,您还可以通过自定义扩展方法来简化代码、减少重复逻辑。例如,添加一个 `SetAccept` 方法,用于快速设置 HTTP 请求头中的 `Accept` 字段: ```cs showLineNumbers {1,3,8} public static class HttpRequestBuilderExtensions { public static HttpRequestBuilder SetAccept(this HttpRequestBuilder httpRequestBuilder, string accept) { // 参数校验:确保 accept 不为空 ArgumentException.ThrowIfNullOrWhiteSpace(accept); return httpRequestBuilder.WithHeader("Accept", accept, replace: true); } } ``` 定义完成后,即可在 `HttpRequestBuilder` 实例中链式调用该方法: ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net") .SetAccept("text/html"); ``` 借助 `C#` 扩展方法的特性,您可以轻松扩展 `HttpRequestBuilder` 的功能,从而提升代码的**可读性**、**可维护性**,并有效减少重复代码。 --- # 3.84 自定义扩展其他构建器 > 原文链接:https://http.furion.net/docs/request-builder/custom-extensions-for-other-builders/ 以下构建器均派生自 `HttpRequestBuilderConfigurator` 抽象类型: - `HttpFileDownloadBuilder` - `HttpFileUploadBuilder` - `HttpLongPollingBuilder` - `HttpServerSentEventsBuilder` - `HttpStressTestHarnessBuilder` 这些构建器共享统一的扩展机制。例如,它们都支持通过 `With(builder => { ... })` 方法为底层 `HttpRequestBuilder` 配置额外的请求参数。 `HttpRequestBuilderConfigurator` 已内置了一些常用方法(如 `Profiler()`)。如果需要进一步扩展,可以按照如下方式编写扩展方法: ```cs showLineNumbers {1,11-12,14} public static class HttpRequestBuilderConfiguratorExtensions { /// /// 当 HTTP 响应的 IsSuccessStatusCode 属性为 false 时,抛出异常。 /// /// /// /// /// 派生构建器的具体类型 /// 返回构建器自身实例,支持链式调用。 public static THttpBuilder EnsureSuccessStatusCode(this HttpRequestBuilderConfigurator configurator) where THttpBuilder : HttpRequestBuilderConfigurator { return configurator.With(builder => builder.EnsureSuccessStatusCode()); } } ``` 这样,上述所有构建器都可以直接调用 `EnsureSuccessStatusCode()` 方法,并返回自身构建器实例,保持流畅的链式调用体验。 --- # 4.1 HttpMultipartFormDataBuilder 表单构建器 > 原文链接:https://http.furion.net/docs/multipart-builder/httpmultipartformdatabuilder-form-builder/ 在互联网应用中,保存用户自定义数据最常用的方法是使用 `Form` 表单提交。`Form` 表单不仅能传输文本数据,还能传输二进制数据(例如文件)。 为了构建包含这些多部分表单内容,我们使用 `HttpMultipartFormDataBuilder` 表单构建器。该构建器最终会生成一个 `MultipartFormDataContent` 对象,并将其设置为 `HttpRequestMessage` 的 `Content` 属性,同时指定请求的内容类型为 `multipart/form-data`。 --- # 4.2 创建构建器实例 > 原文链接:https://http.furion.net/docs/multipart-builder/creating-a-builder-instance/ 由于 `HttpMultipartFormDataBuilder` 的构造函数是私有的,因此无法直接使用 `new` 关键字进行实例化。若想在 `HTTP` 远程请求中设置多部分表单内容,必须通过 `HttpRequestBuilder` 对象提供的 `SetMultipartContent(Action)` 方法进行设置。 ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { // multipart 的类型是 HttpMultipartFormDataBuilder }); ``` > **重要说明** 使用 `SetMultipartContent` 方法会覆盖其他内容设置方法(`SetJsonContent`、`SetHtmlContent`、`SetXmlContent`、`SetTextContent`、`SetRawStringContent`、`SetFormUrlEncodedContent` 和 `SetContent`)。 ```cs showLineNumbers {2-5} HttpRequestBuilder.Post("https://furion.net/") .SetMultipartContent(multipart => { multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"); }) .SetContent(new { id = 1, name = "Furion" }, "application/json"); // 将被覆盖 ``` --- # 4.3 设置内容边界 > 原文链接:https://http.furion.net/docs/multipart-builder/setting-the-content-boundary/ 在构建多部分表单内容时,可以通过以下链式调用方法为多部分表单内容设置边界(`Boundary`): ```cs showLineNumbers {5,8} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { // 属性方式设置边界(非推荐) multipart.Boundary = "--------------------"; // 方法方式设置边界(推荐),支持链式调用 multipart.SetBoundary("--------------------"); }); ``` > **小提示** 框架默认提供 `Boundary`,其默认值为:`$"----{DateTime.Now.Ticks:x}"`。 此外,虽然两种方式都可以设置边界,但推荐使用 `SetBoundary` 方法,因为它支持链式调用,使代码更加简洁和易读。 --- # 4.4 保留内容的默认 Content-Type > 原文链接:https://http.furion.net/docs/multipart-builder/keeping-the-default-content-type-of-the-content/ **在与一些较老的 `HTTP` 服务对接时,提交表单数据时不应设置多部分表单内容的 `Content-Type`,否则可能引发异常。** 而现代 `HTTP` 接口则无此限制。因此,框架默认在提交表单数据时会自动移除多部分表单内容的 `Content-Type`。 若需取消此操作,可通过以下方式设置: ```cs showLineNumbers {4,6,14} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.OmitContentType = false; // 保留多部分内容默认的 Content-Type // 或使用 multipart.SetOmitContentType(false); }); // 【推荐】使用 SetMultipartContent(Action configure, bool omitContentType) 重载方法 HttpRequestBuilder.Post("https://furion.net/") .SetMultipartContent(multipart => { // ... }, false); // 保留多部分内容默认的 Content-Type ``` --- # 4.5 添加单个表单项内容 > 原文链接:https://http.furion.net/docs/multipart-builder/adding-a-single-form-item/ 向多部分表单内容添加独立的项,即添加单个表单属性。 ```cs showLineNumbers {4-5} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFormItem(1, "id"); // 将被赋值给 FormClass 的 Id 属性 multipart.AddFormItem("Furion", "name"); // 将被赋值给 FormClass 的 Name 属性 }); ``` 上述代码对应于服务端接收的类定义,如: ```cs showLineNumbers {3-4} public class FormClass { public int Id { get; set; } public string Name { get; set; } // 其他属性 } ``` --- # 4.6 添加 JSON 内容 > 原文链接:https://http.furion.net/docs/multipart-builder/adding-json-content/ 当需要将 `JSON` 数据添加到多部分表单内容中时,`HttpMultipartFormDataBuilder` 提供了灵活的处理方式,具体取决于是否指定了表单名。 **1. 未指定表单名**:在这种情况下,`JSON` 数据会被解析并遍历,其属性将作为独立的表单项进行设置。无论是传入匿名类型还是 `JSON` 字符串,结果都是相同的。 ```cs showLineNumbers {4-5} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddJson(new { id = 1, name = "furion" }); // 未指定表单名(将被赋值给 FormClass 的 Id 和 Name 属性) // multipart.AddJson("{\"id\":1,\"name\":\"furion\"}"); // 支持 JSON 字符串。同上。 }); ``` 上述代码将生成两个表单项:`Id` 和 `Name`,它们对应于服务端接收的类定义,如: ```cs showLineNumbers {3-4} public class FormClass { public int Id { get; set; } public string Name { get; set; } // 其他属性 } ``` **2. 指定表单名**:如果为 `JSON` 数据指定了表单名,则整个 `JSON` 对象将作为表单的一个嵌套项进行设置。这通常用于服务端期望接收具有嵌套结构的对象。 ```cs showLineNumbers {5-6} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddJson(new { id = 1, name = "furion" }); // 未指定表单名(将被赋值给 FormClass 的 Id 和 Name 属性) multipart.AddJson(new { id = 1, name = "furion" }, "child"); // 指定表单名,将赋值给 FormClass 的 Child 属性) // multipart.AddJson("{\"id\":1,\"name\":\"furion\"}", "child"); // 支持 JSON 字符串。同上。 }); ``` 在这种情况下,服务端接收的类定义应包含一个嵌套类,如: ```cs showLineNumbers {3-5} public class FormClass { public int Id { get; set; } public string Name { get; set; } public ChildClass Child { get; set; } // 嵌套类 // 其他属性... } public class ChildClass { public int Id { get; set; } public string Name { get; set; } // 其他属性... } ``` > **推荐使用【[原始字符串字面量](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string)】设置 `JSON`** 推荐使用[原始字符串字面量](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string)来设置 `JSON` 数据。在 `C# 11` 中,新增了这一特性,允许使用三个双引号(`"""`)包裹的字符串来包含多行文本,同时字符串内的转义字符(如 `\n`,`\t` 等)将作为普通字符处理,无需转义。例如: ```cs showLineNumbers {4-9} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddJson(""" { "id": 1, "name": "Furion" } """); }); ``` 若需在原始字符串中插入变量,只需在首个 `"""` 前添加 `$$`,并使用 `{{变量名}}` 模板来占位。例如: ```cs showLineNumbers {1,6,9} var val = "Furion"; HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddJson($$""" { "id": 1, "name": "{{val}}" } """); }); ``` 使用原始字符串字面量设置 `JSON` 数据,可以简化代码,避免处理转义符的繁琐操作。 > **`JSON` 字符串说明** 若传入的 `JSON` 字符串格式无效,将抛出 `JsonException` 异常。 --- # 4.7 添加 HTML 内容 > 原文链接:https://http.furion.net/docs/multipart-builder/adding-html-content/ 向多部分表单内容中添加 `HTML` 内容。 ```cs showLineNumbers {4} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddHtml("", "data"); }); ``` > **推荐使用【[原始字符串字面量](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string)】设置 `HTML`** 参考【4.6 设置 `JSON` 内容】。 --- # 4.8 添加 XML 内容 > 原文链接:https://http.furion.net/docs/multipart-builder/adding-xml-content/ 向多部分表单内容中添加 `XML` 内容。 ```cs showLineNumbers {4} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddXml("", "data"); }); ``` > **推荐使用【[原始字符串字面量](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string)】设置 `XML`** 参考【4.6 设置 `JSON` 内容】。 --- # 4.9 添加文本内容 > 原文链接:https://http.furion.net/docs/multipart-builder/adding-text-content/ 向多部分表单内容中添加文本内容。 ```cs showLineNumbers {4} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddText("Furion", "data"); }); ``` > **推荐使用【[原始字符串字面量](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/raw-string)】设置文本** 参考【4.6 设置 `JSON` 内容】。 --- # 4.10 添加对象内容(复杂表单/上传文件) > 原文链接:https://http.furion.net/docs/multipart-builder/adding-object-content-complex-forms--file-uploads/ 当需要将对象添加到多部分表单内容中时,`HttpMultipartFormDataBuilder` 提供了灵活的处理方式,具体取决于是否指定了表单名。 **1. 未指定表单名**:在这种情况下,对象会被解析并遍历,其属性将作为独立的表单项进行设置。 ```cs showLineNumbers {4} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddObject(new { id = 1, name = "furion" }); // 未指定表单名(将被赋值给 FormClass 的 Id 和 Name 属性) }); ``` 上述代码将生成两个表单项:`Id` 和 `Name`,它们对应于服务端接收的类定义,如: ```cs showLineNumbers {3-4} public class FormClass { public int Id { get; set; } public string Name { get; set; } // 其他属性 } ``` **2. 指定表单名**:如果为对象指定了表单名,则整个对象将作为表单的一个嵌套项进行设置。这通常用于服务端期望接收具有嵌套结构的对象。 ```cs showLineNumbers {5} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddObject(new { id = 1, name = "furion" }); // 未指定表单名(将被赋值给 FormClass 的 Id 和 Name 属性) multipart.AddObject(new { id = 1, name = "furion" }, "child"); // 指定表单名,将赋值给 FormClass 的 Child 属性) }); ``` 在这种情况下,服务端接收的类定义应包含一个嵌套类,如: ```cs showLineNumbers {3-5} public class FormClass { public int Id { get; set; } public string Name { get; set; } public ChildClass Child { get; set; } // 嵌套类 // 其他属性... } public class ChildClass { public int Id { get; set; } public string Name { get; set; } // 其他属性... } ``` ### 包含文件(或二进制数据)的复杂表单 对象字段除基础数据类型外,还可包含文件或二进制数据(如 `Stream`、`IFormFile`、`FileInfo` 或 `MultipartFile`)。推荐使用 `MultipartFile` 类型声明文件字段。 示例代码如下: ```cs showLineNumbers {4} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddObject(new FormClass { Id = 1, Name = "furion", File = MultipartFile.CreateFromPath("文件路径") }); }); ``` 对应的模型类定义如下: ```cs showLineNumbers {5,7} public class FormClass // 支持属性 [AliasAs] 定义别名 { public int Id { get; set; } public string Name { get; set; } public MultipartFile File { get; set; } // public IFormFile File { get; set; } // 注意:需要按照以下步骤配置 } ``` **注意**:若使用 `IFormFile` 替代 `MultipartFile`,需确保已注册 `FormFileContentProcessor`。可通过全局或局部调用 `.AddHttpContentProcessors(() => [new FormFileContentProcessor()])` 来完成注册: - 单次请求设置: ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net/") .AddHttpContentProcessors(() => [ new FormFileContentProcessor() ]) ``` - 全局配置: 在 `Startup.cs` 或 `Program.cs` 文件中,配置并注册 `HttpRemote` 服务,以启用 `IFormFile` 内容处理器功能: ```cs showLineNumbers {3} services.AddHttpRemote(builder => { builder.AddHttpContentProcessors(() => [ new FormFileContentProcessor() ]); }); ``` > **`JSON` 序列化配置说明** **注意**:当传入类型对象时,框架会先将对象转换为 `IDictionary` 类型,再逐条添加为表单项。因此,该过程不会直接使用 `JSON` 序列化的配置。如需为属性指定别名,请通过 `[AliasAs]` 特性或 `multipart.SetFormNameTransformer(namingPolicy)` 进行定义。 > **小提示** `AddJson`、`AddFormItem`、`AddHtml`、`AddXml` 和 `AddText` 方法均在内部调用了 `AddObject` 方法。 --- # 4.11 添加互联网文件内容 > 原文链接:https://http.furion.net/docs/multipart-builder/adding-internet-file-content/ 在多部分表单内容中添加来自互联网地址的文件内容。 ```cs showLineNumbers {4-7} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFileFromRemote("https://furion.net/img/furionlogo.png", "file"); multipart.AddFileFromRemote("https://furion.net/img/furionlogo.png", "file", "logo.png"); // 自定义文件名 multipart.AddFileFromRemote("https://furion.net/img/furionlogo.png", "file", "logo.png", "image/png"); // 自定义媒体类型,不传 Content-Type 将自动根据文件扩展名解析 multipart.AddFileFromRemote("https://furion.net/img/furionlogo.png", configure: request => {}); // 支持配置 HttpClient 和 HttpRequestMessage 实例 }); ``` **注意**:从互联网地址添加文件时,文件大小限制为 `100MB`。 > **`contentType` 参数说明** - 若提供 `contentType` 参数,则使用该值。 - 若未提供 `contentType` 但提供了 `fileName`,则根据文件名扩展名解析 `MIME` 类型。 - 若两者均未提供,则尝试根据 `URL` 文件名扩展名解析。 - 若解析失败,则默认使用 `application/octet-stream`。 --- # 4.12 添加 Base64 字符串文件内容 > 原文链接:https://http.furion.net/docs/multipart-builder/adding-base64-string-file-content/ 在多部分表单内容中添加来自 `Base64` 字符串的文件内容。 ```cs showLineNumbers {4-5} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFileFromBase64String("77u/5rWL6K+V5paH5Lu25YaF5a65", "file", "test.txt"); multipart.AddFileFromBase64String("77u/5rWL6K+V5paH5Lu25YaF5a65", "file", "test.txt", "text/plain"); // 自定义媒体类型,不传 Content-Type 将自动根据文件扩展名解析 }); ``` **注意**:从 `Base64` 字符串添加文件时,文件大小限制为 `100MB`。 > **`contentType` 参数说明** - 若提供 `contentType` 参数,则使用该值。 - 若未提供 `contentType` 但提供了 `fileName`,则根据文件名扩展名解析 `MIME` 类型。 - 若两者均未提供,则默认使用 `application/octet-stream`。 --- # 4.13 添加本地路径文件内容(进度) > 原文链接:https://http.furion.net/docs/multipart-builder/adding-local-path-file-content-progress/ 在多部分表单内容中添加来自本地路径的文件内容。 ```cs showLineNumbers {5-7,10-12} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { // 文件流方式 multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"); multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file", "test.jpg"); // 自定义文件名 multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file", "test.jpg", "image/jpeg"); // 自定义媒体类型,不传 Content-Type 将自动根据文件扩展名解析 // 字节数组方式 multipart.AddFileAsByteArray(@"C:\Workspaces\httptest.jpg", "file"); multipart.AddFileAsByteArray(@"C:\Workspaces\httptest.jpg", "file", "test.jpg"); // 自定义文件名 multipart.AddFileAsByteArray(@"C:\Workspaces\httptest.jpg", "file", "test.jpg", "image/jpeg"); // 自定义媒体类型,不传 Content-Type 将自动根据文件扩展名解析 }); ``` 此外,系统提供了 `AddFileWithProgressAsStream` 方法,与 `AddFileAsStream` 相比,它允许您实时获取文件传输进度。例如: ```cs showLineNumbers {2,7,11-17} // 创建文件传输进度信息的通道 var progressChannel = Channel.CreateUnbounded(); HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFileWithProgressAsStream(@"C:\Workspaces\httptest.jpg", progressChannel, "file"); }); // 订阅文件传输进度通知 await foreach (var fileTransferProgress in progressChannel.Reader.ReadAllAsync(cancellationToken)) { Console.WriteLine(fileTransferProgress.ToSummaryString()); // 每秒延迟 await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); } ``` 这样,文件传输过程中每秒都会打印传输进度信息。 react-error-boundary > **`contentType` 参数说明** - 若提供 `contentType` 参数,则使用该值。 - 若未提供 `contentType` 但提供了 `fileName`,则根据文件名扩展名解析 `MIME` 类型。 - 若两者均未提供,则尝试根据路径文件名扩展名解析。 - 若解析失败,则默认使用 `application/octet-stream`。 > **禁用请求分析工具** 在打印请求内容时,`Stream` 对象可能会被重复读取或变得不可读。这是因为流会被提前读取到内存中,其位置指针会移动到尾部。这会导致无法准确获取上传进度。 因此,在使用 `AddFileWithProgressAsStream` 进行上传资源时,建议禁用请求分析工具,以确保能够获取准确的上传进度信息。 --- # 4.14 添加 Stream 内容 > 原文链接:https://http.furion.net/docs/multipart-builder/adding-stream-content/ 在多部分表单内容中添加 `Stream` 内容。 ```cs showLineNumbers {4-7} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddStream(stream, "file"); multipart.AddStream(stream, "file", "test.txt"); // 设置文件名 multipart.AddStream(stream, "file", "test.txt", "text/plain"); // 设置媒体类型,不传 Content-Type 将自动根据文件扩展名解析 multipart.AddStream(stream, "file", "test.txt", "text/plain", disposeResourcesOnRequestCompletion: true); // 可设置请求完成后自动释放资源 }); ``` > **`contentType` 参数说明** - 若提供 `contentType` 参数,则使用该值。 - 若未提供 `contentType` 但提供了 `fileName`,则根据文件名扩展名解析 `MIME` 类型。 - 若两者均未提供,则默认使用 `application/octet-stream`。 --- # 4.15 添加字节数组内容 > 原文链接:https://http.furion.net/docs/multipart-builder/adding-byte-array-content/ 在多部分表单内容中添加字节数组内容。 ```cs showLineNumbers {4-6} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddByteArray(bytes, "file"); multipart.AddByteArray(bytes, "file", "test.txt"); // 设置文件名 multipart.AddByteArray(bytes, "file", "test.txt", "text/plain"); // 设置媒体类型,不传 Content-Type 将自动根据文件扩展名解析 }); ``` > **`contentType` 参数说明** - 若提供 `contentType` 参数,则使用该值。 - 若未提供 `contentType` 但提供了 `fileName`,则根据文件名扩展名解析 `MIME` 类型。 - 若两者均未提供,则默认使用 `application/octet-stream`。 --- # 4.16 添加 MultipartFile 内容 > 原文链接:https://http.furion.net/docs/multipart-builder/adding-multipartfile-content/ `MultipartFile` 类型专为处理多部分表单文件而设计,它的构造函数是私有的,因此无法直接使用 `new` 关键字进行实例化,不过,框架提供了 `MultipartFile.CreateFrom[Source]` 的多个静态重载方法创建 `MultipartFile` 的实例。示例如下: ```cs showLineNumbers {5,7,9,11,13} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { // 从字节数组中添加文件 multipart.AddFile(MultipartFile.CreateFromByteArray(bytes, "files")); // 从 Stream 中添加文件 multipart.AddFile(MultipartFile.CreateFromStream(stream, "files")); // 从本地路径中添加文件 multipart.AddFile(MultipartFile.CreateFromPath(@"C:\Workspaces\httptest.jpg", "files")); // 从 Base64 字符串中添加文件 multipart.AddFile(MultipartFile.CreateFromBase64String("77u/5rWL6K+V5paH5Lu25YaF5a65", "files")); //从互联网 URL 中添加文件 multipart.AddFile(MultipartFile.CreateFromRemote("https://furion.net/img/furionlogo.png", "files")); }); ``` > **`MultipartFile` 类型的 `Create` 静态方法说明** `MultipartFile` 提供的多个 `Create` 静态方法,实际上是通过调用 `HttpMultipartFormDataBuilder` 的对应方法来构建的,具体如下: - `CreateFromByteArray`:调用 `HttpMultipartFormDataBuilder` 的 `AddByteArray` 方法。 - `CreateFromStream`:调用 `HttpMultipartFormDataBuilder` 的 `AddStream` 方法。 - `CreateFromPath`:调用 `HttpMultipartFormDataBuilder` 的 `AddFileAsStream` 方法。 - `CreateFromBase64String`:调用 `HttpMultipartFormDataBuilder` 的 `AddFileFromBase64String` 方法。 - `CreateFromRemote`:调用 `HttpMultipartFormDataBuilder` 的 `AddFileFromRemote` 方法。 要了解更多请访问 [HttpAgent - 官方仓库 - `HttpMultipartFormDataBuilder`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Builders/HttpMultipartFormDataBuilder.cs#L434) 进行查阅。 --- # 4.17 添加 IFormFile 和 IFormFileCollection 内容 > 原文链接:https://http.furion.net/docs/multipart-builder/adding-iformfile-and-iformfilecollection-content/ 在 `ASP.NET Core` 中,`IFormFile` 接口用于处理单个文件上传,而 `IFormFileCollection` 接口则管理多个文件上传。这两个接口简化了文件上传功能的实现。框架提供了 `AddFile(IFormFile)` 和 `AddFiles(IFormFileCollection)` 扩展方法,便于为多部分表单内容添加文件。示例如下: ```cs showLineNumbers {4-7,9-10} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFile(formFile); multipart.AddFile(formFile, "file"); // 自定义表单名称 multipart.AddFile(formFile, "file", "test.txt"); // 自定义文件名 multipart.AddFile(formFile, "file", "test.txt", "text/plain"); // 自定义媒体类型 multipart.AddFiles(formFiles); multipart.AddFiles(formFiles, "files"); // 自定义表单名称 }); ``` **注意**:若将 `IFormFile` 作为模型属性的类型,例如: ```cs showLineNumbers {5} public class FormClass // 支持属性 [AliasAs] 定义别名 { public int Id { get; set; } public string Name { get; set; } public IFormFile File { get; set; } } ``` 则需确保已注册 `FormFileContentProcessor`。可通过全局或局部调用 `.AddHttpContentProcessors(() => [new FormFileContentProcessor()])` 来完成注册: - 单次请求设置: ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net/") .AddHttpContentProcessors(() => [ new FormFileContentProcessor() ]) ``` - 全局配置: 在 `Startup.cs` 或 `Program.cs` 文件中,配置并注册 `HttpRemote` 服务,以启用 `IFormFile` 内容处理器功能: ```cs showLineNumbers {3} services.AddHttpRemote(builder => { builder.AddHttpContentProcessors(() => [ new FormFileContentProcessor() ]); }); ``` --- # 4.18 添加 IBrowserFile 和 IEnumerable 内容 > 原文链接:https://http.furion.net/docs/multipart-builder/adding-ibrowserfile-and-ienumerableibrowserfile-content/ 在 `Blazor` 中,`IBrowserFile` 接口用于处理单个文件上传,而 `IEnumerable` 接口则管理多个文件上传。这两个接口简化了文件上传功能的实现。框架提供了 `AddFile(IBrowserFile)` 和 `AddFiles(IEnumerable)` 扩展方法,便于为多部分表单内容添加文件。示例如下: ```cs showLineNumbers {4-8,10-12} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFile(browserFile); multipart.AddFile(browserFile, "file"); // 自定义表单名称 multipart.AddFile(browserFile, "file", "test.txt"); // 自定义文件名 multipart.AddFile(browserFile, "file", "test.txt", "text/plain"); // 自定义媒体类型 multipart.AddFile(browserFile, "file", "test.txt", "text/plain", maxAllowedSize: 512000); // 自定义流可以提供的最大字节数 multipart.AddFiles(browserFiles); multipart.AddFiles(browserFiles, "files"); // 自定义表单名称 multipart.AddFiles(browserFiles, "files", maxAllowedSize: 512000); // 自定义流可以提供的最大字节数 }); ``` --- # 4.19 添加 FileInfo 内容 > 原文链接:https://http.furion.net/docs/multipart-builder/adding-fileinfo-content/ 若需上传本地文件,通常需先创建 `FileInfo` 实例,再通过 `OpenRead()` 获取流进行上传。。框架提供了 `AddFile(FileInfo)` 扩展方法,便于为多部分表单内容添加文件。示例如下: ```cs showLineNumbers {4-5} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFile(fileInfo); multipart.AddFile(fileInfo, "file"); // 自定义表单名称 }); ``` --- # 4.20 添加 URL 编码表单内容 > 原文链接:https://http.furion.net/docs/multipart-builder/adding-url-encoded-form-content/ 在多部分表单内容中添加 `URL` 编码表单内容。 ```cs showLineNumbers {4-5,8} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFormUrlEncoded(new { id = 1 ,name = "Furion" }, "form"); multipart.AddFormUrlEncoded(new { id = 1 ,name = "Furion" }, "form", useStringContent: true); // 使用 StringContent 解决 FormUrlEncodedContent 编码问题 // 支持 URL 编码字符串格式 multipart.AddFormUrlEncoded("id=1&name=Furion", "form", useStringContent: true); }); ``` > **`URL` 编码表单内容说明** - **默认情况下,`URL` 编码表单通过 [`FormUrlEncodedContent`](https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Net.Http/src/System/Net/Http/FormUrlEncodedContent.cs#L44) 类型进行构建,但此类型不支持自定义请求内容编码,它默认使用 `Encoding.Latin1` 而不是 `UTF-8`。** 这可能在提交到某些接口时引发异常。 为解决此问题,可以通过设置参数 `useStringContent` 为 `true` 来采用 `StringContent` 方式构建表单数据,从而允许自定义编码为 `UTF-8`。 ```cs showLineNumbers {3} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddURLForm", builder => builder .SetFormUrlEncodedContent(new { id = 1, name = "furion" }, useStringContent: true)); ``` - 某些服务器要求显式声明字符集(`charset`),此时可通过 `contentEncoding` 参数指定编码方式,例如使用 `UTF-8`: ```cs showLineNumbers {3} var content = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddURLForm", builder => builder .SetFormUrlEncodedContent(new { id = 1, name = "furion" }, Encoding.UTF8)); ``` 此设置在发送远程请求时会生成如下 `Content-Type` 请求头:`application/x-www-form-urlencoded; charset=UTF-8`。 --- # 4.21 添加多部分表单内容 > 原文链接:https://http.furion.net/docs/multipart-builder/adding-multipart-form-content/ 在多部分表单内容中添加多部分表单内容的需求并不常见。 ```cs showLineNumbers {4} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddMultipartFormData(new MultipartFormDataContent(), "form"); }); ``` --- # 4.22 添加 HttpContent 内容 > 原文链接:https://http.furion.net/docs/multipart-builder/adding-httpcontent-content/ 添加所有派生自 `HttpContent` 的请求内容。 ```cs showLineNumbers {4-13} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.Add(new StringContent("test")); multipart.Add(new StringContent("test"), "name"); // 设置表单名 multipart.Add(new FormUrlEncodedContent([ new KeyValuePair("id", "1"), new KeyValuePair("name", "furion") ])); multipart.Add(JsonContent.Create(new { id = 1, name = "Furion" }); multipart.Add(new StreamContent(stream)); multipart.Add(new ByteArrayContent(bytes), "bytes"); multipart.Add(new ReadOnlyMemoryContent(new ReadOnlyMemory(bytes)); multipart.Add(new MultipartFormDataContent()); }); ``` > **表单名和内容类型未设置情况** 使用 `Add` 方法添加 `HttpContent` 时: - 如果未指定表单名称,系统会从 `HttpContent.Headers.ContentDisposition` 的 `Name` 属性中自动解析名称。 - 如果未设置内容类型,系统则会尝试从 `HttpContent.Headers.ContentDisposition` 的 `FileName` 属性中自动推断文件的 `MIME` 类型作为内容类型。 --- # 4.23 设置添加表单项内容前的操作 > 原文链接:https://http.furion.net/docs/multipart-builder/setting-the-operation-before-adding-form-item-content/ 在将 `HttpContent` 实例添加到 `MultipartFormDataContent` 对象之前,您可以执行一些预处理操作。例如,在与某些对象存储服务(如阿里云 `OSS`)对接时,您可能需要移除 `Content-Type` 设置(框架已内置该操作)。 ```cs showLineNumbers {7-10} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.SetBoundary("--------------------------") .AddFormItem("1", "id") .AddFormItem("Furion", "name") .SetOnPreAddContent((content, name) => // 委托参数类型为:Action { content.Headers.ContentType = null; }); }); ``` **注意**:`SetOnPreAddContent` 方法支持多次调用,每次调用的结果会累积叠加。 --- # 4.24 设置表单名称策略(转换器) > 原文链接:https://http.furion.net/docs/multipart-builder/setting-the-form-name-policy-transformer/ 在发送 `HTTP` 表单数据时,与直接发送 `application/json` 格式的 `JSON` 数据不同,无法直接利用自定义 `JSON` 序列化选项对属性名称进行格式化。当将对象设置为表单数据时,框架会先将对象转换为 `IDictionary` 类型,再逐项添加为表单字段。因此,该过程不会沿用 `JSON` 序列化的命名规则。 由于 C# 语言中通常采用大驼峰命名法(`PascalCase`)命名属性,而在与某些第三方服务(如 `Java` 编写的 `API`)交互时,对方可能对字段名称大小写敏感,导致请求失败。为此,框架提供了 `SetFormNameTransformer` 方法,用于配置表单字段名称的转换规则。 ```cs showLineNumbers {5} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddObject(new { Id = 1, Name = "Furion"}) .SetFormNameTransformer(FormNamingPolicy.CamelCase); // 使用小驼峰命名法转换表单字段名 }); ``` 框架内置了以下五种常见的命名规则转换方式,同时也支持自定义转换逻辑: - **小驼峰命名法**(`FormNamingPolicy.CamelCase`):例如将 `TempCelsius` 转换为 `tempCelsius`。 - **小写蛇形命名法**(`FormNamingPolicy.SnakeCaseLower`):例如将 `TempCelsius` 转换为 `temp_celsius`。 - **大写蛇形命名法**(`FormNamingPolicy.SnakeCaseUpper`):例如将 `TempCelsius` 转换为 `TEMP_CELSIUS`。 - **小写短横线命名法**(`FormNamingPolicy.KebabCaseLower`):例如将 `TempCelsius` 转换为 `temp-celsius`。 - **大写短横线命名法**(`FormNamingPolicy.KebabCaseUpper`):例如将 `TempCelsius` 转换为 `TEMP-CELSIUS`。 此外,你还可以通过自定义转换器委托实现特定格式,例如为所有字段名称统一添加 `_` 前缀: ```cs showLineNumbers {5} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddObject(new { Id = 1, Name = "Furion"}) .SetFormNameTransformer(name => "_" + name); }); ``` --- # 4.25 设置多部分表单内容项的排序规则 > 原文链接:https://http.furion.net/docs/multipart-builder/setting-the-sort-rule-for-multipart-form-content-items/ 尽管对多部分表单内容项进行排序的需求并不常见,但在一些安全性要求较高的系统中,往往需要校验表单字段的提交顺序。框架为此提供了排序支持: ```cs showLineNumbers {6} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddObject(new { name = "furion", id = 1 }); }) .SetFormItemsSorter(items => items.OrderBy(u => u.Name)); ``` 通过 `.SetFormItemsSorter()` 方法配置多部分表单内容项的排序规则。该方法接收原始 `MultipartFormDataItem` 集合,并返回排序后的可枚举集合。为 `null` 时不排序(原始添加顺序)。 --- # 4.26 添加 HttpMultipartFormDataBuilder 扩展 > 原文链接:https://http.furion.net/docs/multipart-builder/extensions/ 除了系统自带的 `HttpMultipartFormDataBuilder` 方法,您还可以为其添加自定义扩展方法,以简化代码并减少重复。例如,您可以添加一个 `AddRawString` 方法,用于为多部分表单添加原始 `raw` 字符串内容。具体实现如下: ```cs showLineNumbers {1,3,8} public static class HttpMultipartFormDataBuilderExtensions { public static HttpMultipartFormDataBuilder AddRawString(this HttpMultipartFormDataBuilder multipartFormDataBuilder, string? rawString, string name, Encoding? contentEncoding = null) { // 空检查 ArgumentException.ThrowIfNullOrWhiteSpace(name); return multipartFormDataBuilder.AddText($"\"{rawString}\"", name, contentEncoding); } } ``` 之后,您可以轻松地在 `HttpRequestBuilder` 实例中使用此方法: ```cs showLineNumbers {4} HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddRawString("Furion", "body"); }); ``` 利用 `C#` 扩展方法的特性,您可以极大地丰富 `HttpMultipartFormDataBuilder` 的功能,减少重复代码,同时提高代码的可读性和可维护性。 --- # 5.1 HTTP 声明式请求 > 原文链接:https://http.furion.net/docs/declarative/http-declarative-requests/ `HTTP` 声明式请求机制通过实现 `IHttpDeclarative` 接口,在程序运行时动态地构建实现类。该机制会智能地拦截符合特定规则的方法调用,并自动生成相应的 `HTTP` 远程请求代码。这种方法不仅极大地减轻了开发人员编写 `HTTP` 请求代码的负担,而且使得代码结构更加条理分明,更易于进行组织、维护和复用。 --- # 5.2 接口定义与使用 > 原文链接:https://http.furion.net/docs/declarative/interface-definition-and-usage/ 在利用 `HTTP` 声明式请求之前,您需要定义一个接口,并确保它实现 `IHttpDeclarative` 接口: ```cs showLineNumbers {1} public interface IHttpService : IHttpDeclarative { } ``` 随后,在 `Startup.cs` 或 `Program.cs` 文件中,配置并注册 `HttpRemote` 服务,以启用 `HTTP` 声明式请求功能: ```cs showLineNumbers {1,4,7,10,13,16} services.AddHttpRemote(builder => { // 使用泛型方式注册 IHttpService 声明式接口 builder.AddHttpDeclarative(); // 或者使用类型方式 // builder.AddHttpDeclarative(typeof(IHttpService)); // 若需注册多个接口,可使用以下方法(非示例中的数组语法) // builder.AddHttpDeclaratives(new[] { typeof(IHttpService), typeof(IHttpService) }); // 推荐:从程序集中扫描并批量注册 // builder.AddHttpDeclarativesFromAssemblies([Assembly.GetEntryAssembly()]); // 若使用 Furion 框架,可直接传入 App.Assemblies // builder.AddHttpDeclarativesFromAssemblies(App.Assemblies); }); ``` 在服务中使用 `IHttpService` 声明式请求时,可通过构造函数注入: ```cs showLineNumbers {3,5} public class YourService { private readonly IHttpService _httpService; public YourService(IHttpService httpService) { _httpService = httpService; } } ``` 若您使用的是 `.NET 8` 及以上版本时,可利用[主构造函数](https://learn.microsoft.com/zh-cn/dotnet/csharp/whats-new/tutorials/primary-constructors)注入进一步简化代码: ```cs showLineNumbers {1} public class YourService(IHttpService httpService) { // 使用 httpService 变量 } ``` 某些场景下,您也可以仅在特定方法中注入,通过在参数前添加 `[FromServices]` 特性实现: ```cs showLineNumbers {3} public class YourService { public Task GetResource([FromServices] IHttpService httpService) { // 您的业务逻辑 } } ``` 此外,如果您希望动态解析声明式服务,可以先注入 `IHttpRemoteService`,再调用其 `For()` 方法获取实例: ```cs showLineNumbers {1,5} public class YourService(IHttpRemoteService httpRemoteService) { public async Task InvokeAsync() { var httpService = httpRemoteService.For(); } } ``` ### 开放泛型接口 `HTTP` 声明式接口同样支持开放泛型定义,例如: ```cs showLineNumbers {1} public interface IHttpService : IHttpDeclarative { } ``` 需要注意的是,使用程序集扫描方式(如 `builder.AddHttpDeclarativesFromAssemblies(assemblies)`)时会默认跳过开放泛型接口,因为它要求提供运行时的具体类型(即封闭泛型类型)。此时,应显式注册封闭泛型版本: ```cs showLineNumbers {4} services.AddHttpRemote(builder => { // 注册封闭泛型类型,如 IHttpService builder.AddHttpDeclarative>(); // 或者使用类型方式 // builder.AddHttpDeclarative(typeof(IHttpService)); }); ``` 在业务中使用时,可直接通过依赖注入获取指定封闭类型(如 `IHttpService`),或调用 `IHttpRemoteService.For>()` 动态解析服务实例。 ### 无需实现 `IHttpDeclarative` 接口 某些情况下,您可能希望直接为普通接口生成声明式代理,而不强制要求该接口实现 `IHttpDeclarative`。例如,定义一个普通的 `IMyApi` 接口: ```cs showLineNumbers {1,3-4} public interface IMyApi { [Get("https://api.furion.net/users/{id}")] Task GetUserAsync(int id); } ``` 此时在注册时指定 `requireIHttpDeclarative: false`: ```cs showLineNumbers {3} services.AddHttpRemote(builder => { builder.AddHttpDeclarative(typeof(IMyApi), requireIHttpDeclarative: false); }); ``` 注册后,该接口的使用方式与普通声明式接口完全一致,可通过构造函数注入、`[FromServices]` 特性注入或 `IHttpRemoteService.For()` 动态解析。 > **扫描程序集批量注册** **注意**:这种注册方式**不支持**框架内置的程序集扫描方法(如 `AddHttpDeclarativesFromAssemblies`),因为内置扫描默认会检查 `IHttpDeclarative` 接口。若需批量注册此类接口,您可以自行扫描程序集中所有公开的接口,根据自定义约束(例如命名约定、标记特性等)筛选后,手动调用 `AddHttpDeclarative(declarativeType, false)` 完成注册。示例如下: ```cs showLineNumbers {4,8-11} var assemblies = new[] { Assembly.GetExecutingAssembly() }; var apiInterfaces = assemblies .SelectMany(a => a.GetExportedTypes()) .Where(t => t.IsInterface && !t.IsGenericType && t.Name.EndsWith("Api")); // 例如以 Api 结尾的接口 services.AddHttpRemote(builder => { foreach (var interfaceType in apiInterfaces) { builder.AddHttpDeclarative(interfaceType, requireIHttpDeclarative: false); } }); ``` --- # 5.3 定义请求方法 > 原文链接:https://http.furion.net/docs/declarative/defining-request-methods/ 在 `IHttpService` 声明式接口中,您可以定义各种 `API` 请求方法。这些方法需要标记有从 `HttpMethodAttribute` 派生的特性,以指明其对应的 `HTTP` 请求类型。系统预置了多种常见的 `HTTP` 请求方法特性,同时也支持自定义方法特性: ```cs showLineNumbers {4,8,12,16,20,24,28,32,36,40,45} public interface IHttpService : IHttpDeclarative { // 定义 HTTP GET 请求 [Get("https://furion.net/")] Task GetMethodAsync(); // 定义 HTTP PUT 请求 [Put("https://furion.net/")] Task PutMethodAsync(); // 定义 HTTP POST 请求 [Post("https://furion.net/")] Task PostMethodAsync(); // 定义 HTTP DELETE 请求 [Delete("https://furion.net/")] Task DeleteMethodAsync(); // 定义 HTTP HEAD 请求 [Head("https://furion.net/")] Task HeadMethodAsync(); // 定义 HTTP OPTIONS 请求 [Options("https://furion.net/")] Task OptionsMethodAsync(); // 定义 HTTP TRACE 请求 [Trace("https://furion.net/")] Task TraceMethodAsync(); // 定义 HTTP PATCH 请求 [Patch("https://furion.net/")] Task PatchMethodAsync(); // 定义 HTTP QUERY 请求 [Query("https://furion.net/")] Task PatchMethodAsync(); // 自定义 HTTP 请求方法 [HttpMethod("Connect", "https://furion.net/")] Task ConnectMethodAsync(); // 定义泛型方法 [Get("https://furion.net/")] Task GenericMethodAsync(); } ``` > **接口方法命名规则** 在接口方法命名上,建议遵循异步方法命名惯例,即在方法名后添加 `Async` 后缀,以清晰地表明这些方法执行异步操作。 > **未标记 `HttpMethodAttribute` 特性的方法** 若接口中的方法未标记有 `HttpMethodAttribute` 派生特性,在调用时将会抛出 `InvalidOperationException` 异常,提示信息为“`No '[HttpMethod]' annotation was found in method 'System.Threading.Tasks.Task UnknownMethodAsync()' of type 'HttpAgent.Samples.IHttpService'.`”。 ```cs showLineNumbers {4} public interface IHttpService : IHttpDeclarative { // 缺少 [HttpMethod] 特性,将导致异常 Task UnknownMethodAsync(); } ``` ### 自定义请求方法 除了直接利用 `[HttpMethod("Connect", "https://furion.net/")]` 来添加自定义的 `HTTP` 请求方法外,我们还可以创建一个具体的 `ConnectAttribute` 特性类,以提高代码的复用性和可读性。这个特性类将继承自 `HttpMethodAttribute`,并专门用于表示 `Connect` 请求。 ```cs showLineNumbers {1,2,4-5} [AttributeUsage(AttributeTargets.Method)] public sealed class ConnectAttribute : HttpMethodAttribute { public ConnectAttribute(string? requestUri = null) : base("Connect", requestUri) { } } ``` 现在,我们可以在 `IHttpService` 接口中使用自定义的 `[Connect]` 特性来替代之前的 `[HttpMethod("Connect", ...)]` 特性: ```cs showLineNumbers {4} public interface IHttpService : IHttpDeclarative { // 使用自定义 Connect 特性 [Connect("https://furion.net/")] Task ConnectMethodAsync(); } ``` 这样的代码更加简洁明了,同时提升了代码的可维护性和复用性。 --- # 5.4 定义请求地址 > 原文链接:https://http.furion.net/docs/declarative/defining-request-addresses/ 在 `HttpMethodAttribute` 及其派生特性的构造函数中,您可以配置请求的地址。以下展示了如何在 `IHttpService` 接口中利用这些特性来定义不同的请求地址: ```cs showLineNumbers {4,8,12,16,20,24} public interface IHttpService : IHttpDeclarative { // 使用完整 URL 地址 [Get("https://furion.net/")] Task GetFullUrlMethodAsync(); // 使用相对地址(不含前导斜杠) [Get("api/get/user")] Task GetRelativeUrlMethod1Async(); // 使用相对地址(含前导斜杠) [Get("/api/get/user")] Task GetRelativeUrlMethod2Async(); // 请求地址为空字符串,实际请求为 BaseAddress [Get("")] Task GetEmptyUrlMethodAsync(); // 请求地址为 null,实际请求为 BaseAddress [Get(null)] Task GetNullUrlMethodAsync(); // 请求地址为 null,实际请求为 BaseAddress [Get] Task GetNullUrlMethodAsync(); } ``` - 当提供的请求地址为完整 `URL` 时,它将直接作为最终的请求地址。 - 若请求地址为相对地址(无论是否包含前导斜杠 `/`),框架将尝试将其与 `HttpClient` 配置的 `BaseAddress` 合并,以生成最终的请求地址。例如: ```cs showLineNumbers {3} services.AddHttpClient(string.Empty, client => { client.BaseAddress = new Uri("https://furion.net/"); }); ``` 在上述配置中,若请求地址为 `"api/get/user"` 或 `"/api/get/user"`,则最终的请求地址将为 `"https://furion.net/api/get/user"`。 - 若请求地址为空字符串或 `null`,则 `HttpClient` 配置的 `BaseAddress` 将直接作为最终的请求地址。这意味着,如果 `BaseAddress` 为 `"https://furion.net/"`,则最终请求地址也将是 `"https://furion.net/"`。 --- # 5.5 同步与异步方法 > 原文链接:https://http.furion.net/docs/declarative/synchronous-and-asynchronous-methods/ 在 `IHttpService` 的声明式请求接口方法定义中,我们既提供了异步方法的实现,也支持同步方法的定义。例如: ```cs showLineNumbers {5,9} public interface IHttpService : IHttpDeclarative { // 异步请求方法 [Get("https://furion.net/")] Task GetMethodAsync(); // 同步请求方法 [Get("https://furion.net/")] string GetMethod(); } ``` > **小建议** 尽管同步方法使用起来更为直观,但为了最大化硬件资源利用率并提升应用程序的吞吐量,**我们强烈建议采用异步方法**。异步方法不仅能有效避免死锁和资源竞争等问题,还能使您的应用程序在处理 `I/O` 密集型任务时更加高效和响应迅速。 --- # 5.6 定义返回值类型 > 原文链接:https://http.furion.net/docs/declarative/defining-return-value-types/ 在 `HTTP` 声明式请求接口方法中,除了支持常见的 `HTTP` 响应类型如 `string`、`byte[]`、`Stream`、`HttpRequestMessage`、`HttpResponseMessage`、`void`、`IAsyncEnumerable` 和 `IActionResult` 以及它们的异步版本(`Task/Task/ValueTask/ValueTask`)外,还支持自定义类型和框架内置的 `HttpRemoteResult`、`HttpRequestBuilder` 和 `VoidContent` 类型及其异步版本。 ```cs showLineNumbers {3,9,15,21,27,38,44,50,54,60,66,70} public interface IHttpService : IHttpDeclarative { // 字符串类型 [Get("https://furion.net/")] Task GetStringAsync(); [Get("https://furion.net/")] string GetString(); // 字节数组类型 [Get("https://furion.net/")] Task GetBytesAsync(); [Get("https://furion.net/")] byte[] GetBytes(); // Stream 类型 [Get("https://furion.net/")] Task GetStreamAsync(); [Get("https://furion.net/")] Stream GetStream(); // HttpResponseMessage 类型 [Get("https://furion.net/")] Task GetHttpResponseMessageAsync(); [Get("https://furion.net/")] HttpResponseMessage GetHttpResponseMessage(); // 无返回值 [Get("https://furion.net/")] Task GetVoidAsync(); [Get("https://furion.net/")] void GetVoid(); [Get("https://furion.net/")] Task GetVoidContentAsync(); [Get("https://furion.net/")] VoidContent GetVoidContent(); // 框架内置 HttpRemoteResult 类型 [Get("https://furion.net/")] Task> GetHttpRemoteResultAsync(); [Get("https://furion.net/")] HttpRemoteResult GetHttpRemoteResult(); // IActionResult 类型 [Get("https://furion.net/")] Task GetYourModelAsync(); [Get("https://furion.net/")] IActionResult GetYourModel(); // IAsyncEnumerable 类型 [Get("https://furion.net/")] IAsyncEnumerable GetAsyncEnumerable(); // 自定义类型 [Get("https://furion.net/")] Task GetYourModelAsync(); [Get("https://furion.net/")] YourModel GetYourModel(); // ValueTask/ValueTask 类型 [Post("https://furion.net/user/add")] ValueTask PostDataAsync(object data); [Get("https://furion.net/")] ValueTask GetValueTaskAsync(); // HttpRequestBuilder 类型,不发送请求(预检请求) [Get("https://furion.net/")] Task GetRequestBuilderAsync(); // HttpRequestMessage 类型,不发送请求(预检请求) [Get("https://furion.net/")] Task GetRequestMessageAsync(); } ``` > **关于 `VoidContent` 类型** 由于 `void` 关键字不能用作泛型的类型参数,系统提供了 `VoidContent` 类型来表示无返回值的情况。例如,`GetAsAsync` 表示不接收响应内容。 > **返回值类型说明** 默认情况下,当返回值类型不是 `string`、`byte[]`、`Stream`、`HttpResponseMessage`、`void`、`VoidContent`、`IAsyncEnumerable`、`IActionResult` 和 `HttpRemoteResult` 时,其他类型将使用 `System.Text.Json` 进行反序列化处理。 如果需要更改此行为,可以在后续章节中了解如何实现 `IHttpContentConverter` 内容转换器接口进行自定义。 --- # 5.7 设置跟踪标识 > 原文链接:https://http.furion.net/docs/declarative/setting-trace-identifier/ 为请求指定一个唯一标识符,便于跟踪和调试。该标识符将被设置在 `X-Trace-ID` 请求标头中。 `HTTP` 声明式请求通过 `TraceIdentifierAttribute` 特性来设置跟踪标识。相应的 `HTTP` 声明式提取器实现为 [`TraceIdentifierDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/TraceIdentifierDeclarativeExtractor.cs) 类型,该类型负责解析 `TraceIdentifierAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的跟踪标识配置。 ```cs showLineNumbers {2,9} // 在接口定义上应用,影响所有方法 [TraceIdentifier("your-id")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync(); // 在方法上应用 [TraceIdentifier("your-method-id")] [Get("https://furion.net/")] Task GetStringAsync(); } ``` > **`TraceIdentifierAttribute` 特性作用范围** `TraceIdentifierAttribute` 特性适用于方法或接口。 `TraceIdentifierAttribute` 包含以下构造函数和属性: - **构造函数**: - `new(traceIdentifier)`:作用于方法或接口,设置跟踪标识。 - **属性**: - `Identifier`:跟踪标识(`string` 类型)。 --- # 5.8 设置超时时间 > 原文链接:https://http.furion.net/docs/declarative/setting-timeout/ 为单次请求设置超时时长。 `HTTP` 声明式请求通过 `TimeoutAttribute` 特性来设置超时时间。相应的 `HTTP` 声明式提取器实现为 [`TimeoutDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/TimeoutDeclarativeExtractor.cs) 类型,该类型负责解析 `TimeoutAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的超时时间配置。 ```cs showLineNumbers {2,9} // 在接口定义上应用,影响所有方法 [Timeout(100_000)] // 100 秒 public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync(); // 在方法上应用 [Timeout(200_000)] // 200 秒 [Get("https://furion.net/")] Task GetStringAsync(); } ``` > **`HttpClient` 超时时间说明** 在 `HttpClient` 中设置超时时间时,请确保单次请求的超时时间不超过 `HttpClient` 配置的超时时间。例如,如果 `HttpClient` 超时时间设置为 `10` 分钟,而单次请求的超时时间设为 `15` 分钟,那么单次请求在超过 `10` 分钟时仍会触发超时异常。示例代码如下: ```cs showLineNumbers {1,3} services.AddHttpClient(string.Empty, client => { client.Timeout = TimeSpan.FromMinutes(10); // 默认超时时间为 100 秒,需显式设置 }); ``` 因此,**若单次请求需要更长的超时时间,请确保 `HttpClient` 的超时时间设置得相应更长。** > **`TimeoutAttribute` 特性作用范围** `TimeoutAttribute` 特性适用于方法或接口。 `TimeoutAttribute` 包含以下构造函数和属性: - **构造函数**: - `new(milliseconds)`:作用于方法或接口,设置超时时间。 - **属性**: - `Timeout`:超时时间(毫秒)(`double` 类型)。 --- # 5.9 配置重试策略 > 原文链接:https://http.furion.net/docs/declarative/configuring-retry-strategy/ 为单次请求配置重试策略。默认情况下,若已配置重试策略,则当请求出现未被抑制的异常时,将自动触发重试机制。 `HTTP` 声明式请求通过 `RetryAttribute` 特性来配置重试策略。相应的 `HTTP` 声明式提取器实现为 [`RetryDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/RetryDeclarativeExtractor.cs) 类型,该类型负责解析 `RetryAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的重试策略配置。 ```cs showLineNumbers {2,9,13,17,21,25} // 在接口定义上应用,影响所有方法 [Retry(3)] // 最大重试 3 次 public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync(); // 在方法上应用 [Retry(10)] // 最大重试 10 次 [Get("https://furion.net/")] Task GetStringAsync(); [Retry(10, 1000)] // 配置重试间隔时间 [Get("https://furion.net/")] Task GetStringAsync(); [Retry(10, RetryStatusCodes = [401])] // 对特定 HTTP 状态码进行重试 [Get("https://furion.net/")] Task GetStringAsync(); [Retry(10, RetryExceptionTypes = [typeof(InvalidOperationException)])] // 对特定异常类型进行重试 [Get("https://furion.net/")] Task GetStringAsync(); [Retry(RetryIndefinitely = true)] // 设置无限重试,直到成功 [Get("https://furion.net/")] Task GetStringAsync(); } ``` > **`HttpClient` 超时时间对重试的影响说明** 重试的总耗时受 `HttpClient` 超时时间约束。例如,超时设为 `3` 秒,最大重试 `4` 次,每次间隔 `1` 秒,若在 `3` 秒内重试仍未成功,则后续重试将被取消。因此,如需确保重试不被中断,请合理配合超时设置,否则可能导致请求被提前终止或无限阻塞。 > **`RetryAttribute` 特性作用范围** `RetryAttribute` 特性适用于方法或接口。 `RetryAttribute` 包含以下构造函数和属性: - **构造函数**: - `new(maxRetries)`:作用于方法或接口,设置最大重试次数(`0` 表示不重试)。 - `new(maxRetries, retryInterval)`:作用于方法或接口,设置最大重试次数(`0` 表示不重试)和重试间隔基准时间(毫秒)。 - **属性**: - `MaxRetries`:最大重试次数(`int` 类型)。默认值为 `0`,表示不重试。如果设置了 `RetryIntervals`,此值将自动被覆盖为数组长度。 - `RetryInterval`:重试间隔基准时间(毫秒)(`double` 类型)。默认值为 `1000` 毫秒。仅在未设置 `RetryIntervals` 时生效。 - `UseExponentialBackoff`:是否采用指数退避重试(`bool` 类型)。默认值为:`false`。当设置为 `true` 时,每次重试间隔 = `RetryInterval * 2^(retry-1)`。仅在未设置 `RetryIntervals` 时生效。 - `RetryIntervals`:自定义重试间隔数组(毫秒)(`double[]?` 类型)。如果设置了此属性,则重试次数将等于数组长度,`MaxRetries` 和 `UseExponentialBackoff` 将被忽略。每次重试将按顺序使用数组中对应索引的间隔时间。 - `RetryStatusCodes`:需要重试的 `HTTP` 状态码集合(`int[]?` 类型)。若为空,则仅重试因异常引发的失败。 - `RetryExceptionTypes`:需要重试的异常类型集合(`Type[]?` 类型)。若为空,则对所有 `Exception` 进行重试(受 `MaxRetries` 限制)。 - `RetryIndefinitely`:是否无限重试,直到成功(`bool` 类型)。默认值为:`false`。当设置为 `true` 时,`MaxRetries` 和 `RetryIntervals` 的长度将被忽略,一直重试直到成功或发生不可重试的异常。 --- # 5.10 设置路径片段 > 原文链接:https://http.furion.net/docs/declarative/setting-path-segments/ 添加或移除 `URL` 路径片段。 `HTTP` 声明式请求通过 `PathSegmentAttribute` 特性来设置或移除路径片段。相应的 `HTTP` 声明式提取器实现为 [`PathSegmentDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/PathSegmentDeclarativeExtractor.cs) 类型,该类型负责解析 `PathSegmentAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的路径片段配置。 **1. 添加路径片段** 利用 `PathSegmentAttribute` 特性,可以便捷地在接口、方法或参数上添加路径片段。 ```cs showLineNumbers {2-3,7-8,15,19,23} // 在接口定义上应用,影响所有方法 [PathSegment("segment1")] [PathSegment("segment2")] public interface IHttpService : IHttpDeclarative { // 在方法上应用 [PathSegment("segment3")] [PathSegment("segment4")] [Get("https://furion.net/")] Task GetStringAsync(); // 在参数上应用,支持多重指定 [PathSegment("segment3")] [Get("https://furion.net/")] Task GetStringAsync([PathSegment] string segment3, [QueryParam][QueryParam] int lastSegment); // 在参数上可通过 Segment 属性设定默认值,同样可为 segment 参数设定,例如 string? segment = "default" [Get("https://furion.net/")] Task GetStringAsync([PathSegment(Segment = "default")] string? segment); // 冻结参数类型将被忽略 [Get("https://furion.net/")] Task GetStringAsync([PathSegment] CancellationToken cancellationToken); } ``` 若存在重复的路径片段,它们将在后续追加中重复出现(如:`/docs/docs/users/docs/`)。 **2. 移除路径片段** `PathSegmentAttribute` 特性中,**设置 `Remove = true`**,即表示移除该路径片段。在接口、方法或参数上应用有效。 ```cs showLineNumbers {2,7,9} [PathSegment("segment1")] // 添加 segment1 路径片段 [PathSegment("segment2", Remove = true)] // 标记 segment2 为待移除 public interface IHttpService : IHttpDeclarative { [PathSegment("segment2")] // 添加 segment2 路径片段 [PathSegment("segment3")] // 添加 segment3 路径片段 [PathSegment("segment3", Remove = true)] // 标记 segment3 为待移除 [Get("https://furion.net/")] Task GetStringAsync([PathSegment(Remove = true)] string seg); // 动态根据 seg 值标记为待移除 } ``` 在发送 `HTTP` 请求之前,将移除配置中指定的待移除路径片段集合。也就是说,移除操作会在所有设置操作调用之后执行。 在上述示例中,尽管 `GetStringAsync` 方法尝试通过 `[PathSegment]` 特性添加 `segment2` 和 `segment3` 路径片段,但由于随后分别有 `[PathSegment("segment2", Remove = true)]` 和 `[PathSegment("segment3", Remove = true)]` 特性仅指定了 `Remove = true` 属性,因此这两个键在最终构建请求 `URL` 时会被移除。只有 `segment1` 路径片段会保留在请求 `URL` 中。 `PathSegmentAttribute` 包含以下构造函数和属性: - **构造函数**: - `new()`:作用于参数时有效,表示添加路径片段,路径片段为参数值。 - `new(segment)`:作用于方法或接口时,则表示添加指定路径片段操作;作用于参数且参数值为 `null` 时,表示添加路径片段,路径片段为参数 `segment` 的值。 - **属性**: - `Segment`:路径片段(`string` 类型),特性作用于参数且参数值为 `null` 时,可用作默认值。 - `Remove`:是否标记为待删除(`bool` 类型),默认值为 `false`(追加)。 > **冻结参数类型说明** 在系统中,`Action`、`Action`、`HttpCompletionOption` 以及 `CancellationToken` 被视为冻结参数类型,它们专门服务于特定的操作执行。因此,`PathSegmentAttribute` 特性作用于这些参数类型时将被忽略。 > **小知识** `C#` 支持特性合并,使代码更简洁: ```cs showLineNumbers {1,4} [PathSegment("segment1"), PathSegment("segment2")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/"), PathSegment("segment3"), PathSegment("segment4")] Task GetStringAsync(); } ``` --- # 5.11 设置查询参数(URL 参数) > 原文链接:https://http.furion.net/docs/declarative/setting-query-parameters-url-parameters/ 添加、修改或移除 `URL` 查询参数。 `HTTP` 声明式请求通过 `QueryParamAttribute` 特性来设置或移除查询参数。相应的 `HTTP` 声明式提取器实现为 [`QueryParamDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/QueryParamDeclarativeExtractor.cs) 类型,该类型负责解析 `QueryParamAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的查询参数配置。 **1. 添加查询参数** 利用 `QueryParamAttribute` 特性,可以便捷地在接口、方法或参数上添加查询参数。 ```cs showLineNumbers {2-3,7-8,15,19,23,27,31,35,39} // 在接口定义上应用,影响所有方法 [QueryParam("query1", "value1")] [QueryParam("query2", "value2")] public interface IHttpService : IHttpDeclarative { // 在方法上应用 [QueryParam("query3", "value3")] [QueryParam("query4", "value4")] [Get("https://furion.net/")] Task GetStringAsync(); // 在参数上应用,支持 AliasAs 属性指定别名,且可多重指定 [QueryParam("query3", "value3")] [Get("https://furion.net/")] Task GetStringAsync([QueryParam] string query4, [QueryParam][QueryParam(AliasAs = "query5")] int lastQuery); // 在参数上可通过 Value 属性设定默认值,同样可为 age 参数设定,例如 int? age = 30 [Get("https://furion.net/")] Task GetStringAsync([QueryParam(Value = 30)] int? age); // 支持将对象作为查询参数,并指定前缀 [Get("https://furion.net/")] Task GetStringAsync([QueryParam(Prefix = "user")] object obj); // 支持 [AliasAs] 定义别名 [Get("https://furion.net/")] Task GetStringAsync([QueryParam][AliasAs("query5")] int lastQuery); // 支持忽略空值参数,若 str1 值为 null 则忽略 [Get("https://furion.net/")] Task GetStringAsync([QueryParam(IgnoreNullValues = true)] string? str1, [QueryParam] string? str2); // 支持 format 格式化 [Get("https://furion.net/")] Task GetStringAsync([QueryParam(Format = "yyyyMMdd")] DateTime date); // 冻结参数类型将被忽略 [Get("https://furion.net/")] Task GetStringAsync([QueryParam] CancellationToken cancellationToken); } ``` 若存在重复的查询参数键,它们将合并成多个键值对(如 `key1=value1&key1=value2`)。通过设置 `Replace = true` 属性,可以覆盖先前的查询参数和原始 `URL` 地址参数。**默认情况下,值为 `null` 的查询参数会被添加到 URL 中;若需忽略这些参数,可设置 `IgnoreNullValues = true`。** **2. 移除查询参数** `QueryParamAttribute` 特性中,**仅指定查询参数键而不赋值**,即表示移除该参数。在接口或方法上应用有效。 ```cs showLineNumbers {2,7} [QueryParam("query1", "value1")] // 添加 query1 参数 [QueryParam("query2")] // 标记 query2 为待移除 public interface IHttpService : IHttpDeclarative { [QueryParam("query2", "value2")] // 添加 query2 参数 [QueryParam("query3", "value3")] // 添加 query3 参数 [QueryParam("query3")] // 标记 query3 为待移除 [Get("https://furion.net/")] Task GetStringAsync(); } ``` 在发送 `HTTP` 请求之前,将移除配置中指定的待移除查询参数集合。也就是说,移除操作会在所有设置操作调用之后执行。 在上述示例中,尽管 `GetStringAsync` 方法尝试通过 `[QueryParam]` 特性添加 `query2` 和 `query3` 参数,但由于随后分别有 `[QueryParam("query2")]` 和 `[QueryParam("query3")]` 特性仅指定了查询参数键而未赋值,因此这两个键在最终构建请求 `URL` 时会被移除。只有 `query1` 参数会保留在请求 `URL` 中。 **3. `URL` 参数格式化程序** 在设置 `HTTP` 请求的查询参数时,框架会将参数键和值传递给 `IUrlParameterFormatter` 进行格式化。默认实现 `UrlParameterFormatter` 会为每个值生成一个 `key=value` 形式的键值对。但某些类型(如 `DateTime`)可能需要特殊处理,或希望改变整个键值对的输出形态(例如将多个值输出为 `key[0]=val1&key[1]=val2` 这样的数组格式),此时可以通过自定义格式化程序实现。 以下示例展示如何重写 `Format` 方法,以便将 `DateTime` 类型的值格式化为 `yyyyMMdd` 格式,其余类型使用默认处理: ```csharp showLineNumbers {1,4,6-15} public class CustomUrlParameterFormatter : UrlParameterFormatter { /// public override IEnumerable>? Format(UrlFormattingContext context, string key, IEnumerable values) { foreach (var value in values) { if (value is DateTime dateTime) { yield return new(key, dateTime.ToString("yyyyMMdd")); // 格式化 continue; } yield return new(key, FormatValue(context, value)); } } } ``` 完成自定义格式化程序后,可以在配置 `HttpRemoteOptions` 时将其注册为默认的 `URL` 参数格式化器: ```csharp showLineNumbers {2,4} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { options.UrlParameterFormatter = new CustomUrlParameterFormatter(); }); ``` 如此一来,在构建 `URL` 查询参数时,若遇到 `DateTime` 类型的值,框架将自动将其格式化为 `yyyyMMdd` 格式的字符串,从而确保输出符合预期。 **4. `URL` 参数排序** 尽管对 `URL` 查询参数排序的需求相对少见,但在一些对安全性要求较高的系统中,往往需要验证参数的顺序。框架为此提供了排序支持,排序对象为最终的键值对集合: ```cs showLineNumbers {3} HttpRequestBuilder.Get("https://furion.net/") .WithQueryParameters(new { name = "furion", id = 1}) .SetQueryParametersSorter(pairs => pairs.OrderBy(kv => kv.Key)); ``` 通过 `.SetQueryParametersSorter()` 方法配置查询参数排序规则。该方法接收一个 `KeyValuePair` 序列,返回排序后的新序列。为 `null` 时不排序(原始添加顺序)。 `QueryParamAttribute` 包含以下构造函数和属性: - **构造函数**: - `new()`:作用于参数时有效,表示添加查询参数,默认键为参数名。 - `new(name)`:作用于方法或接口时,则表示移除指定查询参数操作;作用于参数时,表示添加查询参数,键为参数 `name` 的值。 - `new(name, value)`:作用于接口、方法或参数,表示添加查询参数,键为参数 `name` 的值,优先级低于 `AliasAs` 属性。 - **属性**: - `Name`:查询参数键(`string` 类型),优先级低于 `AliasAs` 属性。 - `Value`:查询参数的值(`object` 类型),当特性作用于参数时,表示默认值。 - `AliasAs`:查询参数键别名(`string` 类型),优先级高于 `Name` 属性。 - `Prefix`:查询参数前缀(`string` 类型),仅对象参数有效。 - `Replace`:是否替换已存在的查询参数(`bool` 类型),默认值为 `false`(追加)。 - `IgnoreNullValues`:是否忽略空值(`null`)的查询参数(`bool` 类型),默认值为 `false`(不忽略)。 - `Format`:要使用的格式(`string?` 类型),仅当 `Value` 实现 `IFormattable` 时有效。 > **冻结参数类型说明** 在系统中,`Action`、`Action`、`HttpCompletionOption` 以及 `CancellationToken` 被视为冻结参数类型,它们专门服务于特定的操作执行。因此,`QueryParamAttribute` 特性作用于这些参数类型时将被忽略。 > **小知识** `C#` 支持特性合并,使代码更简洁: ```cs showLineNumbers {1,4} [QueryParam("query1", "value1"), Query("query2", "value2")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/"), Query("query3", "value3"), Query("query4", "value4")] Task GetStringAsync(); } ``` --- # 5.12 设置请求标头 > 原文链接:https://http.furion.net/docs/declarative/setting-request-headers/ 添加、修改或移除请求标头。 `HTTP` 声明式请求通过 `HeaderAttribute` 特性来设置或移除请求标头。相应的 `HTTP` 声明式提取器实现为 [`HeaderDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HeaderDeclarativeExtractor.cs) 类型,该类型负责解析 `HeaderAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的请求标头配置。 **1. 添加请求标头** 利用 `HeaderAttribute` 特性,可以便捷地在接口、方法或参数上添加请求标头。 ```cs showLineNumbers {2-3,7-8,15,19,23,27,32,36} // 在接口定义上应用,影响所有方法 [Header("header1", "value1")] [Header("header2", "value2")] public interface IHttpService : IHttpDeclarative { // 在方法上应用 [Header("header3", "value3")] [Header("header4", "value4")] [Get("https://furion.net/")] Task GetStringAsync(); // 在参数上应用,支持 AliasAs 属性指定别名,且可多重指定 [Header("header3", "value3")] [Get("https://furion.net/")] Task GetStringAsync([Header] string header4, [Header][Header(AliasAs = "header5")] int lastHeader); // 在参数上可通过 Value 属性设定默认值,同样可为 age 参数设定,例如 int? age = 30 [Get("https://furion.net/")] Task GetStringAsync([Header(Value = 30)] int? age); // 支持 [AliasAs] 定义别名 [Get("https://furion.net/")] Task GetStringAsync([Header][AliasAs("header5")] int lastHeader); // 支持使用冒号(:)配置 [Get("https://furion.net/")] [Header("User-Agent: HttpAgent")] Task GetStringAsync(); // 支持 format 格式化 [Get("https://furion.net/")] Task GetStringAsync([Header(Format = "yyyyMMdd")] DateTime date); // 冻结参数类型将被忽略 [Get("https://furion.net/")] Task GetStringAsync([Header] CancellationToken cancellationToken); } ``` 若存在重复的请求标头,它们将被合并,并用逗号加空格(`, `)分隔多个值。通过设置 `Replace = true` 属性,可以覆盖先前的请求标头设置。 **2. 移除请求标头** `HeaderAttribute` 特性中,**仅指定请求标头键而不赋值**,即表示移除该标头。在接口或方法上应用有效。 ```cs showLineNumbers {2,7} [Header("header1", "value1")] // 添加 header1 标头 [Header("header2")] // 标记 header2 为待移除 public interface IHttpService : IHttpDeclarative { [Header("header2", "value2")] // 添加 header2 标头 [Header("header3", "value3")] // 添加 header3 标头 [Header("header3")] // 标记 header3 为待移除 [Get("https://furion.net/")] Task GetStringAsync(); } ``` 在发送 `HTTP` 请求之前,将移除配置中指定的待移除请求标头集合。也就是说,移除操作会在所有设置操作调用之后执行。 在上述示例中,尽管 `GetStringAsync` 方法尝试通过 `[Header]` 特性添加 `header2` 和 `header3` 标头,但由于随后分别有 `[Header("header2")]` 和 `[Header("header3")]` 特性仅指定了请求标头键而未赋值,因此这两个键在最终构建请求标头时会被移除。只有 `header1` 标头会保留在请求标头中。 `HeaderAttribute` 包含以下构造函数和属性: - **构造函数**: - `new()`:作用于参数时有效,表示添加请求标头,默认键为参数名。 - `new(name)`:作用于方法或接口时,若配置字符串不含冒号(`:`),则表示移除指定请求标头;若含冒号,则以第一个冒号为分隔,左侧为键,右侧为值。作用于参数时,表示添加请求标头,键为参数 `name` 的值。 - `new(name, value)`:作用于接口、方法或参数,表示添加请求标头,键为参数 `name` 的值,优先级低于 `AliasAs` 属性。 - **属性**: - `Name`:请求标头键(`string` 类型),优先级低于 `AliasAs` 属性。 - `Value`:请求标头的值(`object` 类型),当特性作用于参数时,表示默认值。 - `AliasAs`:请求标头键别名(`string` 类型),优先级高于 `Name` 属性。 - `Escape`:是否转义请求标头值(`bool` 类型),默认值为 `false`(不转义)。 - `Replace`:是否替换已存在的请求标头(`bool` 类型),默认值为 `false`(追加)。 - `Format`:要使用的格式(`string?` 类型),仅当 `Value` 实现 `IFormattable` 时有效。 > **冻结参数类型说明** 在系统中,`Action`、`Action`、`HttpCompletionOption` 以及 `CancellationToken` 被视为冻结参数类型,它们专门服务于特定的操作执行。因此,`HeaderAttribute` 特性作用于这些参数类型时将被忽略。 > **小知识** `C#` 支持特性合并,使代码更简洁: ```cs showLineNumbers {1,4} [Header("header1", "value1"), Header("header2", "value2")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/"), Header("header3", "value3"), Header("header4", "value4")] Task GetStringAsync(); } ``` > **配置参数支持** 请求标头支持配置参数,用于读取配置信息进行替换操作。配置参数使用 `[[key]]` 语法。 --- # 5.13 设置路径参数(模板/配置参数) > 原文链接:https://http.furion.net/docs/declarative/setting-path-parameters-templateconfiguration-parameters/ 在 `URL` 路径中替换对象模板字符串。 `HTTP` 声明式请求通过 `PathAttribute` 特性和方法定义的非冻结类型参数来配置路径参数。相应的 `HTTP` 声明式提取器为 [`PathDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/PathDeclarativeExtractor.cs) 类型,它负责解析这些 `PathAttribute` 特性及方法中定义的非冻结类型参数,并构建 `HttpRequestBuilder` 实例所需的路径参数配置。 ```cs showLineNumbers {2-3,7-8,12,15,18,23} // 在接口定义上应用,影响所有方法 [Path("path1", "value1")] [Path("path2", "value2")] public interface IHttpService : IHttpDeclarative { // 在方法上应用 [Path("path3", "value3")] [Get("https://furion.net/{path1}/{path2}/{path3}")] Task GetStringAsync(); // 方法上定义的非冻结类型参数默认会添加到路径参数中,可在 URL 地址中直接使用 [Get("https://furion.net/{path1}/{path2}/?id={id}&name={name}&address={address}&age={age}&name1={user.Name}&obj={obj}")] Task GetStringAsync(int id, string name, string[] address, int age, User user, object? obj); [Get("https://furion.net/{name?}")] // "?" 结尾表示键不存在时替换为空字符串,可结合 [RemoveTrailingSlash] 特性使用 Task GetStringAsync(string name); [Get("https://furion.net/{**path}")] // "**" 开头表示不对路径分割符 "/" 转义 Task GetStringAsync(string path); // 冻结参数类型将被忽略 [Get("https://furion.net/")] Task GetStringAsync(CancellationToken cancellationToken); } ``` 若存在重复的路径参数键,则后设置的键值会覆盖先前的设置。 **模板路径语法** 模板路径除了直接使用 `{key}` 外,还支持通过 `.` 访问对象的属性、嵌套属性,以及通过 `[index]` 访问集合中的元素。此外,对象类型的属性在未找到同名属性时,会自动尝试将其视为字典,并以路径标识符作为键进行取值(等同于 `dict["key"]`)。 - `{key}`:直接替换对应的值。 - `{key.property}`:访问 `key` 对象的 `property` 属性,或当 `key` 为字典时访问键为 `"property"` 的值。 - `{key.property.nested}`:多级属性/键访问。 - `{list[0]}`:访问 `list` 集合(数组、`List` 等)中索引为 `0` 的元素。 - `{user.names[1]}`:先访问 `user` 对象的 `names` 属性,再取其索引 `1` 处的元素。 - `{dic.key}`:当 `dic` 是字典时(包括 `Dictionary` 和 `Hashtable` 等),`dic.key` 会作为 `dic["key"]` 取值。 - `{obj.dictProp.someKey[0].another}`:混合使用点号和索引,逐级深入取值。 > **字典值中的 `JSON` 嵌套取值** 当使用字典(`IDictionary`)作为数据源时,如果某个键的值本身就是合法的 `JSON` 字符串(例如对象或数组),框架会自动解析该 `JSON` 并通过 `.` 和 `[index]` 继续访问内部数据。 例如:字典中包含 `["user"] = "{\"name\":\"Monk\",\"tags\":[\"A\",\"B\"]}"`,则 `{user.name}` 将替换为 `Monk`,`{user.tags[0]}` 将替换为 `A`。这样只需要将复杂对象序列化为 `JSON` 存入字典,即可用统一占位符语法进行深层取值,极大简化模板拼接逻辑。 以上路径都支持在结尾添加 `?` 表示值不存在时替换为空字符串,以及添加 `**` 前缀表示不对路径分隔符 `/` 进行转义。 > **`PathAttribute` 特性作用范围** `PathAttribute` 特性仅适用于方法或接口,不适用于参数。因为**方法上定义的非冻结类型参数默认会添加到路径参数中**,因此无需手动标记。 > **冻结参数类型说明** 在系统中,`Action`、`Action`、`HttpCompletionOption` 以及 `CancellationToken` 被视为冻结参数类型,它们专门服务于特定的操作执行。因此,这些参数类型时将被忽略作为路径参数。 `PathAttribute` 包含以下构造函数和属性: - **构造函数**: - `new(name, value)`:作用于接口或方法,表示添加路径参数,键为参数 `name` 的值。 - **属性**: - `Name`:路径参数键(`string` 类型)。 - `Value`:路径参数的值(`object` 类型)。 > **小知识** `C#` 支持特性合并,使代码更简洁: ```cs showLineNumbers {1,4} [Path("path1", "value1"), Path("path2", "value2")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/{path1}/{path2}/{path3}"), Path("path3", "value3")] Task GetStringAsync(); } ``` --- **配置参数** 除了通过 `{key}` 模板语法设置路径参数外,框架还提供了配置参数,用于读取配置信息进行替换操作。配置参数使用 `[[key]]` 语法,例如: ```cs showLineNumbers {3} public interface IHttpService : IHttpDeclarative { [Get("https://furion.net?id=[[id]]&name=[[name]]")] Task GetStringAsync(); } ``` **启用配置参数支持** 要在 `HttpRemote` 服务中启用配置参数支持,请按照以下步骤进行配置: ```cs showLineNumbers {2,5} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // 设置用于替换 URL 地址中配置模板参数的提供源 options.Configuration = builder.Configuration; // 若使用 Furion 框架可直接设置 App.Configuration }); ``` **配置参数的使用** 配置参数将从您的配置文件中读取并替换到 `URL` 中。例如,您的配置文件可能如下所示: ```json showLineNumbers title="appsettings.json" { "id": 1, "name": "Furion" } ``` 配置参数的键支持多种格式语法,以便更灵活地访问配置文件中的值: - `[[key]]`:直接访问 `key` 对应的值。 - `[[key:sub]]`:访问 `key` 下的 `sub` 子项的值。 - `[[key:sub:nest]]`:访问 `key` 下的 `sub` 子项中的 `nest` 子项的值。 - 备用值查找: - `[[notfound | bak]]`:如果 `notfound` 不存在,则查找 `bak`。 - `[[notfound | bak | other]]`:如果 `notfound` 和 `bak` 都不存在,则查找 `other`。 - `[[notfound | bak:sub | other:sub:nest]]`:支持更深层次的备用查找。 - 默认值: - `[[notfound || default]]`:如果 `notfound` 不存在,则使用 `default` 作为值。 - `[[notfound | bak | other || 默认值]]`:结合备用查找和默认值,确保总有值可用。 --- # 5.14 设置 Cookie > 原文链接:https://http.furion.net/docs/declarative/setting-cookie/ 添加、修改或移除 `Cookie`。 `HTTP` 声明式请求通过 `CookieAttribute` 特性来设置或移除 `Cookie` 。相应的 `HTTP` 声明式提取器实现为 [`CookieDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/CookieDeclarativeExtractor.cs) 类型,该类型负责解析 `CookieAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的 `Cookie` 配置。 **1. 添加 `Cookie`** 利用 `CookieAttribute` 特性,可以便捷地在接口、方法或参数上添加 `Cookie` 。 ```cs showLineNumbers {2-3,7-8,15,19,23,27,31} // 在接口定义上应用,影响所有方法 [Cookie("cookie1", "value1")] [Cookie("cookie2", "value2")] public interface IHttpService : IHttpDeclarative { // 在方法上应用 [Cookie("cookie3", "value3")] [Cookie("cookie4", "value4")] [Get("https://furion.net/")] Task GetStringAsync(); // 在参数上应用,支持 AliasAs 属性指定别名,且可多重指定 [Cookie("cookie3", "value3")] [Get("https://furion.net/")] Task GetStringAsync([Cookie] string cookie4, [Cookie][Cookie(AliasAs = "cookie5")] int lastCookie); // 在参数上可通过 Value 属性设定默认值,同样可为 age 参数设定,例如 int? age = 30 [Get("https://furion.net/")] Task GetStringAsync([Cookie(Value = 30)] int? age); // 支持 [AliasAs] 定义别名 [Get("https://furion.net/")] Task GetStringAsync([Cookie][AliasAs("cookie5")] int lastCookie); // 支持 format 格式化 [Get("https://furion.net/")] Task GetStringAsync([Cookie(Format = "yyyyMMdd")] DateTime date); // 冻结参数类型将被忽略 [Get("https://furion.net/")] Task GetStringAsync([Cookie] CancellationToken cancellationToken); } ``` 若存在重复的 `Cookie` 键,则后设置的键值会覆盖先前的设置。 **2. 移除 `Cookie`** `CookieAttribute` 特性中,**仅指定 `Cookie` 键而不赋值**,即表示移除该 `Cookie`。在接口或方法上应用有效。 ```cs showLineNumbers {2,7} [Cookie("cookie1", "value1")] // 添加 cookie1 [Cookie("cookie2")] // 标记 cookie2 为待移除 public interface IHttpService : IHttpDeclarative { [Cookie("cookie2", "value2")] // 添加 cookie2 [Cookie("cookie3", "value3")] // 添加 cookie3 [Cookie("cookie3")] // 标记 cookie3 为待移除 [Get("https://furion.net/")] Task GetStringAsync(); } ``` 在发送 `HTTP` 请求之前,将移除配置中指定的待移除 `Cookie` 集合。也就是说,移除操作会在所有设置操作调用之后执行。 在上述示例中,尽管 `GetStringAsync` 方法尝试通过 `[Cookie]` 特性添加 `cookie2` 和 `cookie3`,但由于随后分别有 `[Cookie("cookie2")]` 和 `[Cookie("cookie3")]` 特性仅指定了 `Cookie` 键而未赋值,因此这两个键在最终构建请求标头 `Cookie` 时会被移除。只有 `cookie1` 参数会保留在请求标头 `Cookie` 中。 `CookieAttribute` 包含以下构造函数和属性: - **构造函数**: - `new()`:作用于参数时有效,表示添加 `Cookie` ,默认键为参数名。 - `new(name)`:作用于方法或接口时,则表示移除指定 `Cookie` 操作;作用于参数时,表示添加 `Cookie` ,键为参数 `name` 的值。 - `new(name, value)`:作用于接口、方法或参数,表示添加 `Cookie` ,键为参数 `name` 的值,优先级低于 `AliasAs` 属性。 - **属性**: - `Name`:`Cookie` 键(`string` 类型),优先级低于 `AliasAs` 属性。 - `Value`:`Cookie` 的值(`object` 类型),当特性作用于参数时,表示默认值。 - `AliasAs`:`Cookie` 键别名(`string` 类型),优先级高于 `Name` 属性。 - `Format`:要使用的格式(`string?` 类型),仅当 `Value` 实现 `IFormattable` 时有效。 > **冻结参数类型说明** 在系统中,`Action`、`Action`、`HttpCompletionOption` 以及 `CancellationToken` 被视为冻结参数类型,它们专门服务于特定的操作执行。因此,`CookieAttribute` 特性作用于这些参数类型时将被忽略。 > **小知识** `C#` 支持特性合并,使代码更简洁: ```cs showLineNumbers {1,4} [Cookie("cookie1", "value1"), Cookie("cookie2", "value2")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/"), Cookie("cookie3", "value3"), Cookie("cookie4", "value4")] Task GetStringAsync(); } ``` > **配置参数支持** `Cookie` 值支持配置参数,用于读取配置信息进行替换操作。配置参数使用 `[[key]]` 语法。 --- # 5.15 设置 HttpClient 实例的名称(多个基地址) > 原文链接:https://http.furion.net/docs/declarative/setting-the-httpclient-instance-name-multiple-base-addresses/ 系统默认使用 `IHttpClientFactory` 创建 `HttpClient` 实例,并将默认客户端名称设为空字符串(`string.Empty`)。您可以通过 `HttpClientNameAttribute` 特性设置创建 `HttpClient`实例时的客户端名称。 `HTTP` 声明式请求通过 `HttpClientNameAttribute` 特性来设置 `HttpClient` 实例的名称。相应的 `HTTP` 声明式提取器实现为 [`HttpClientNameDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpClientNameDeclarativeExtractor.cs) 类型,该类型负责解析 `HttpClientNameAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的 `HttpClient` 实例的名称配置。 ```cs showLineNumbers {2,9} // 在接口定义上应用,影响所有方法 [HttpClientName(string.Empty)] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync(); // 默认客户端 // 在方法上应用 [HttpClientName("weixin")] // 指定为名为 "weixin" 的客户端 [Get("https://furion.net/")] Task GetStringAsync(); } ``` 您还可以在 `Startup.cs` 或 `Program.cs` 文件中为命名 `HttpClient` 客户端提供配置: ```cs showLineNumbers {2,5} // 配置默认客户端(名称为空字符串) services.AddHttpClient(string.Empty, client => { }); // 配置名为 "weixin" 的客户端 services.AddHttpClient("weixin", client => { }); ``` > **`HttpClientNameAttribute` 特性作用范围** `HttpClientNameAttribute` 特性适用于方法或接口。 `HttpClientNameAttribute` 包含以下构造函数和属性: - **构造函数**: - `new(name)`:作用于方法或接口,设置 `HttpClient` 实例的名称。 - **属性**: - `Name`:`HttpClient` 实例的名称(`string` 类型)。 --- # 5.16 设置请求内容(Body) > 原文链接:https://http.furion.net/docs/declarative/setting-request-content-body/ 支持设置任意类型的请求内容。 `HTTP` 声明式请求通过 `BodyAttribute` 特性来配置请求内容。相应的 `HTTP` 声明式提取器为 [`BodyDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/BodyDeclarativeExtractor.cs) 类型,它负责解析**单个** `BodyAttribute` 特性,并构建 `HttpRequestBuilder` 实例所需的请求内容配置。 ```cs showLineNumbers {5,9,13,17,21,25,29,32,36,40} public interface IHttpService : IHttpDeclarative { // 标记参数为请求内容 [Post("https://furion.net/")] Task PostStringAsync([Body] object body); // 自动推断 Content-Type // 支持设置 Content-Type [Post("https://furion.net/")] Task PostStringAsync([Body("application/json")] object body); // 或使用 [Body(MediaTypeNames.Application.Json)] // 支持设置 Content-Type 和字符集 [Post("https://furion.net/")] Task PostStringAsync([Body("application/json; charset=utf-8")] object body); // URL 编码表单 [Post("https://furion.net/")] Task PostStringAsync([Body("application/x-www-form-urlencoded")] object body); // 使用 StringContent 构建 URL 编码表单 [Post("https://furion.net/")] Task PostStringAsync([Body("application/x-www-form-urlencoded", UseStringContent = true)] object body); // 可配置不进行 URL 编码处理 [Post("https://furion.net/")] Task PostStringAsync([Body("application/x-www-form-urlencoded", urlEncode = false)] object body); // 支持原始 raw 字符串内容 [Post("https://furion.net/")] Task PostStringAsync([Body(RawString = true)] string body); // 默认内容类型 text/plain [Post("https://furion.net/")] Task PostStringAsync([Body("application/json", RawString = true)] string body); // 支持配置文件路径(或互联网地址) [Post("https://furion.net/")] Task PostStringAsync([Body(AsFile = true)] string filePath); // 冻结参数类型将被忽略 [Post("https://furion.net/")] Task PostStringAsync([Body] CancellationToken cancellationToken); } ``` > **多种 `Body` 参数声明特性** 为方便快速标注请求内容参数,框架提供了多种常用的 `Body` 特性,如 `[JsonBody]`、`[HtmlBody]`、`[FormUrlEncodedBody]`、`[RawStringBody]`、`[TextBody]` 和 `[XmlBody]`。 > **未提供 `Content-Type` 时的默认行为** 当未指定 `Content-Type` 时,框架会根据以下优先级确定 `Content-Type`: 1. **请求内容标头**:如果通过 `WithHeader` 等方式设置了 `Content-Type`,则优先使用。 2. **内容类型自动推断**:如果未设置内容标头,框架会根据 `RawContent` 的具体类型,按以下规则自动推断: - **`JsonContent`**:`application/json` - **`JsonNode` 或 `JsonElement`** - 若表示 `JSON` 对象或数组,则为 `application/json` - 否则(如 `JSON` 标量值)回退为 `text/plain` - **`FormUrlEncodedContent`**:`application/x-www-form-urlencoded` - **`StringContent`**:`text/plain` - **`MultipartFormDataContent`**:`multipart/form-data` - **`MultipartContent`**(非 `FormData` 子类):`multipart/mixed` - **`ByteArrayContent`、`StreamContent`、`ReadOnlyMemoryContent`**:`application/octet-stream` - **`byte[]`、`Stream`、`ReadOnlyMemory`**:`application/octet-stream` - **其他自定义 `HttpContent` 子类**(未设置标头且未命中上述具体类型):`application/octet-stream` - **`MultipartFile`**:`application/octet-stream` - **`FileInfo`**:根据文件扩展名通过 `FileTypeMapper` 推断,若无法识别则默认为 `application/octet-stream` - **其他复杂对象**(非基础类型、非枚举、非集合):`application/json`(假定将其序列化为 `JSON`) 3. **全局默认回退值**:如果以上规则均未命中,则使用 `HttpClientOptions.DefaultContentType` 中配置的值。该值默认为 `text/plain`,如需修改此回退值,可通过以下代码配置: ```cs showLineNumbers {2,5} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // 设置默认的请求内容类型(最终回退值) options.DefaultContentType = "application/json"; }); ``` `BodyAttribute` 标记的参数通过底层的 `httpRequestBuilder.SetContent` 方法进行设置,支持任意非冻结类型的参数。 > **`BodyAttribute` 特性作用范围** `BodyAttribute` 特性仅适用于参数。 > **冻结参数类型说明** 在系统中,`Action`、`Action`、`HttpCompletionOption` 以及 `CancellationToken` 被视为冻结参数类型,它们专门服务于特定的操作执行。因此,`BodyAttribute` 特性作用于这些参数类型时将被忽略。 > **多个参数标记 `BodyAttribute` 特性** 由于请求内容只能包含一个值,如果方法中有多个 `BodyAttribute` 特性标记的参数,将抛出 `InvalidOperationException` 异常。异常消息为:`The input sequence contains more than one element.`。 > **`URL` 编码表单内容说明** - **默认情况下,`URL` 编码表单通过 [`FormUrlEncodedContent`](https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Net.Http/src/System/Net/Http/FormUrlEncodedContent.cs#L44) 类型进行构建,但此类型不支持自定义请求内容编码,它默认使用 `Encoding.Latin1` 而不是 `UTF-8`。** 这可能在提交到某些接口时引发异常。 为解决此问题,可以通过设置属性 `UseStringContent` 为 `true` 来采用 `StringContent` 方式构建表单数据,从而允许自定义编码为 `UTF-8`。 ```cs showLineNumbers {4} public interface IHttpService : IHttpDeclarative { [Post("https://furion.net/")] Task PostStringAsync([Body("application/x-www-form-urlencoded", UseStringContent = true)] object body); // body 同时支持 URL 编码字符串,例如:id=1&name=furion } ``` - 某些服务器要求显式声明字符集(`charset`),此时可通过 `contentEncoding` 参数指定编码方式,例如使用 `UTF-8`: ```cs showLineNumbers {3} public interface IHttpService : IHttpDeclarative { [Post("https://furion.net/")] Task PostStringAsync([Body("application/x-www-form-urlencoded", "UTF-8")] object body); // body 同时支持 URL 编码字符串,例如:id=1&name=furion } ``` 此设置在发送远程请求时会生成如下 `Content-Type` 请求头:`application/x-www-form-urlencoded; charset=UTF-8`。 `BodyAttribute` 包含以下构造函数和属性: - **构造函数**: - `new()`:作用于参数,将参数作为请求内容。 - `new(contentType)`:作用于参数,将参数作为请求内容,支持设置内容类型。 - `new(contentType, contentEncoding)`:作用于参数,将参数作为请求内容,支持设置内容类型和编码。 - **属性**: - `ContentType`:内容类型(`string` 类型)。 - `ContentEncoding`:内容编码(`string` 类型)。 - `UseStringContent`:是否使用 `StringContent` 构建 [`FormUrlEncodedContent`](https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Net.Http/src/System/Net/Http/FormUrlEncodedContent.cs#L44),默认值为 `false`,仅当 `ContentType` 为 `application/x-www-form-urlencoded` 时有效。 - `UrlEncode`:是否对表单数据进行 `URL` 编码(`bool` 类型),默认值为 `true`。 - `RawString`:是否为原始字符串内容(`bool` 类型),默认值为 `false`,仅当参数为字符串类型且此属性为 `true` 时有效。 - `AsFile`:将字符串作为文件路径(支持互联网地址)(`bool` 类型),默认值为 `false`,仅当参数为字符串类型且此属性为 `true` 时有效。 - `DisposeResourcesOnRequestCompletion`:是否在请求结束后自动释放资源(`bool` 类型),默认值为 `false`。 --- # 5.17 设置多部分表单内容(复杂表单/上传文件) > 原文链接:https://http.furion.net/docs/declarative/multipart/ 将请求的内容类型设置为 `multipart/form-data` 并发送多部分表单内容。 `HTTP` 声明式请求通过 `MultipartAttribute` 特性来设置多部分表单内容。相应的 `HTTP` 声明式提取器实现为 [`MultipartDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/MultipartDeclarativeExtractor.cs) 类型,该类型负责解析 `MultipartAttribute` 和 `MultipartFormAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的多部分表单内容配置。 ```cs showLineNumbers {6-12,17-20,24,27,33,38} public interface IHttpService : IHttpDeclarative { // 添加常见表单项内容 [Post("https://furion.net/")] Task PostStringAsync( [Multipart] int id, [Multipart] string name, [Multipart] object obj, [Multipart] Stream stream, [Multipart("bytes")] byte[] byteArray, // 自定义表单名,还可以通过 FileName 属性指定文件名 [Multipart] StringContent content [Multipart] MultipartFile file); // 添加文件内容 [Post("https://furion.net/")] Task PostStringAsync( [Multipart(AsFileFrom = FileSourceType.None)] string none, // 不做任何操作 [Multipart("files", AsFileFrom = FileSourceType.Path, ContentType = "image/jpeg")] string filePath, // 从本地文件路径中添加,不传 Content-Type 将自动根据文件扩展名解析 [Multipart("files", AsFileFrom = FileSourceType.Base64String)] string base64String, // 从 Base64 字符串文件中添加,不传 Content-Type 将自动根据文件扩展名解析 [Multipart("files", AsFileFrom = FileSourceType.Remote)] string remote); // 从互联网文件地址中添加,不传 Content-Type 将自动根据文件扩展名解析 // 添加对象内容,AsFormItem 为 false 时,对象属性会被解析并遍历,其属性将作为独立的表单项进行设置 [Post("https://furion.net/")] Task PostStringAsync([Multipart(AsFormItem = false)] object obj); // 推荐使用 [MultipartObject] // 设置多部分表单内容的边界 [MultipartForm("--------------------")] [Post("https://furion.net/")] Task PostStringAsync([Multipart] int id); // 设置表单名称命名策略(转换器) [Post("https://furion.net/")] [MultipartForm(NamingPolicy = FormNamingPolicy.CamelCase)] Task PostStringAsync([MultipartObject] object obj); // 冻结参数类型将被忽略 [Post("https://furion.net/")] Task PostStringAsync([Multipart] CancellationToken cancellationToken); } ``` ### 包含文件(或二进制数据)的复杂表单 在处理包含基础数据与文件(或二进制数据)的复杂表单时,可以使用 `[MultipartObject]` 特性标记对应的复杂类型。其中,文件字段推荐使用 `MultipartFile` 类型声明文件字段。示例接口定义如下: ```cs showLineNumbers {4} public interface IHttpService : IHttpDeclarative { [Post("https://furion.net/")] Task PostStringAsync([MultipartObject] FormClass data); } ``` 对应的模型类定义如下: ```cs showLineNumbers {5,7} public class FormClass // 支持属性 [AliasAs] 定义别名 { public int Id { get; set; } public string Name { get; set; } public MultipartFile File { get; set; } // public IFormFile File { get; set; } // 注意:需要按照以下步骤配置 } ``` **注意**:若使用 `IFormFile` 替代 `MultipartFile`,需确保已注册 `FormFileContentProcessor`。可通过全局调用 `.AddHttpContentProcessors(() => [new FormFileContentProcessor()])` 来完成注册: 在 `Startup.cs` 或 `Program.cs` 文件中,配置并注册 `HttpRemote` 服务,以启用 `IFormFile` 内容处理器功能: ```cs showLineNumbers {3} services.AddHttpRemote(builder => { builder.AddHttpContentProcessors(() => [ new FormFileContentProcessor() ]); }); ``` 通过这种方式,框架会自动将对象中的基本类型属性作为普通表单项提交,并将 `MultipartFile` 类型的属性作为文件上传内容进行正确编码和传输。 > **`JSON` 序列化配置说明** **注意**:当传入类型对象时,框架会先将对象转换为 `IDictionary` 类型,再逐条添加为表单项。因此,该过程不会直接使用 `JSON` 序列化的配置。如需为属性指定别名,请通过 `[AliasAs]` 特性或使用 `[MultipartForm(NamingPolicy)]` 特性进行定义。 `MultipartAttribute` 标记的参数通过底层的 `httpRequestBuilder.SetMultipartContent` 方法进行设置,支持任意非冻结类型的参数。以下代码示例展示了如何使用 `HttpRequestBuilder` 达到相同配置效果: ```cs showLineNumbers {5-11,18-20,27,34-35,42} // 添加常见表单项内容 HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFormItem(1, "id"); multipart.AddFormItem("Furion", "name"); multipart.AddFormItem(new { id = 1, name = "Furion" }, "obj"); multipart.AddStream(stream, "stream"); multipart.AddByteArray(bytes, "bytes"); multipart.Add(stringContent, "content"); multipart.AddFile(Multipart.CreateFromPath("路径")); }); // 添加文件内容 HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "files", contentType: "image/jpeg"); multipart.AddFileFromBase64String("77u/5rWL6K+V5paH5Lu25YaF5a65", "files"); multipart.AddFileFromRemote("https://furion.net/img/furionlogo.png", "files"); }); // 添加对象内容,AsFormItem 为 false 时,对象会被解析并遍历,其属性将作为独立的表单项进行设置 HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddObject(new { id = 1, name = "furion" }); // AsFormItem 为 false 相当于不设置表单名 }); // 设置多部分表单内容的边界 HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.SetBoundary("--------------------"); multipart.AddFormItem(1, "id"); }); // 添加复杂表单内容 HttpRequestBuilder.Post("https://furion.net") .SetMultipartContent(multipart => { multipart.AddObject(new FormClass { Id = 1, Name = "furion", File = MultipartFile.CreateFromPath("文件路径") }); }); ``` 对比上述两种发送多部分表单内容的方法,`HTTP` 声明式请求方式的代码结构更加条理分明,更易于进行组织、维护和复用。 > **`MultipartAttribute` 和 `MultipartFormAttribute` 特性作用范围** - `MultipartAttribute` 特性仅适用于参数。 - `MultipartFormAttribute` 特性仅适用于方法。 > **冻结参数类型说明** 在系统中,`Action`、`Action`、`HttpCompletionOption` 以及 `CancellationToken` 被视为冻结参数类型,它们专门服务于特定的操作执行。因此,`MultipartAttribute` 特性作用于这些参数类型时将被忽略。 `MultipartAttribute` 包含以下构造函数和属性: - **构造函数**: - `new()`:作用于参数,将参数作为多部分表单项内容。 - `new(name)`:作用于参数,将参数作为多部分表单项内容,支持设置表单名。 - **属性**: - `Name`:表单名称(`string` 类型)。 - `FileName`:文件的名称(`string` 类型)。 - `ContentType`:内容类型(`string` 类型)。 - `ContentEncoding`:内容编码(`string` 类型)。 - `AsFileFrom`:表示将字符串作为多部分表单文件的来源(`FileSourceType` 类型),用于设置多部分表单文件内容,仅当参数为字符串类型时有效。`FileSourceType` 枚举包含以下选项: - `None`(默认值):不用作为文件的来源。 - `Path`:作为本地文件路径。 - `Base64String`:作为 `Base64` 字符串文件。 - `Remote`:作为互联网文件地址。 - `AsFormItem`:表示是否作为表单的一项(`bool` 类型),默认值为 `true`(作为),仅当参数为对象类型时有效。为 `false`(不作为) 时,对象会被解析并遍历,其属性将作为独立的表单项进行设置。 `MultipartFormAttribute` 包含以下构造函数和属性: - **构造函数**: - `new()`:作用于方法,配置多部分表单内容属性。 - `new(boundary)`:作用于方法,配置多部分表单内容属性,支持设置多部分表单内容的边界。 - **属性**: - `Boundary`:多部分表单内容的边界(`string` 类型)。默认值为:`$"----{DateTime.Now.Ticks:x}"`。 - `OmitContentType`:是否移除默认的多部分内容 `Content-Type`(`bool` 类型)。默认值为 `true`。 - `NamingPolicy`:表单名称命名策略(转换器)(`FormNamingPolicy` 类型)。默认值为 `FormNamingPolicy.None`。 --- # 5.18 禁用 HTTP 缓存 > 原文链接:https://http.furion.net/docs/declarative/disabling-http-caching/ 在发送 `HTTP GET` 请求时,服务器可能会缓存该请求的结果以提高性能。为了取消其缓存行为,可以添加 `DisableCacheAttribute` 特性。 `HTTP` 声明式请求通过 `DisableCacheAttribute` 特性来禁用 `HTTP` 缓存。相应的 `HTTP` 声明式提取器实现为 [`DisableCacheDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/DisableCacheDeclarativeExtractor.cs) 类型,该类型负责解析 `DisableCacheAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的禁用 `HTTP` 缓存配置。 ```cs showLineNumbers {2,6,10} // 在接口定义上应用,影响所有方法 [DisableCache] public interface IHttpService : IHttpDeclarative { // 在方法上应用 [DisableCache] [Get("https://furion.net/")] Task GetStringAsync(); [DisableCache(false)] // 启用缓存(默认) [Get("https://furion.net/")] Task GetStringAsync(); } ``` 在添加该特性之后,`HTTP` 请求将在发送前自动附带以下请求标头,以确保缓存控制: ```bash showLineNumbers Cache-Control: must-revalidate, no-cache, no-store Pragma: no-cache If-None-Match: "" ``` > **`DisableCacheAttribute` 特性作用范围** `DisableCacheAttribute` 特性适用于方法或接口。 `DisableCacheAttribute` 包含以下构造函数和属性: - **构造函数**: - `new()`:作用于方法或接口,禁用 `HTTP` 缓存。 - `new(disabled)`:作用于方法或接口,设置是否禁用 `HTTP` 缓存。 - **属性**: - `Disabled`:是否禁用(`bool` 类型),默认值为 `true`(禁用)。 --- # 5.19 确保请求成功 > 原文链接:https://http.furion.net/docs/declarative/ensuring-request-success/ 添加 `EnsureSuccessStatusCodeAttribute` 特性后,当 `HTTP` 响应的状态码不在 `200-299` 范围内时(即 `IsSuccessStatusCode` 属性为 `false`),将自动抛出异常。 `HTTP` 声明式请求通过 `EnsureSuccessStatusCodeAttribute` 特性来确保请求成功。相应的 `HTTP` 声明式提取器实现为 [`EnsureSuccessStatusCodeDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/EnsureSuccessStatusCodeDeclarativeExtractor.cs) 类型,该类型负责解析 `EnsureSuccessStatusCodeAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的确保请求成功配置。 ```cs showLineNumbers {2,6,10} // 在接口定义上应用,影响所有方法 [EnsureSuccessStatusCode] public interface IHttpService : IHttpDeclarative { // 在方法上应用 [EnsureSuccessStatusCode] [Get("https://furion.net/")] Task GetStringAsync(); [EnsureSuccessStatusCode(false)] // 关闭验证 [Get("https://furion.net/")] Task GetStringAsync(); } ``` > **`EnsureSuccessStatusCodeAttribute` 特性作用范围** `EnsureSuccessStatusCodeAttribute` 特性适用于方法或接口。 `EnsureSuccessStatusCodeAttribute` 包含以下构造函数和属性: - **构造函数**: - `new()`:作用于方法或接口,确保请求成功。 - `new(enabled)`:作用于方法或接口,设置是否确保请求成功。 - **属性**: - `Enabled`:是否启用(`bool` 类型),默认值为 `true`(启用)。 --- # 5.20 模拟浏览器环境(爬虫检测) > 原文链接:https://http.furion.net/docs/declarative/simulating-a-browser-environment-crawler-detection/ 在开发爬虫程序时,目标网站可能会根据用户代理(`User-Agent`)或其他因素提供不同的页面版本,如 `PC` 端和移动端。此外,一些网站还具备反爬虫机制,能够识别并阻止爬虫程序的访问。为应对这些问题,我们可以配置请求标头以模拟真实的浏览器环境进行请求。 `HTTP` 声明式请求通过 `SimulateBrowserAttribute` 特性来模拟浏览器环境。相应的 `HTTP` 声明式提取器实现为 [`SimulateBrowserDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/SimulateBrowserDeclarativeExtractor.cs) 类型,该类型负责解析 `SimulateBrowserAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的模拟浏览器环境配置。 ```cs showLineNumbers {2,9} // 在接口定义上应用,影响所有方法 [SimulateBrowser] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync(); // 在方法上应用 [SimulateBrowser(IsMobile = true)] // 模拟移动端浏览器环境 [Get("https://furion.net/")] Task GetStringAsync(); } ``` 在添加该特性之后,`HTTP` 请求将在发送前自动附带以下请求标头,以确保服务器能够准确识别并处理请求: ```bash showLineNumbers {2,5} # PC 浏览器代理 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0 # 移动端浏览器代理 Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Mobile Safari/537.36 Edg/142.0.0.0 ``` > **`SimulateBrowserAttribute` 特性作用范围** `SimulateBrowserAttribute` 特性适用于方法或接口。 `SimulateBrowserAttribute` 包含以下构造函数和属性: - **构造函数**: - `new()`:作用于方法或接口,启用模拟浏览器环境。 - **属性**: - `IsMobile`:是否为移动端(`bool` 类型),默认值为 `false`(即桌面端)。 --- # 5.21 启用请求分析工具 > 原文链接:https://http.furion.net/docs/declarative/enabling-the-request-profiler/ 在现代化的浏览器中,通常内置了开发者工具,这些工具能够捕获并直观展示用户访问网站时的所有请求与响应数据。类似地,我们也为 `HTTP` 远程请求模块提供了一套分析工具。 `HTTP` 声明式请求通过 `ProfilerAttribute` 特性来启用请求分析工具。相应的 `HTTP` 声明式提取器实现为 [`ProfilerDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/ProfilerDeclarativeExtractor.cs) 类型,该类型负责解析 `ProfilerAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的启用请求分析工具配置。 ```cs showLineNumbers {2,6,10} // 在接口定义上应用,影响所有方法 [Profiler] public interface IHttpService : IHttpDeclarative { // 在方法上应用 [Profiler] [Get("https://furion.net/")] Task GetStringAsync(); [Profiler(false)] // 禁用请求分析工具 [Get("https://furion.net/")] Task GetStringAsync(); } ``` 启用后,当执行 `HTTP` 远程请求时,控制台将输出如下详细信息: ```bash showLineNumbers {8} General: Request URL: https://furion.net/ Request Method: GET Status Code: 200 OK HTTP Version: 1.1 HTTP Content: Content Type: Declarative: System.Threading.Tasks.Task GetStringAsync() | HttpAgent.Samples.IHttpService HttpClient Name: Request Duration (ms): 24.00 Response Headers: Server: nginx/1.22.1 Date: Sun, 24 Nov 2024 16:48:50 GMT Connection: keep-alive Vary: Accept-Encoding ETag: "67426a3f-f366" Cache-Control: max-age=315360000 Accept-Ranges: bytes Content-Type: text/html Content-Length: 62310 Last-Modified: Sat, 23 Nov 2024 23:50:23 GMT Expires: Thu, 31 Dec 2037 23:55:55 GMT ``` > **关于 `Blazor WebAssembly` 项目的说明** 在 `Blazor WebAssembly` 应用中,请求分析工具的内容将在客户端(即浏览器)的开发者工具控制台中显示。请确保在开发过程中检查此控制台以获取相关分析信息。 此外,除了为单个请求启用分析工具,还可以全局注册以在 `HttpClient` 中启用: ```cs showLineNumbers {3,7,10,13-14,17-18,21-22} // 为默认客户端启用 services.AddHttpClient(string.Empty) .AddProfilerDelegatingHandler(); // 还可以提供条件禁用,例如生产环境中禁用 services.AddHttpClient(string.Empty) .AddProfilerDelegatingHandler(disableIn: () => builder.Environment.EnvironmentName == "Production"); services.AddHttpClient(string.Empty) .AddProfilerDelegatingHandler(disableInProduction: true); // 为特定客户端启用 //services.AddHttpClient("weixin") // .AddProfilerDelegatingHandler(); // 还可以一键为所有客户端配置启用 services.ConfigureHttpClientDefaults(clientBuilder => clientBuilder.AddProfilerDelegatingHandler()); // 或使用 IHttpRemoteBuilder 扩展方法进行一键配置 services.AddHttpRemote() .ConfigureHttpClientDefaults(clientBuilder => clientBuilder.AddProfilerDelegatingHandler()); ``` 通过启用请求分析工具,开发者能够更直观、便捷地观察和调试 `HTTP` 请求,从而提升开发效率与调试准确性。 > **`ProfilerAttribute` 特性作用范围** `ProfilerAttribute` 特性适用于方法或接口。 `ProfilerAttribute` 包含以下构造函数和属性: - **构造函数**: - `new()`:作用于方法或接口,启用请求分析工具。 - `new(enabled)`:作用于方法或接口,设置是否启用请求分析工具。 - **属性**: - `Enabled`:是否启用(`bool` 类型),默认值为 `true`(启用)。 > **生产环境禁用** 为了确保生产环境的最佳性能和安全性,建议在生产环境中**禁用**请求分析工具。 此外,打印请求内容时可能会导致 `Stream` 对象被重复读取或变得不可读,因为流会被提前读取到内存中,其 `Position` 随之移动到尾部。 **补充说明:** 请求分析工具默认仅展示请求或响应内容中最多 `5KB` 的内容数据。 --- # 5.22 设置客户端偏好的语言和区域 > 原文链接:https://http.furion.net/docs/declarative/setting-the-clients-preferred-language-and-region/ 全球化是互联网应用产品的发展趋势,因此,面向全球的应用产品应具备国际化功能。发送 `HTTP` 请求时,可通过添加 `AcceptLanguageAttribute` 特性来指定客户端偏好的自然语言和区域。 `HTTP` 声明式请求通过 `AcceptLanguageAttribute` 特性来设置客户端偏好的语言和区域。相应的 `HTTP` 声明式提取器实现为 [`AcceptLanguageDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/AcceptLanguageDeclarativeExtractor.cs) 类型,该类型负责解析 `AcceptLanguageAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的客户端偏好的语言和区域配置。 ```cs showLineNumbers {2,6,10} // 在接口定义上应用,影响所有方法 [AcceptLanguage("en-US")] public interface IHttpService : IHttpDeclarative { // 在方法上应用 [AcceptLanguage("zh-CN,en;q=0.5")] [Get("https://furion.net/")] Task GetStringAsync(); [AcceptLanguage("fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5")] [Get("https://furion.net/")] Task GetStringAsync(); } ``` > **`AcceptLanguageAttribute` 特性作用范围** `AcceptLanguageAttribute` 特性适用于方法或接口。 `AcceptLanguageAttribute` 包含以下构造函数和属性: - **构造函数**: - `new(language)`:作用于方法或接口,配置客户端偏好的语言和区域。 - **属性**: - `Language`:客户端偏好的语言和区域(`string` 类型)。 --- # 5.23 设置 HttpRequestMessage 属性 > 原文链接:https://http.furion.net/docs/declarative/setting-httprequestmessage-properties/ 在特定场景下,我们可能需要为 `HttpRequestMessage` 请求添加额外的属性,而非通过请求标头。 `HTTP` 声明式请求通过 `PropertyAttribute` 特性来设置 `HttpRequestMessage` 请求属性。相应的 `HTTP` 声明式提取器实现为 [`PropertyDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/PropertyDeclarativeExtractor.cs) 类型,该类型负责解析 `PropertyAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的 `HttpRequestMessage` 请求属性配置。 利用 `PropertyAttribute` 特性,可以便捷地在接口、方法或参数上添加 `HttpRequestMessage` 请求属性。 ```cs showLineNumbers {2-4,8-9,14,16,20,24,28,32} // 在接口定义上应用,影响所有方法 [Property("property1", "value1")] [Property("property2", "value2")] [Property("property0")] // 值为 null public interface IHttpService : IHttpDeclarative { // 在方法上应用 [Property("property3", "value3")] [Property("property4", "value4")] [Get("https://furion.net/")] Task GetStringAsync(); // 在参数上应用,支持 AliasAs 属性指定别名,且可多重指定 [Property("property3", "value3")] [Get("https://furion.net/")] Task GetStringAsync([Property] string property4, [Property][Property(AliasAs = "property5")] int lastProperty); // 在参数上可通过 Value 属性设定默认值,同样可为 age 参数设定,例如 int? age = 30 [Get("https://furion.net/")] Task GetStringAsync([Property(Value = 30)] int? age); // 支持 [AliasAs] 定义别名 [Get("https://furion.net/")] Task GetStringAsync([Property][AliasAs("property5")] int lastProperty); // 添加对象内容,AsItem 为 false 时,对象会被解析并遍历,其属性将作为独立的 HttpRequestMessage 请求属性项进行设置 [Get("https://furion.net/")] Task GetStringAsync([Property(AsItem = false)] object obj); // 冻结参数类型将被忽略 [Get("https://furion.net/")] Task GetStringAsync([Property] CancellationToken cancellationToken); } ``` 这些属性会被添加到 `HttpRequestMessage` 对象的 `Options` 属性中([参考文档](https://learn.microsoft.com/zh-cn/dotnet/api/system.net.http.httprequestmessage.options))。要获取这些值,可以这样做: ```cs showLineNumbers httpRequestMessage.Options.TryGetValue(new HttpRequestOptionsKey("key1"), out var value); ``` 若属性键出现重复,则后设置的键值会覆盖先前的设置。 > **小提示** 此功能常被集成在自定义的 `DelegatingHandler` 和 `IHttpRequestEventHandler` 组件中。 `PropertyAttribute` 包含以下构造函数和属性: - **构造函数**: - `new()`:作用于参数时有效,表示添加 `HttpRequestMessage` 请求属性,默认键为参数名。 - `new(name)`:作用于方法或接口时,则表示添加 `HttpRequestMessage` 请求属性操作,值为 `null`;作用于参数时,表示添加 `HttpRequestMessage` 请求属性,键为参数 `name` 的值。 - `new(name, value)`:作用于接口、方法或参数,表示添加 `HttpRequestMessage` 请求属性,键为参数 `name` 的值,优先级低于 `AliasAs` 属性。 - **属性**: - `Name`:`HttpRequestMessage` 请求属性键(`string` 类型),优先级低于 `AliasAs` 属性。 - `Value`:`HttpRequestMessage` 请求属性的值(`object` 类型),当特性作用于参数时,表示默认值。 - `AliasAs`:`HttpRequestMessage` 请求属性键别名(`string` 类型),优先级高于 `Name` 属性。 - `AsItem`:表示是否作为 `HttpRequestMessage` 请求属性的一项(`bool` 类型),默认值为 `true`(作为),仅当参数为对象类型时有效。为 `false`(不作为) 时,对象会被解析并遍历,其属性将作为独立的 `HttpRequestMessage` 请求属性项进行设置。 > **冻结参数类型说明** 在系统中,`Action`、`Action`、`HttpCompletionOption` 以及 `CancellationToken` 被视为冻结参数类型,它们专门服务于特定的操作执行。因此,`PropertyAttribute` 特性作用于这些参数类型时将被忽略。 > **小知识** `C#` 支持特性合并,使代码更简洁: ```cs showLineNumbers {1,4} [Property("property1", "value1"), Property("property2", "value2")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/"), Property("property3", "value3"), Property("property4", "value4")] Task GetStringAsync(); } ``` --- # 5.24 启用标准请求标头 > 原文链接:https://http.furion.net/docs/declarative/enabling-standard-request-headers/ 为了提升应用通过 `HTTP` 客户端发送网络请求的兼容性并避免被 `WAF`(`Web` 应用防火墙)拦截,框架提供了一键式配置方法,方便快速统一设置标准的请求头部。 `HTTP` 声明式请求通过 `StandardRequestHeadersAttribute` 特性来启用标准请求标头。相应的 `HTTP` 声明式提取器实现为 [`StandardRequestHeadersDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/StandardRequestHeadersDeclarativeExtractor.cs) 类型,该类型负责解析 `StandardRequestHeadersAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的配置。 ```cs showLineNumbers {2,6,10} // 在接口定义上应用,影响所有方法 [StandardRequestHeaders] public interface IHttpService : IHttpDeclarative { // 在方法上应用 [StandardRequestHeaders] [Get("https://furion.net/")] Task GetStringAsync(); [StandardRequestHeaders(false)] // 关闭标准请求标头 [Get("https://furion.net/")] Task GetStringAsync(); } ``` 此外,除了为单个请求启用标准标头配置,还可以全局注册以在 `HttpClient` 中启用: ```cs showLineNumbers {4} // 为默认客户端启用 services.AddHttpClient(string.Empty, client => { client.UseStandardRequestHeaders(); }); services.AddHttpRemote(); ``` 启用标准请求标头后,请求将自动添加以下头部: - **`Accept`**:`application/json`、`text/plain;q=0.9`、`*/*;q=0.8`(明确的媒体类型优先级,避免被 `WAF` 拦截) - **`Connection`**:启用持久连接(`Keep-Alive`),减少 `TCP` 连接建立和关闭的开销 > **`StandardRequestHeadersAttribute` 特性作用范围** `StandardRequestHeadersAttribute` 特性适用于方法或接口。 `StandardRequestHeadersAttribute` 包含以下构造函数和属性: - **构造函数**: - `new()`:作用于方法或接口,启用标准请求标头。 - `new(enabled)`:作用于方法或接口,设置是否启用标准请求标头。 - **属性**: - `Enabled`:是否启用(`bool` 类型),默认值为 `true`(启用)。 --- # 5.25 设置自动 Host 标头 > 原文链接:https://http.furion.net/docs/declarative/setting-the-automatic-host-header/ `Host` 标头是 `HTTP/1.1` 协议中的一个必需标头。`Host` 标头用于指定请求的目标服务器的主机名和端口号,确保服务器能正确区分同一 `IP` 地址上的不同域名并进行相应处理。框架提供了简便的方法进行设置: `HTTP` 声明式请求通过 `AutoSetHostHeaderAttribute` 特性来设置自动 `Host` 标头。相应的 `HTTP` 声明式提取器实现为 [`AutoSetHostHeaderDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/AutoSetHostHeaderDeclarativeExtractor.cs) 类型,该类型负责解析 `AutoSetHostHeaderAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的自动 `Host` 标头配置。 ```cs showLineNumbers {2,6,10} // 在接口定义上应用,影响所有方法 [AutoSetHostHeader] // 启用 public interface IHttpService : IHttpDeclarative { // 在方法上应用 [AutoSetHostHeader] [Get("https://furion.net/")] Task GetStringAsync(); [AutoSetHostHeader(false)] // 关闭自动 Host 标头 [Get("https://furion.net/")] Task GetStringAsync(); } ``` 启用后,发送 `HTTP` 远程请求时会自动添加 `Host: furion.net` 标头。 > **小提示** 当对接旧程序提供的 `API` 接口时,建议启用该配置以提升兼容性。 > **`HttpClient` 自动重定向导致的 `Host` 问题** 在发送 `HTTP` 远程请求时,如果目标服务器返回重定向响应(如 `301 Moved Permanently` 或 `302 Found`),框架默认会自动跟随重定向。然而,当启用自动 `Host` 标头时,可能会遇到无法更新 `Host` 标头的问题。此时,可以关闭 `AllowAutoRedirect` 选项,使框架能够正确处理重定向: ```cs showLineNumbers {3,5} // 配置默认客户端 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { AllowAutoRedirect = false }); ``` 另外,如需设置框架内置重定向行为的最大重定向次数,可以使用以下方式: ```cs showLineNumbers {2,4} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { MaximumAutomaticRedirections = 20; }); ``` 这样,可以确保重定向行为符合预期,同时避免 `Host` 标头设置错误的问题。 > **`AutoSetHostHeaderAttribute` 特性作用范围** `AutoSetHostHeaderAttribute` 特性适用于方法或接口。 `AutoSetHostHeaderAttribute` 包含以下构造函数和属性: - **构造函数**: - `new()`:作用于方法或接口,设置自动 `Host` 标头。 - `new(enabled)`:作用于方法或接口,设置是否设置自动 `Host` 标头。 - **属性**: - `Enabled`:是否启用(`bool` 类型),默认值为 `true`(启用)。 --- # 5.26 设置请求基地址 > 原文链接:https://http.furion.net/docs/declarative/setting-the-request-base-address/ 当需要对接多个第三方 `API` 时,我们通常会全局注册并配置多个 `HttpClient` 实例的 `BaseAddress`。例如: ```cs showLineNumbers {4,10} // 配置默认客户端的基地址 services.AddHttpClient(string.Empty, client => { client.BaseAddress = new Uri("https://furion.net/"); }); // 配置GitHub客户端的基地址 services.AddHttpClient("github", client => { client.BaseAddress = new Uri("https://github.com/"); }); ``` 随后可通过 `[HttpClientName(客户端名称)]` 特性指定要使用的客户端。 除了全局配置,框架还支持在声明式请求中局部设置基地址,允许在构建请求时动态指定。 `HTTP` 声明式请求通过 `BaseAddressAttribute` 特性来设置请求基地址。相应的 `HTTP` 声明式提取器实现为 [`BaseAddressDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/BaseAddressDeclarativeExtractor.cs) 类型,该类型负责解析 `BaseAddressAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的请求基地址配置。 ```cs showLineNumbers {2,6,9,14} // 在接口定义上应用,影响所有方法 [BaseAddress("https://furion.net")] public interface IHttpService : IHttpDeclarative { [Get("/api/test")] Task GetStringAsync(); // 在方法上应用 [BaseAddress("https://baiqian.com")] [Get("/api/test/2")] Task GetStringAsync(); // 可充当前缀使用 [BaseAddress("/furion")] [Get("/api/test/2")] Task GetStringAsync(); } ``` 启用后,发送 `HTTP` 远程请求时会自动设置请求基地址。 > **特别说明** 请确保设置的请求基地址是绝对路径,即以 `http://` 或 `https://` 开头。 **处理逻辑说明**: - 若请求地址为绝对地址,则直接使用该地址发送请求。 - 若请求地址为相对地址: - 未设置局部 `BaseAddress` 时,将其与全局 `HttpClient` 实例的 `BaseAddress` 拼接,作为最终请求地址。 - 已设置局部 `BaseAddress` 时: - 若局部 `BaseAddress` 为相对地址,则先将局部 `BaseAddress` 拼接到请求地址前,再与全局 `BaseAddress` 拼接。 - 若局部 `BaseAddress` 为绝对地址,则直接将该绝对地址与请求地址拼接,作为最终请求地址(此时忽略全局 `BaseAddress`)。 > **`BaseAddressAttribute` 特性作用范围** `BaseAddressAttribute` 特性适用于方法或接口。 `BaseAddressAttribute` 包含以下构造函数和属性: - **构造函数**: - `new(baseAddress)`:作用于方法或接口,设置请求基地址。 - **属性**: - `BaseAddress`:请求基地址(`string` 类型)。 > **配置参数支持** 请求基地址支持配置参数,用于读取配置信息进行替换操作。配置参数使用 `[[key]]` 语法。 --- # 5.27 设置来源地址(防盗链) > 原文链接:https://http.furion.net/docs/declarative/setting-the-referrer-address-hotlink-protection/ 当访问某些第三方服务器时,服务器可能会验证请求头中的 `Referer` 来源地址。例如在下载图片时,可能因触发防盗链机制导致获取的图片不符合预期。此时,可通过设置 `Referer` 请求头,模拟来源页面以绕过防盗链检测。 `HTTP` 声明式请求通过 `RefererAttribute` 特性来设置请求来源地址。相应的 `HTTP` 声明式提取器实现为 [`RefererDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/RefererDeclarativeExtractor.cs) 类型,该类型负责解析 `RefererAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的请求来源地址配置。 ```cs showLineNumbers {2,6,9} // 在接口定义上应用,影响所有方法 [Referer("https://furion.net")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/logo.png")] Task GetStringAsync(); // 在方法上应用 [Referer("https://baiqian.com")] [Get("https://furion.net/logo2.png")] Task GetStringAsync(); } ``` 启用后,发送 `HTTP` 远程请求时会自动设置请求来源地址。 为简化配置,框架提供了内置模板字符串 `"{BASE_ADDRESS}"`,可自动提取请求地址的基地址作为 `Referer`: ```cs showLineNumbers {1} [Referer("{BASE_ADDRESS}")] // 发送时自动替换 {BASE_ADDRESS} 为 https://furion.net/ public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/logo.png")] Task GetStringAsync(); } ``` > **`RefererAttribute` 特性作用范围** `RefererAttribute` 特性适用于方法或接口。 `RefererAttribute` 包含以下构造函数和属性: - **构造函数**: - `new(referer)`:作用于方法或接口,设置请求来源地址。 - **属性**: - `Referer`:请求来源地址(`string` 类型)。 --- # 5.28 配置 HTTP 版本 > 原文链接:https://http.furion.net/docs/declarative/configuring-the-http-version/ 在发起 `HTTP` 远程请求时,默认采用的 `HTTP` 协议版本为 `1.1`。不过,在访问部分第三方服务器时,这些服务器可能会对 `HTTP` 版本进行校验(例如,要求使用 `2.0` 版本)。 `HTTP` 声明式请求通过 `HttpVersionAttribute` 特性来设置请求的 `HTTP` 版本。相应的 `HTTP` 声明式提取器实现为 [`HttpVersionDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpVersionDeclarativeExtractor.cs) 类型,该类型负责解析 `HttpVersionAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的 `HTTP` 版本配置。 ```cs showLineNumbers {2,6,9} // 在接口定义上应用,影响所有方法 [HttpVersion("1.2")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/logo.png")] Task GetStringAsync(); // 在方法上应用 [HttpVersion("2.0")] [Get("https://furion.net/logo2.png")] Task GetStringAsync(); } ``` 启用后,发送 `HTTP` 远程请求时会自动设置 `HTTP` 版本。 除可通过 `[HttpVersion]` 特性进行配置外,系统还支持全局设置方式,具体示例如下: ```cs showLineNumbers {2,4,8,10} // 配置默认客户端 services.AddHttpClient(string.Empty, client => { client.DefaultRequestVersion = HttpVersion.Version10; }); // 配置特定客户端 services.AddHttpClient("weixin", client => { client.DefaultRequestVersion = HttpVersion.Version10; }); ``` > **`HttpVersionAttribute` 特性作用范围** `HttpVersionAttribute` 特性适用于方法或接口。 `HttpVersionAttribute` 包含以下构造函数和属性: - **构造函数**: - `new(version)`:作用于方法或接口,设置 `HTTP` 版本。 - **属性**: - `Version`:`HTTP` 版本(`string` 类型)。 --- # 5.29 异常抑制机制(静默处理) > 原文链接:https://http.furion.net/docs/declarative/exception-suppression-mechanism-silent-handling/ 在发起 `HTTP` 远程请求时,可能会遇到以下异常情况: - 目标主机不可达 - 请求被取消 - 请求超时 - 其他网络异常 默认情况下,这些异常会中断程序执行。虽然开发者通常使用 `try/catch` 进行异常处理,但在某些场景下,我们更希望异常发生时静默返回 `null` 而不中断流程。为此,框架提供了灵活的异常抑制功能。 `HTTP` 声明式请求通过 `SuppressExceptionsAttribute` 特性来抑制异常。相应的 `HTTP` 声明式提取器实现为 [`SuppressExceptionsDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/SuppressExceptionsDeclarativeExtractor.cs) 类型,该类型负责解析 `SuppressExceptionsAttribute` 特性并构建 `HttpRequestBuilder` 实例所需异常抑制配置。 ```cs showLineNumbers {2,6,9,13} // 在接口定义上应用,影响所有方法 [SuppressExceptions] // 抑制所有异常 public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/logo.png")] Task GetStringAsync(); // 在方法上应用 [SuppressExceptions(typeof(TimeoutException), typeof(TaskCanceledException))] // 抑制超时和取消异常 [Get("https://furion.net/logo2.png")] Task GetStringAsync(); [SuppressExceptions(false)] // 禁用抑制异常(恢复缺省配置) [Get("https://furion.net/logo2.png")] Task GetStringAsync(); } ``` 启用后,发送 `HTTP` 远程请求时会自动设置异常抑制机制。 > **`SuppressExceptionsAttribute` 特性作用范围** `SuppressExceptionsAttribute` 特性适用于方法或接口。 `SuppressExceptionsAttribute` 包含以下构造函数和属性: - **构造函数**: - `new()`:作用于方法或接口,抑制所有异常。 - `new(enable)`:作用于方法或接口,是否启用异常抑制机制。 - `new(types)`:作用于方法或接口,抑制指定类型异常。 - **属性**: - `Types`:异常抑制类型集合(`type[]` 类型,数组中的每个元素必须是 `System.Exception` 类型或其派生类型)。 > **注意事项** 当启用异常抑制功能时,请注意以下事项: 1. **覆盖规则** 多次调用 `SuppressExceptions()` 或相关配置时,**仅最后一次调用生效**。 2. **状态码检查与异常抑制的优先级** 即使已配置 `EnsureSuccessStatusCode()`,被抑制的异常仍会返回 `null`,不会触发状态码检查逻辑。 3. **异常抑制的优先级** 异常抑制功能的优先级高于状态码检查。如果同时启用状态码检查和异常抑制,异常抑制会优先生效。 4. **请求拦截器依旧可用** 若通过 `SetOnRequestFailed(ex, res)` 或其他请求处理机制捕获异常,即使异常被抑制,拦截器或回调方法仍会被调用。 5. **异常类型选择建议** 应根据具体业务场景谨慎选择需要抑制的异常类型,避免因过度抑制异常而掩盖潜在问题。 6. **自动输出抑制日志** 当异常被成功抑制时,框架会自动输出一条 `Warning` 级别日志(例如 `"An exception occurred but was suppressed by SuppressExceptionPipelineHandler."`),便于排查问题。 --- # 5.30 启用参数验证 > 原文链接:https://http.furion.net/docs/declarative/validation/ 在调用 `HTTP` 声明式接口方法时,支持验证传递的参数数据的合法性。 `HTTP` 声明式请求通过派生自 `ValidationAttribute` 的特性来启用参数验证。相应的 `HTTP` 声明式提取器实现为 [`ValidationDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/ValidationDeclarativeExtractor.cs) 类型,该类型负责解析派生自 `ValidationAttribute` 的特性并对传递的参数数据进行合法性验证。 - **验证单个值和对象数据** ```cs showLineNumbers {9,14-17,21,29-31} public interface IHttpService : IHttpDeclarative { // 无需验证参数合法性 [Get("https://furion.net/")] Task GetStringAsync(string str, object obj); // 验证参数合法性,支持验证对象模型内部验证特性 [Get("https://furion.net/")] Task GetStringAsync([Length(10, 20)] string str, [Required] ValidationModel obj); // 支持为参数添加多个验证规则 [Get("https://furion.net/")] Task GetStringAsync( [Required] [MinLength(2)] [MaxLength(5)] string str, [Range(0, 10)] int age); // 冻结参数类型将被忽略 [Get("https://furion.net/")] Task GetStringAsync([Required] CancellationToken cancellationToken); } // 对象属性验证 public class ValidationModel { public int Id { get; set; } [Required] [MinLength(3)] public string? Name { get; set; } } ``` - **验证实现 `IValidatableObject` 接口的对象数据** ```cs showLineNumbers {4,8,16-22} public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync(ValidationObject obj); } // 实现 IValidatableObject 进行复杂验证 public class ValidationObject : IValidatableObject { public int Id { get; set; } [Required] [MinLength(3)] public string? Name { get; set; } public IEnumerable Validate(ValidationContext validationContext) { if (Id < 0) { yield return new ValidationResult("Id must be greater than or equal to 0.", [nameof(Id)]); } } } ``` - **验证自定义 `ValidationAttribute`** ```cs showLineNumbers {4,8,15-23} public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync([StringEqual("Furion")] string str); } // 自定义验证特性 public class StringEqualAttribute : ValidationAttribute { public StringEqualAttribute(string value) => Value = value; public string Value { get; } /// protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { if (value?.ToString() != Value) { return new ValidationResult($"Value is not equal to {Value}."); } return ValidationResult.Success; } } ``` 派生自 `ValidationAttribute` 的标记和实现 `IValidatableObject` 接口的参数通过底层 `Validator.ValidateValue` 和 `Validator.ValidateObject` 方法进行数据验证,支持任意非冻结类型的参数。 > **派生自 `ValidationAttribute` 的特性作用范围** 派生自 `ValidationAttribute` 的特性仅适用于参数。 > **冻结参数类型说明** 在系统中,`Action`、`Action`、`HttpCompletionOption` 以及 `CancellationToken` 被视为冻结参数类型,它们专门服务于特定的操作执行。因此,派生自 `ValidationAttribute` 的特性作用于这些参数类型时将被忽略。 > **小知识** `C#` 支持特性合并,使代码更简洁: ```cs showLineNumbers {1,4} public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync([Required, MinLength(2), MaxLength(5)] string str, [Range(0, 10)] int age) } ``` - **禁用参数验证** 框架提供了 `SuppressValidationAttribute` 特性,可用于禁用 `HTTP` 声明式请求的参数验证。将该特性应用于接口时,会禁用该接口下所有方法的参数验证;应用于具体方法时,则仅禁用该方法的参数验证。 ```cs showLineNumbers {3} public interface IHttpService : IHttpDeclarative { [SuppressValidation] // 仅禁用 GetStringAsync 方法的参数验证 [Get("https://furion.net/")] Task GetStringAsync([Length(10, 20)] string str, [Required] ValidationModel obj); } ``` ```cs showLineNumbers {1,8} [SuppressValidation] // 禁用接口定义的所有方法参数验证 public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync([Length(10, 20)] string str, [Required] ValidationModel obj); [Get("https://furion.net/")] [SuppressValidation(false)] // 启用 GetStringAsync 方法的参数验证 Task GetStringAsync([Length(10, 20)] string str, [Required] ValidationModel obj); } ``` --- # 5.31 启用 JSON 响应反序列化包装器 > 原文链接:https://http.furion.net/docs/declarative/enabling-the-json-response-deserialization-wrapper/ 在与第三方 `API` 进行 `HTTP` 远程通信时,通常会返回统一结构的 `JSON` 响应,例如 `ApiResult` 类型,其中实际数据存放在 `Data` 属性中: ```cs showLineNumbers {1,4} public class ApiResult { public bool Success { get; set; } public T? Data { get; set; } // 实际返回数据 } ``` `HTTP` 声明式请求通过 `JsonResponseWrapperAttribute` 特性来启用 `JSON` 响应反序列化包装器。相应的 `HTTP` 声明式提取器实现为 [`JsonResponseWrapperDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/JsonResponseWrapperDeclarativeExtractor.cs) 类型,该类型负责解析 `JsonResponseWrapperAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的启用 `JSON` 响应反序列化包装器配置。 在未启用 `JSON` 响应反序列化包装器功能时,每次调用都需要显式指定 `ApiResult` 类型: ```cs showLineNumbers {4,7} public interface IHttpService : IHttpDeclarative { [Get("https://furion.net")] Task> GetStringAsync(); [Get("https://furion.net/")] Task> GetJsonModelAsync(); } ``` ### 启用方式 #### 1. 单次启用 为简化调用流程,可配置 `JSON` 响应反序列化包装器,使其自动提取 `Data` 属性内容: ```cs showLineNumbers {2-3,5} // 配置默认 HTTP 客户端 services.AddHttpClient(string.Empty) .ConfigureOptions(options => { options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data)); }); ``` 配置完成后,通过 `[JsonResponseWrapper]` 启用该功能,之后只需指定目标数据类型,无需重复声明 `ApiResult`: ```cs showLineNumbers {2,7,10,12,14,16} // 在接口定义上应用,影响所有方法 [JsonResponseWrapper] public interface IHttpService : IHttpDeclarative { // 默认自动应用 [Get("https://furion.net/")] Task GetStringAsync(); // 在方法上应用 [JsonResponseWrapper] // 可显示启用(无需) [Get("https://furion.net/")] Task GetStringAsync(); [JsonResponseWrapper(false)] // 禁用 JSON 响应反序列化包装器,需传入完整的响应类型 [Get("https://furion.net/")] Task> GetJsonModelAsync(); } ``` 框架将在运行时自动创建 `ApiResult` 实例,并返回其 `Data` 属性的值。 #### 2. 全局启用(默认对所有请求生效) 也可全局启用 `JSON` 响应反序列化包装器功能,只需设置 `UseJsonResponseWrapper` 为 `true`: ```cs showLineNumbers {2-3,6} // 配置默认 HTTP 客户端 services.AddHttpClient(string.Empty) .ConfigureOptions(options => { options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data)); options.UseJsonResponseWrapper = true; }); ``` 全局启用后,所有请求默认使用包装功能: ```cs showLineNumbers {1} // [JsonResponseWrapper] // 无需显式设置 [JsonResponseWrapper] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync(); } ``` #### 3. 单次禁用(覆盖全局设置) 若需对特定请求禁用该功能,可设置 `[JsonResponseWrapper(false)]` 特性。 ### 自定义结果处理(`ResultHandler`) 有时除了提取 `Data`,还需对响应做额外校验或转换。可通过 `ResultHandler` 回调实现: ```cs showLineNumbers {7,12,16,19} // 配置默认 HTTP 客户端 services.AddHttpClient(string.Empty) .ConfigureOptions(options => { options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data)) { ResultHandler = context => { if (context.Instance is { } instance) { // 可访问包装类型实例,获取其任意属性 var success = context.GetPropertyValue(nameof(ApiResult<>.Success)); } // 例如确保请求成功 context.ResponseMessage.EnsureSuccessStatusCode(); // 返回最终的目标结果(即 Data 的值) return context.Result; } }; }); ``` 通过 `ResultHandler`,您可以在返回最终数据前执行任何自定义逻辑(如校验、转换或异常处理),使请求处理更加灵活。 `context` 参数的类型为 `JsonResponseWrapperContext`,包含以下属性和方法: - **属性**: - `Instance`:包装类型的具体实例(如 `ApiResult`,`object?` 类型)。 - `Result`:目标结果(即 `Data` 的值,`object?` 类型)。 - `ResponseMessage`:响应消息(`HttpResponseMessage` 类型)。 - **方法**: - `GetPropertyValue(propertyName)`:获取包装类型的具体类型(即 `Instance`) 指定属性值。 > **`JsonResponseWrapperAttribute` 特性作用范围** `JsonResponseWrapperAttribute` 特性适用于方法或接口。 `JsonResponseWrapperAttribute` 包含以下构造函数和属性: - **构造函数**: - `new()`:作用于方法或接口,启用 `JSON` 响应反序列化包装器。 - `new(enabled)`:作用于方法或接口,设置是否启用 `JSON` 响应反序列化包装器。 - **属性**: - `Enabled`:是否启用(`bool` 类型),默认值为 `true`(启用)。 --- # 5.32 响应 JSON 双重序列化处理 > 原文链接:https://http.furion.net/docs/declarative/response-json-double-serialization-handling/ 在与第三方 `API` 进行 `HTTP` 远程通信时,极少数情况下可能会遇到服务器返回的 `JSON` 数据被意外进行了双重序列化(有时也可能是刻意为之)。例如,本应返回 `"{\"id\":1,\"name\":\"furion\"}"`,却因双重序列化变成了 `"\"{\\\"id\\\":10, \\\"name\\\":\\\"furion\\\"}\""`。针对这类情况,框架提供了解包支持。 `HTTP` 声明式请求通过 `JsonResponseStringUnwrapAttribute` 特性来启用 `JSON` 响应内容字符串的解包处理。相应的 `HTTP` 声明式提取器实现为 [`JsonResponseStringUnwrapDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/JsonResponseStringUnwrapDeclarativeExtractor.cs) 类型,该类型负责解析 `JsonResponseStringUnwrapAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的启用 `JSON` 响应内容字符串的解包处理配置。 ```cs showLineNumbers {2,7,10,12,14,16} // 在接口定义上应用,影响所有方法 [JsonResponseStringUnwrap] public interface IHttpService : IHttpDeclarative { // 默认自动应用 [Get("https://furion.net/")] Task GetAsync(); // 在方法上应用 [JsonResponseStringUnwrap] // 可显示启用(无需) [Get("https://furion.net/")] Task GetAsync(); [JsonResponseStringUnwrap(false)] // 禁用 JSON 响应内容字符串的解包处理 [Get("https://furion.net/")] Task> GetAsync(); } ``` 若需对特定请求禁用该功能,可设置 `[JsonResponseStringUnwrap(false)]` 特性。 > **`JsonResponseStringUnwrapAttribute` 特性作用范围** `JsonResponseStringUnwrapAttribute` 特性适用于方法或接口。 `JsonResponseStringUnwrapAttribute` 包含以下构造函数和属性: - **构造函数**: - `new()`:作用于方法或接口,启用 `JSON` 响应内容字符串的解包处理。 - `new(enabled)`:作用于方法或接口,设置是否启用 `JSON` 响应内容字符串的解包处理。 - **属性**: - `Enabled`:是否启用(`bool` 类型),默认值为 `true`(启用)。 --- # 5.33 设置请求处理程序 > 原文链接:https://http.furion.net/docs/declarative/setting-the-request-event-handler/ `IHttpRequestEventHandler` 接口允许您定义 `HTTP` 请求的预处理操作。通过实现该接口,您可以创建自定义的请求处理程序,例如 `CustomRequestEventHandler` 类: ```cs showLineNumbers {1} public class CustomRequestEventHandler : IHttpRequestEventHandler { // 在发送 HTTP 请求之前的操作 public void OnPreSendRequest(HttpRequestMessage httpRequestMessage) {} // 在收到 HTTP 响应之后的操作 public Task OnPostReceiveResponseAsync(HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken) {} // 当发送 HTTP 请求发生异常时的操作 public void OnRequestFailed(Exception exception, HttpResponseMessage? httpResponseMessage = null) {} } ``` 发送 `HTTP` 请求时,可通过添加 `RequestEventHandlerAttribute` 特性来设置请求处理程序。 `HTTP` 声明式请求通过 `RequestEventHandlerAttribute` 特性来设置请求处理程序。相应的 `HTTP` 声明式提取器实现为 [`RequestEventHandlerDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/RequestEventHandlerDeclarativeExtractor.cs) 类型,该类型负责解析 `RequestEventHandlerAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的请求处理程序配置。 ```cs showLineNumbers {2,7,10} // 在接口定义上应用,影响所有方法 [RequestEventHandler(typeof(CustomRequestEventHandler))] public interface IHttpService : IHttpDeclarative { // 自动应用接口声明 [Get("https://furion.net/")] Task GetStringAsync(); // 在方法上应用 [RequestEventHandler(typeof(CustomRequestEventHandler))] [Get("https://furion.net/")] Task GetStringAsync(); } ``` > **`RequestEventHandlerAttribute` 特性作用范围** `RequestEventHandlerAttribute` 特性适用于方法或接口。 `RequestEventHandlerAttribute` 包含以下构造函数和属性: - **构造函数**: - `new(handlerType)`:作用于方法或接口,配置请求处理程序。 - **属性**: - `HandlerType`:请求处理程序(`Type` 类型)。 --- # 5.34 禁用 Access Token 自动管理 > 原文链接:https://http.furion.net/docs/declarative/disabling-automatic-access-token-management/ 框架内置了 `Access Token` 自动管理功能,只需实现 `IHttpAccessTokenProvider` 接口,并在 `GetAsync` 方法中编写获取 `Access Token` 的逻辑即可。通常情况下,获取 `Access Token` 需要借助独立的 `HTTP` 请求,而若在 `GetAsync` 内部直接使用 `IHttpRemoteService` 发送请求,则会因触发自动管理机制而陷入递归死循环。此时,需通过标记 `[SuppressTokenManagement]` 特性显式禁用当前请求的 `Access Token` 自动管理,避免循环调用。 ```cs showLineNumbers {1,6} public class CustomHttpAccessTokenProvider(IHttpBaseService httpBaseService): IHttpAccessTokenProvider { /// public async Task GetAsync(CancellationToken cancellationToken) { var serverToken = await httpBaseService.LoginAsync(new { username = "furion", password = "your-password"}); return new HttpAccessToken(serverToken.Token, serverToken.ExpiresAt) }; } ``` `HTTP` 声明式请求通过 `SuppressTokenManagementAttribute` 特性来禁用框架的 `Access Token` 自动管理。相应的 `HTTP` 声明式提取器实现为 [`SuppressTokenManagementDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/DeclarativeExtractor.cs) 类型,该类型负责解析 `SuppressTokenManagementAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的禁用框架的 `Access Token` 自动管理配置。 ```cs showLineNumbers {4} public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] [SuppressTokenManagement] // 跳过 Token 管理,避免递归调用 Task LoginAsync(object auth); [Get("https://furion.net/")] Task> GetAsync(); // 无需标记 } ``` 若需对特定请求禁用该功能,可设置 `[JsonResponseStringUnwrap(false)]` 特性。 > **`SuppressTokenManagementAttribute` 特性作用范围** `SuppressTokenManagementAttribute` 特性仅适用于方法。 `SuppressTokenManagementAttribute` 包含以下构造函数和属性: 无。 --- # 5.35 移除 URL 地址末尾的 / > 原文链接:https://http.furion.net/docs/declarative/removing-the-trailing--from-the-url-address/ 部分服务端对路径末尾的 `/` 敏感(如 `/api/` 与 `/api`),可能造成 `301` 重定向或路由匹配失败。开启该功能后,框架会在构造最终请求地址时自动去除路径末尾的 `/`。 `HTTP` 声明式请求通过 `RemoveTrailingSlashAttribute` 特性来移除 `URL` 地址末尾的 `/`。相应的 `HTTP` 声明式提取器实现为 [`RemoveTrailingSlashDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/RemoveTrailingSlashDeclarativeExtractor.cs) 类型,该类型负责解析 `RemoveTrailingSlashAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的移除 `URL` 地址末尾的 `/` 配置。 ```cs showLineNumbers {2,6,10} // 在接口定义上应用,影响所有方法 [RemoveTrailingSlash] public interface IHttpService : IHttpDeclarative { // 在方法上应用 [RemoveTrailingSlash] [Get("https://furion.net/")] Task GetStringAsync(); [RemoveTrailingSlash(false)] // 禁用该功能 [Get("https://furion.net/")] Task GetStringAsync(); } ``` 请求地址将变为 `https://furion.net`。 > **`RemoveTrailingSlashAttribute` 特性作用范围** `RemoveTrailingSlashAttribute` 特性适用于方法或接口。 `RemoveTrailingSlashAttribute` 包含以下构造函数和属性: - **构造函数**: - `new()`:作用于方法或接口,移除 `URL` 地址末尾的 `/`。 - `new(enabled)`:作用于方法或接口,设置是否移除 `URL` 地址末尾的 `/`。 - **属性**: - `Enabled`:是否启用(`bool` 类型),默认值为 `true`(启用)。 --- # 5.36 设置请求接口配额键 > 原文链接:https://http.furion.net/docs/declarative/setting-the-request-interface-quota-key/ 为当前请求指定配额键,用于关联在 `HttpClientOptions` 中配置的配额限制规则。 `HTTP` 声明式请求通过 `QuotaKeyAttribute` 特性来设置请求接口配额键。相应的 `HTTP` 声明式提取器实现为 [`QuotaKeyDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/QuotaKeyDeclarativeExtractor.cs) 类型,该类型负责解析 `QuotaKeyAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的请求接口配额键配置。 ```cs showLineNumbers {2,9} // 在接口定义上应用,影响所有方法 [QuotaKey("weixin")] public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync(); // 在方法上应用 [QuotaKey("qq")] [Get("https://furion.net/")] Task GetStringAsync(); } ``` > **配置键 `QuotaKey` 说明** - 如果未指定配额键,或指定的键在 `QuotaLimits` 中不存在,则不会进行配额检查,请求正常发送。 - 配额键可以是任意自定义字符串,推荐使用与接口路径相关的名称,便于识别和管理。 > **`QuotaKeyAttribute` 特性作用范围** `QuotaKeyAttribute` 特性适用于方法或接口。 `QuotaKeyAttribute` 包含以下构造函数和属性: - **构造函数**: - `new(key)`:作用于方法或接口,设置请求接口配额键。 - **属性**: - `Key`:配额键(`string` 类型)。 --- # 5.37 启用 ETag 响应缓存 > 原文链接:https://http.furion.net/docs/declarative/enabling-etag-response-caching/ `ETag`(实体标签)是 `HTTP` 协议中用于标识资源版本的一种机制。服务器在响应头中返回资源的 `ETag` 值(例如 `"abc123"`),客户端在后续请求中可通过 `If-None-Match` 头携带该值。若资源未发生变化,服务器返回 `304 Not Modified`,无需重新传输内容;否则返回新内容及新的 `ETag`。 启用 `ETag` 缓存后,框架会自动处理这一过程:首次请求缓存响应及 `ETag`,后续请求自动附加 `If-None-Match`;收到 `304` 状态码时直接复用缓存内容,减少数据传输并提升请求效率。在弱网环境或移动端流量计费场景下,这一机制能显著降低带宽消耗,同时加快响应速度。 `HTTP` 声明式请求通过 `UseETagAttribute` 特性来启用 `ETag` 响应缓存。相应的 `HTTP` 声明式提取器实现为 [`UseETagDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/UseETagDeclarativeExtractor.cs) 类型,该类型负责解析 `UseETagAttribute` 特性并构建 `HttpRequestBuilder` 实例所需的启用 `ETag` 响应缓存配置。 ```cs showLineNumbers {2,6,10} // 在接口定义上应用,影响所有方法 [UseETag] public interface IHttpService : IHttpDeclarative { // 在方法上应用 [UseETag] [Get("https://furion.net/")] Task GetStringAsync(); [UseETag(false)] // 禁用该功能 [Get("https://furion.net/")] Task GetStringAsync(); } ``` > **`ETag` 响应缓存说明** - 仅对 `GET` 和 `HEAD` 请求生效。 - 若请求显式调用了 `DisableCache()`,`ETag` 功能将自动跳过。 - 缓存默认存储在内存中,支持通过实现 `IHttpETagCache` 接口替换为分布式缓存(如 `Redis`)。 - 默认内存缓存未限制缓存项数量和单条响应内容大小,大量唯一 `URL` 或大响应可能导致内存持续增长。为避免此问题,建议实现 `IHttpETagCache` 接口并替换默认实现,例如: ```cs showLineNumbers services.Replace(ServiceDescriptor.Singleton()); ``` - 若同时启用了全局请求分析工具(`AddProfilerDelegatingHandler`)且发现响应内容未打印,请在请求上显式调用 `Profiler()` 方法即可解决。 > **`UseETagAttribute` 特性作用范围** `UseETagAttribute` 特性适用于方法或接口。 `UseETagAttribute` 包含以下构造函数和属性: - **构造函数**: - `new()`:作用于方法或接口,启用 `ETag` 响应缓存。 - `new(enabled)`:作用于方法或接口,设置是否启用 `ETag` 响应缓存。 - **属性**: - `Enabled`:是否启用(`bool` 类型),默认值为 `true`(启用)。 --- # 5.38 继承与复用 > 原文链接:https://http.furion.net/docs/declarative/inheritance-and-reuse/ `HTTP` 声明式请求支持面向对象的封装与继承等特性。可将通用接口定义在父接口中,再由派生接口继承,实现复用。例如: ```cs showLineNumbers {1,7} public interface IHttpBaseService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStringAsync(); } public interface IHttp1 : IHttpBaseService { [Get("https://baiqian.com/")] Task GetWebsiteAsync(); } ``` 此外,也支持继承未实现 `IHttpDeclarative` 接口的普通接口,例如: ```cs showLineNumbers {1,7} public interface IHttpNormal { [Get("https://baiqian.com/")] Task GetBaiduAsync(); } public interface IHttp2 : IHttpNormal, IHttpBaseService { // ... } ``` 借助封装与继承,可以更高效地组织与复用代码。 --- # 5.39 冻结参数类型 > 原文链接:https://http.furion.net/docs/declarative/frozen-parameter-types/ 在之前的章节中,我们多次提及冻结参数类型,现在终于可以对其进行深入探讨了。 在系统中,`Action`、`Action`、`Action`、`HttpCompletionOption` 以及 `CancellationToken` 被定义为冻结参数类型,它们专门服务于 `HTTP` 声明式请求接口,以提供额外的配置和操作功能。这些冻结参数类型能够极大地扩展 `HTTP` 声明式请求接口的功能,使其覆盖更广泛的使用场景。 - **`Action`** 此参数允许开发者在调用 `HTTP` 声明式接口时,对 `HttpClient` 发送的 `HttpRequestMessage` 进行额外的配置。例如,添加自定义的 `HTTP` 头、设置认证信息等。相应的 `HTTP` 声明式提取器实现为 [`HttpRequestMessageDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpRequestMessageDeclarativeExtractor.cs) 类型,该类型负责解析**单个** `Action` 类型参数,并提供发送请求前的操作。 ```cs showLineNumbers {4} public interface IHttpService : IHttpDeclarative { [Post("https://furion.net/")] Task PostStringAsync([QueryParam] int id, [Body("application/json")] object body, Action? configure = null) } ``` ```cs showLineNumbers {5-8} // 默认调用 await httpService.PostStringAsync(1, new { id = 1, name = "Furion" }); // 提供更多 HttpRequestMessage 配置 await httpService.PostStringAsync(1, new { id = 1, name = "Furion" }, requestMessage => { requestMessage.Headers.TryAddWithoutValidation("header1", "value1"); // 例如添加名为 "header1" 的请求标头 }); ``` - **`Action`** 此参数允许开发者在调用 `HTTP` 声明式接口时,对 `HttpRequestBuilder` 进行额外的配置。例如,添加自定义的 `HTTP` 头、设置认证信息等。相应的 `HTTP` 声明式提取器实现为 [`HttpRequestBuilderDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpRequestBuilderDeclarativeExtractor.cs) 类型,该类型负责解析**单个** `Action` 类型参数,并提供构建 `HttpRequestBuilder` 实例额外的配置。 ```cs showLineNumbers {4} public interface IHttpService : IHttpDeclarative { [Post("https://furion.net/")] Task PostStringAsync([QueryParam] int id, [Body("application/json")] object body, Action? configure = null) } ``` ```cs showLineNumbers {5-8} // 默认调用 await httpService.PostStringAsync(1, new { id = 1, name = "Furion" }); // 提供更多 HttpRequestBuilder 配置 await httpService.PostStringAsync(1, new { id = 1, name = "Furion" }, builder => { builder.AddBearerAuthentication("your-token"); // 例如添加 Bearer 授权 }); ``` - **`Action`** 此参数用于配置多部分表单数据的设置。通过它,开发者可以添加文件、设置文件类型等。相应的 `HTTP` 声明式提取器实现为 [`HttpMultipartFormDataBuilderDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpMultipartFormDataBuilderDeclarativeExtractor.cs) 类型,该类型负责解析**单个** `Action` 类型参数,并提供构建 `HttpMultipartFormDataBuilder` 多部分表单实例额外的配置。 ```cs showLineNumbers {4} public interface IHttpService : IHttpDeclarative { [Post("https://furion.net/")] Task PostStringAsync([Multipart] string name, Action? configure = null); } ``` ```cs showLineNumbers {5-10} // 默认调用 await httpService.PostStringAsync("Furion"); // 提供更多配置多部分表单内容配置 await httpService.PostStringAsync("Furion", multipart => { multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "files", contentType: "image/jpeg"); multipart.AddFileFromBase64String("77u/5rWL6K+V5paH5Lu25YaF5a65", "files"); multipart.AddFileFromRemote("https://furion.net/img/furionlogo.png", "files"); }); ``` - **`HttpCompletionOption`** 此参数用于指定 `HTTP` 响应的读取方式。例如,是否等待整个响应内容读取完毕再返回,还是只读取响应头即返回。 ```cs showLineNumbers {4} public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStreamAsync([QueryParam] string version, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead); } ``` ```cs showLineNumbers {5} // 默认调用 await httpService.GetStreamAsync("v4"); // 自定义响应读取方式 await httpService.GetStreamAsync("v5", HttpCompletionOption.ResponseHeadersRead); ``` - **`CancellationToken`** 此参数允许开发者在发送 `HTTP` 请求时,提供可取消操作的配置。通过它,可以设定请求在特定条件下被取消。 ```cs showLineNumbers public interface IHttpService : IHttpDeclarative { [Get("https://furion.net/")] Task GetStreamAsync(CancellationToken cancellationToken = default); } ``` ```cs showLineNumbers {5-6,8} // 默认调用(无法取消) await httpService.GetStreamAsync("v4"); // 设置 100 毫秒后取消请求 using var cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource.CancelAfter(100); await httpService.GetStreamAsync("v5", cancellationTokenSource.Token); // 假设该请求时间大于 100 毫秒 ``` 值得注意的是,这些冻结参数类型可以组合使用,并且通常(建议)放在方法参数列表的最后面,作为可选配置。**但同一方法参数定义中,同类型的冻结参数必须是唯一的**,否则将抛出 `InvalidOperationException` 异常。 ```cs showLineNumbers {6-9,14-15} public interface IHttpService : IHttpDeclarative { // 支持组合使用 [Post("https://furion.net/")] Task PostStringAsync([QueryParam] int id, [Body("application/json")] object body, Action? multipartConfigure = null, Action? configure = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default); // Action 类型参数不是唯一的,将抛出异常 ❎ [Post("https://furion.net/")] Task PostStringAsync([QueryParam] int id, [Body("application/json")] object body, Action? configure = null, Action? configure1 = null); } ``` > **冻结参数类型执行顺序** 为了确保这些冻结参数类型能够按照预期的顺序执行,它们都实现了 `IFrozenHttpDeclarativeExtractor` 接口,该接口包含一个 `Order` 属性,用于指示执行顺序。 它们执行顺序为:`Action` -> `Action` -> `Action` -> `HttpCompletionOption` -> `CancellationToken`。 通过这些冻结参数类型,`HTTP` 声明式请求接口不仅极大地减轻了开发人员编写 HTTP 请求代码的负担,而且使得代码结构更加清晰、易于维护和复用。 --- # 5.40 获取请求构建器或请求消息(预检请求) > 原文链接:https://http.furion.net/docs/declarative/getting-the-request-builder-or-request-message-pre-flight-request/ 在某些情况下,您可能希望**只获取 `HTTP` 请求对象本身,而不实际发送请求**。例如:在单元测试中验证生成的请求是否符合预期,或拿到请求构建器后进一步手动修改,亦或是将请求消息传递给另一个系统执行。 为此,当发送 `HTTP` 远程请求的方法的返回类型为 `HttpRequestBuilder` 或 `HttpRequestMessage` 时,框架将直接构建并返回该对象,**跳过实际的网络传输**。示例如下: ```cs showLineNumbers {5,9} public interface IHttpService : IHttpDeclarative { // HttpRequestBuilder 类型,不发送请求(预检请求) [Get("https://furion.net/")] Task GetRequestBuilderAsync(); // HttpRequestMessage 类型,不发送请求(预检请求) [Get("https://furion.net/")] Task GetRequestMessageAsync(); } ``` 调用时直接获取对象: ```cs showLineNumbers {2,4,7-8} // 获取构建器,可继续链式配置后手动发送 var builder = await httpService.GetRequestBuilderAsync(); // 不发送请求 builder.WithHeader("X-Custom", "value"); var httpResponseMessage = await httpRemoteService.SendAsync(builder); // 发起网络请求 // 获取 HttpRequestMessage,用于断言或外部传递 var httpRequestMessage = await httpService.GetRequestMessageAsync(); // 不发送请求 Assert.Equal("https://furion.net/", httpRequestMessage.RequestUri?.ToString()); ``` ### 应用场景 - **预检请求(Pre-flight Check)**:在正式发送前,检查生成的请求对象是否符合预期。确认 `URL`、请求头、`Token` 注入等都正确无误后,再手动发送或继续处理。 - **单元测试**:无需模拟网络环境,直接验证生成的 `HttpRequestMessage` 是否包含了正确的参数、头和认证信息。 - **请求对象传递**:将构造好的 `HttpRequestMessage` 传递给其他服务、库或进程执行,实现请求构造与请求执行的分离。 - **混合编程**:先通过构建器或声明式完成大部分配置(参数映射、`Token` 注入等),再拿到构建器进行少量动态修改后手动发送,兼顾声明式的简洁与命令式的灵活。 > **注意** - 返回 `HttpRequestBuilder` 或 `HttpRequestMessage` 的方法**不会发起网络请求**,框架仅在内存中完成构建。 - 如果方法返回其他类型(如 `string`、`HttpResponseMessage` 等),框架会正常发送请求并返回相应结果。 - 此特性与[冻结参数类型](/docs/declarative/frozen-parameter-types/)(如 `Action`)互补,后者在发送前注入配置,但无法阻止发送。 --- # 5.41 自定义 HTTP 声明提取器 > 原文链接:https://http.furion.net/docs/declarative/custom-http-declarative-extractor/ 在 `5.1 声明式请求` 章节中,我们了解到每种特性或参数类型都对应着一种 `HTTP` 声明式提取器。以下系统预置的特性提取器及其对应的实现: - `BaseAddressAttribute` 特性提取器:[`BaseAddressDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/BaseAddressDeclarativeExtractor.cs) - `SuppressExceptionsAttribute` 特性提取器:[`ValidationDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/ValidationDeclarativeExtractor.cs) - `AutoSetHostHeaderAttribute` 特性提取器:[`AutoSetHostHeaderDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/AutoSetHostHeaderDeclarativeExtractor.cs) - `StandardRequestHeadersAttribute` 特性提取器:[`StandardRequestHeadersDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/StandardRequestHeadersDeclarativeExtractor.cs) - `HttpClientNameAttribute` 特性提取器:[`HttpClientNameDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpClientNameDeclarativeExtractor.cs) - `TraceIdentifierAttribute` 特性提取器:[`TraceIdentifierDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/TraceIdentifierDeclarativeExtractor.cs) - `ProfilerAttribute` 特性提取器:[`ProfilerDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/ProfilerDeclarativeExtractor.cs) - `SimulateBrowserAttribute` 特性提取器:[`SimulateBrowserDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/SimulateBrowserDeclarativeExtractor.cs) - `AcceptLanguageAttribute` 特性提取器:[`AcceptLanguageDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/AcceptLanguageDeclarativeExtractor.cs) - `DisableCacheAttribute` 特性提取器:[`DisableDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/DisableDeclarativeExtractor.cs) - `EnsureSuccessStatusCodeAttribute` 特性提取器:[`EnsureSuccessStatusCodeDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/EnsureSuccessStatusCodeDeclarativeExtractor.cs) - `RetryAttribute` 特性提取器:[`RetryDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/RetryDeclarativeExtractor.cs) - `TimeoutAttribute` 特性提取器:[`TimeoutDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/TimeoutDeclarativeExtractor.cs) - `PathSegmentAttribute` 特性提取器:[`PathSegmentDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/PathSegmentDeclarativeExtractor.cs) - `QueryParamAttribute` 特性提取器:[`QueryParamDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/QueryParamDeclarativeExtractor.cs) - `QuotaKeyAttribute` 特性提取器:[`QuotaKeyDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/QuotaKeyDeclarativeExtractor.cs) - `PathAttribute` 特性提取器:[`PathDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/PathDeclarativeExtractor.cs) - `CookieAttribute` 特性提取器:[`CookieDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/CookieDeclarativeExtractor.cs) - `RefererAttribute` 特性提取器:[`RefererDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/RefererDeclarativeExtractor.cs) - `HeaderAttribute` 特性提取器:[`HeaderDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HeaderDeclarativeExtractor.cs) - `PropertyAttribute` 特性提取器:[`PropertyDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/PropertyDeclarativeExtractor.cs) - `HttpVersionAttribute` 特性提取器:[`HttpVersionDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpVersionDeclarativeExtractor.cs) - `SuppressExceptionsAttribute` 特性提取器:[`SuppressExceptionsDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/SuppressExceptionsDeclarativeExtractor.cs) - `RemoveTrailingSlashAttribute` 特性提取器:[`RemoveTrailingSlashDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/RemoveTrailingSlashDeclarativeExtractor.cs) - `RequestEventHandlerAttribute` 特性提取器:[`RequestEventHandlerDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/RequestEventHandlerDeclarativeExtractor.cs) - `JsonResponseWrapperAttribute` 特性提取器:[`JsonResponseWrapperDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/JsonResponseWrapperDeclarativeExtractor.cs) - `JsonResponseStringUnwrapAttribute` 特性提取器:[`JsonResponseStringUnwrapDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/JsonResponseStringUnwrapDeclarativeExtractor.cs) - `SuppressTokenManagementAttribute` 特性提取器:[`SuppressTokenManagementDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/SuppressTokenManagementDeclarativeExtractor.cs) - `UseETagAttribute` 特性提取器:[`UseETagDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/UseETagDeclarativeExtractor.cs) - `BodyAttribute` 特性提取器:[`BodyDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/BodyDeclarativeExtractor.cs) - `MultipartAttribute` 和 `MultipartFormAttribute` 特性提取器:[`MultipartDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/MultipartDeclarativeExtractor.cs) - **`Action`** 参数提取器:[`HttpRequestMessageDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpRequestMessageDeclarativeExtractor.cs) - **`Action`** 参数提取器:[`HttpMultipartFormDataBuilderDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpMultipartFormDataBuilderDeclarativeExtractor.cs) - **`Action`** 参数提取器:[`HttpRequestBuilderDeclarativeExtractor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Parsers/Declarative/Extractors/HttpRequestBuilderDeclarativeExtractor.cs) 通过自定义 `HTTP` 声明式提取器,您可以为 `HTTP` 声明式接口提供额外的功能。以下是一个自定义 `AcceptAttribute` 特性及其提取器的示例: **1. 定义 `AcceptAttribute` 特性** 设置 `AcceptAttribute` 特性作用范围为方法或接口上。 ```cs showLineNumbers {1-2} [AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)] public sealed class AcceptAttribute : Attribute { public AcceptAttribute(string accept) { ArgumentException.ThrowIfNullOrWhiteSpace(accept); Accept = accept; } public string Accept { get; set; } } ``` **2. 实现 `AcceptDeclarativeExtractor` 提取器** 解析 `AcceptAttribute` 特性并设置给 `HttpRequestBuilder` 实例。 ```cs showLineNumbers {1,4,7,13} public sealed class AcceptDeclarativeExtractor : IHttpDeclarativeExtractor { // 实现 Extract 方法 public void Extract(HttpRequestBuilder httpRequestBuilder, HttpDeclarativeParsingContext context) { // 获取方法或接口定义的 AcceptAttribute 特性 if (!context.IsMethodDefined(out var acceptAttribute, true)) { return; } // 设置 Accept 头 httpRequestBuilder.WithHeader("Accept", acceptAttribute.Accept, replace: true); } } ``` 提取器 `Extract` 方法的 `context` 参数的类型为 `HttpDeclarativeParsingContext`,包含以下属性和方法: - **属性**: - `Method`:被调用方法(`MethodInfo` 类型)。 - `Args`:被调用方法的参数值数组(`object[]` 类型)。 - `MethodMetadata`:被调用方法的元数据(`HttpDeclarativeMethodMetadata` 类型)。 - `Parameters`:被调用方法的参数键值字典(`IReadOnlyDictionary` 类型)。 - `UnFrozenParameters`:被调用方法的**非冻结**类型参数键值字典(`IReadOnlyDictionary` 类型)。 - **方法**: - `IsFrozenParameter(parameter)`:判断参数是否是冻结参数类型。 - `IsMethodDefined(out var attribute, inherit)`:检查被调用方法是否定义了指定特性。 - `GetMethodDefinedCustomAttributes(inherit, methodScanFirst)`:获取被调用方法指定特性的所有实例。 **3. 在配置中注册自定义提取器** 在 `Startup.cs` 或 `Program.cs` 文件中,配置并注册 `HttpRemote` 服务,以启用自定义 `HTTP` 声明式提取器功能。 ```cs showLineNumbers {1,3} services.AddHttpRemote(builder => { builder.AddHttpDeclarativeExtractors(() => [ new AcceptDeclarativeExtractor() ]); }); ``` **4. 在 `HTTP` 声明式接口中使用自定义特性** ```cs showLineNumbers {1,5} [Accept("text/html")] public interface IHttpService : IHttpDeclarative { // 在方法上应用 [Accept("text/xml")] [Get("https://furion.net/")] Task GetStringAsync(); } ``` > **小知识** 当自定义特性允许在参数中使用时,请务必通过 `HttpDeclarativeParsingContext.IsFrozenParameter(parameter)` 方法来排除冻结类型的参数。示例代码如下: ```cs showLineNumbers {2,6} // 通过 UnFrozenParameters 属性返回非冻结类型参数键值对 context.UnFrozenParameters; // 通过 HttpDeclarativeParsingContext.IsFrozenParameter 静态方法手动判断 var parameters = context.Parameters.Where(param => !HttpDeclarativeParsingContext.IsFrozenParameter(param.Key) && // 过滤掉冻结类型的参数 param.Key.IsDefined(typeof(YourAttribute), true)).ToArray(); ``` 此代码段展示了如何筛选出不包含冻结参数且标记了特定特性的参数数组。 通过上述步骤,您已经成功创建了一个自定义的 `HTTP` 声明式提取器。这不仅可以增强 `HTTP` 声明式接口的功能,还可以使代码更加简洁和易于维护。您可以根据自己的需求继续扩展和自定义其他 `HTTP` 声明式提取器。 如需更多自定义的 `HTTP` 声明式提取器,请参考框架内置的 `HTTP` 声明式提取器代码实现。 --- # 5.42 自定义 HTTP 声明提取器(授权) > 原文链接:https://http.furion.net/docs/declarative/custom-http-declarative-extractor-authorization/ 以下是一个示例,展示了如何通过自定义 `AuthenticationAttribute` 和 `AllowAnonymousAttribute` 特性,并添加相应的提取器,以实现自动授权和匿名访问功能。 **1. 定义 `AuthenticationAttribute` 特性** 将 `AuthenticationAttribute` 特性应用于方法或接口上。 ```cs showLineNumbers {1-2} [AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)] public class AuthenticationAttribute : Attribute; ``` **2. 实现 `AuthenticationDeclarativeExtractor` 和 `AllowAnonymousDeclarativeExtractor` 提取器** ```cs showLineNumbers {4,10,13,16,24,30,33} /// /// [Authentication] 特性提取器 /// public class AuthenticationDeclarativeExtractor : IHttpDeclarativeExtractor { /// public void Extract(HttpRequestBuilder httpRequestBuilder, HttpDeclarativeParsingContext context) { // 如果贴了 [AllowAnonymous] 特性则跳过 if (context.IsMethodDefined(out _, true)) return; // 检查是否已经设置了授权信息 if (httpRequestBuilder.AuthenticationHeader is not null) return; // 添加授权标头(这里可以实现任何授权的逻辑,比如从参数获取 token 等等) httpRequestBuilder.AddBearerAuthentication( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); } } /// /// [AllowAnonymous] 特性提取器 /// public class AllowAnonymousDeclarativeExtractor : IHttpDeclarativeExtractor { /// public void Extract(HttpRequestBuilder httpRequestBuilder, HttpDeclarativeParsingContext context) { // 如果没有贴 [AllowAnonymous] 特性则跳过 if (!context.IsMethodDefined(out _, true)) return; // 移除授权标头 httpRequestBuilder.RemoveHeaders("Authorization"); } } ``` **3. 在配置中注册自定义提取器** 在 `Startup.cs` 或 `Program.cs` 文件中,配置并注册 `HttpRemote` 服务,以启用自定义 `HTTP` 声明式提取器功能。 ```cs showLineNumbers {1,4,7} services.AddHttpRemote(builder => { // 添加自定义 HTTP 声明式提取器 builder.AddHttpDeclarativeExtractors(() => [ new AuthenticationDeclarativeExtractor(), new AllowAnonymousDeclarativeExtractor() ]); // 扫描程序集批量添加 HTTP 声明式提取器(推荐) // builder.AddHttpDeclarativeExtractorsFromAssemblies([ assembly1, assembly2, ... ]); // 若使用 Furion 框架可直接设置 App.Assemblies }); ``` **4. 在 `HTTP` 声明式接口中使用自定义特性** ```cs showLineNumbers {1,7} [Authentication] // 添加全局授权 public interface IAuthService : IHttpDeclarative { [Get("https://furion.net/")] Task GetDataAsync(); // 访问这个接口需要授权 [AllowAnonymous] // 匿名访问 [Get("https://furion.net/")] Task LoginAsync(string username, string password); } ``` 当调用 `GetDataAsync` 方法时,将自动添加授权标头(实现授权)。调用 `LoginAsync` 方法时,将自动移除授权请求标头(实现匿名访问)。 通过这个示例可以看出,自定义 `HTTP` 声明提取器为实现复杂的授权逻辑提供了极大的灵活性。 --- # 5.43 HttpDeclarativeBuilder 构建器(动态构建) > 原文链接:https://http.furion.net/docs/declarative/declarative-builder/ `HttpDeclarativeBuilder` 构建器是框架提供专门用来动态构建 `HTTP` 声明式请求所需的各项设置。`HttpDeclarativeBuilder` 的构造函数是私有的,因此无法直接使用 `new` 关键字进行实例化,不过,框架提供了 `HttpRequestBuilder.Declarative` 的多个静态重载方法创建 `HttpDeclarativeBuilder` 的实例。 ```cs showLineNumbers HttpRequestBuilder.Declarative(methodInfo, args); ``` 上述代码演示了如何利用 `MethodInfo` 类型参数和参数数组来动态构建一个 `HTTP` 声明式请求构建器。这一机制使得我们能够针对任意类型的方法实现 `HTTP` 声明式请求功能。以下是一个具体的例子: ```cs showLineNumbers {1,3,6} public class NormalClass { [Get("https://furion.net"/)] public Task GetStringAsync() { throw new NotImplementedException(); // 无需实现 } } ``` 通过以下步骤,我们可以动态地基于 `NormalClass` 的 `GetStringAsync` 方法构建 `HTTP` 声明式请求: ```cs showLineNumbers {2,5} // 获取 NormalClass 类型的 GetStringAsync 方法 var getStringMethod = typeof(NormalClass).GetMethod(nameof(NormalClass.GetStringAsync), BindingFlags.Instance | BindingFlags.Public); // 发送 HTTP 请求 var str = await httpRemoteService.SendAsAsync(HttpRequestBuilder.Declarative(getStringMethod, [])); ``` 通过这种方式,我们实现了对任意类型方法的 `HTTP` 声明式请求的动态构建。此外,`HTTP` 声明式请求还支持多种方法,包括但不限于: ```cs showLineNumbers {1-2,4-5} httpRemoteService.Declarative(method, args); await httpRemoteService.DeclarativeAsync(method, args); httpRemoteService.SendAs(httpDeclarativeBuilder); await httpRemoteService.SendAsAsync(httpDeclarativeBuilder); ``` --- # 6.1 IHttpContentProcessor 内容处理器 > 原文链接:https://http.furion.net/docs/advanced-guide/ihttpcontentprocessor-content-processor/ `IHttpContentProcessor` 用于根据用户设置的原始请求内容和类型构建 `HttpContent` 实例,并将其设置为 `HttpRequestMessage` 对象的 `Content` 属性。如下图所示: ![httpagent](/images/httpagent.jpg) [**查看高清架构图**](https://gitee.com/dotnetchina/HttpAgent/blob/master/drawio/HttpAgent.drawio) --- # 6.2 内置内容处理器 > 原文链接:https://http.furion.net/docs/advanced-guide/built-in-content-processors/ - **`StringContentProcessor` 内容处理器** 当原始请求内容满足以下条件时,将使用 [`StringContentProcessor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Processors/StringContentProcessor.cs) 来构建 `StringContent` 实例: - 原始请求内容为 `StringContent` 或 `JsonContent`。 - 内容类型包括 `application/json`、`application/json-patch+json`、`application/xml`、`application/xml-patch+xml`、`text/xml`、`text/html`、`text/plain` 以及 `application/soap+xml`,且即使这些类型后附加有 `charset` 字符集(例如 `application/json; charset=utf-8`),依然适用。 > **关于默认 `JSON` 序列化配置** `StringContentProcessor` 内容处理器默认采用 `JsonSerializerOptions.Web` 配置,该配置提供了一套适用于 Web 场景的默认序列化设置。如需了解该配置的详细信息,请查阅官方文档:[`JsonSerializerOptions` 的 `Web` 默认值](https://learn.microsoft.com/zh-cn/dotnet/standard/serialization/system-text-json/configure-options#web-defaults-for-jsonserializeroptions)。 若需自定义这些 `JSON` 序列化选项,可通过以下方式进行调整: ```cs showLineNumbers {2-3,6,10-11,14} // 全局配置(适用于所有客户端) services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // 自定义 JSON 序列化行为,例如忽略空值 options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); // 客户端级配置(优先级更高) services.AddHttpClient("客户端名称") .ConfigureOptions(options => { // 自定义 JSON 序列化行为,例如忽略空值 options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); ``` 在上述代码中,通过 `ConfigureOptions` 方法,您可以灵活地调整 `JsonSerializerOptions` 的各项设置,以满足特定的序列化需求。 --- - **`StreamContentProcessor` 内容处理器** 当原始请求内容满足以下条件时,将使用 [`StreamContentProcessor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Processors/StreamContentProcessor.cs) 来构建 `StreamContent` 实例: - 原始请求内容为 `StreamContent` 或 `Stream`。 --- - **`ByteArrayContentProcessor` 内容处理器** 当原始请求内容满足以下条件时,将使用 [`ByteArrayContentProcessor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Processors/ByteArrayContentProcessor.cs) 来构建 `ByteArrayContent` 实例: - 原始请求内容为 `ByteArrayContent` 或 `byte[]`,且不为 `FormUrlEncodedContent` 和 `StringContent`。 --- - **`FormUrlEncodedContentProcessor` 内容处理器** 当原始请求内容满足以下条件时,将使用 [`FormUrlEncodedContentProcessor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Processors/FormUrlEncodedContentProcessor.cs) 来构建 `FormUrlEncodedContent` 实例: - 原始请求内容为 `FormUrlEncodedContent` 或 `URL` 编码格式的字符串。 - 内容类型为 `application/x-www-form-urlencoded`,且即使类型后附加有 `charset` 字符集(例如 `application/x-www-form-urlencoded; charset=utf-8`),依然适用。 --- - **`StringContentForFormUrlEncodedContentProcessor` 内容处理器** 当原始请求内容满足以下条件时,将使用 [`StringContentForFormUrlEncodedContentProcessor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Processors/StringContentForFormUrlEncodedContentProcessor.cs) 来构建内容类型为 `application/x-www-form-urlencoded` 的 `StringContent` 实例: - 原始请求内容为 `FormUrlEncodedContent` 或 `URL` 编码格式的字符串。 - 内容类型为 `application/x-www-form-urlencoded`,且即使类型后附加有 `charset` 字符集(例如 `application/x-www-form-urlencoded; charset=utf-8`),依然适用。 - **`useStringContent` 参数或 `UseStringContent` 属性 为 `true`。** `StringContentForFormUrlEncodedContentProcessor` 派生自 `FormUrlEncodedContentProcessor`。 --- - **`ReadOnlyMemoryContentProcessor` 内容处理器** 当原始请求内容满足以下条件时,将使用 [`ReadOnlyMemoryContentProcessor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Processors/ReadOnlyMemoryContentProcessor.cs) 来构建 `ReadOnlyMemoryContent` 实例: - 原始请求内容为 `ReadOnlyMemoryContent` 或 `ReadOnlyMemory`。 --- - **`MultipartFormDataContentProcessor` 内容处理器** 当原始请求内容满足以下条件时,将使用 [`MultipartFormDataContentProcessor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Processors/MultipartFormDataContentProcessor.cs) 来构建 `MultipartFormDataContent` 实例: - 原始请求内容为 `MultipartFormDataContent`。 - 内容类型为 `multipart/form-data`,且即使类型后附加有 `charset` 字符集(例如 `multipart/form-data; charset=utf-8`),依然适用。 --- - **`MessagePackContentProcessor` 内容处理器** 当原始请求内容满足以下条件时,将使用 [`MessagePackContentProcessor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Processors/MessagePackContentProcessor.cs) 来构建 `ByteArrayContent` 实例: - 内容类型为 `application/msgpack`,且即使类型后附加有 `charset` 字符集(例如 `application/msgpack; charset=utf-8`),依然适用。 --- - **`JsonLinesContentProcessor` 内容处理器** 当原始请求内容满足以下条件时,将使用 [`JsonLinesContentProcessor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Processors/JsonLinesContentProcessor.cs) 来构建 `StringContent` 实例: - 内容类型包括 `application/x-ndjson`、`application/x-jsonlines`、`application/jsonlines` 以及 `application/jsonl`,且即使类型后附加有 `charset` 字符集(例如 `application/x-ndjson; charset=utf-8`),依然适用。 - **`FileInfoContentProcessor` 内容处理器** 当原始请求内容满足以下条件时,将使用 [`FileInfoContentProcessor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Processors/FileInfoContentProcessor.cs) 来构建 `StreamContent` 实例: - 原始请求内容为 `FileInfo`。 --- - **`FormFileContentProcessor` 内容处理器** 当原始请求内容满足以下条件时,将使用 [`FormFileContentProcessor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent.AspNetCore/src/Processors/FormFileContentProcessor.cs) 来构建 `StreamContent` 实例: - 原始请求内容为 `IFormFile`。 --- --- # 6.3 IHttpContentProcessorFactory 内容处理器工厂 > 原文链接:https://http.furion.net/docs/advanced-guide/ihttpcontentprocessorfactory-content-processor-factory/ `IHttpContentProcessorFactory` 内容处理器工厂负责根据原始请求的内容和类型,确定合适的 `IHttpContentProcessor` 内容处理器,并调用其 `Process` 方法来生成 `HttpContent` 实例。该工厂服务被配置为单例模式,以确保其在应用程序生命周期中的唯一性和稳定性。 如果未找到合适的 `IHttpContentProcessor` 内容处理器,将引发 `InvalidOperationException` 异常,异常信息如下: ```bash showLineNumbers No processor found that can handle the content type `application/pdf` and the provided raw content of type `System.Span`1[T]`. Please ensure that the correct content type is specified and that a suitable processor is registered. ``` 以下是使用 `IHttpContentProcessorFactory` 内容处理器工厂与 `HttpClient` 结合的示例,展示了如何通过其 `Build` 方法轻松构建合适的 `HttpContent` 内容。根据之前的章节说明,当内容类型为 `application/json` 时,将使用 `StringContentProcessor` 处理器,并生成 `StringContent` 实例。 ```cs showLineNumbers {1,8} public class YourService(IHttpContentProcessorFactory httpContentProcessorFactory) // .NET8+ 支持主构造函数注入 { public async Task GetStringAsync() { var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "https://furion.net/"); // 调用 Build 方法构建 HttpContent 实例,实例具体类型为 StringContent var httpContent = httpContentProcessorFactory.Build(new HttpContentProcessorContext(new { id = 1, name = "Furion" }, "application/json")); httpRequestMessage.Content = httpContent; using var httpClient = new HttpClient(); var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage); return await httpResponseMessage.Content.ReadAsStringAsync(); } } ``` > **关于查找 `IHttpContentProcessor` 内容处理器** `IHttpContentProcessorFactory` 工厂会按照最后新增的内容处理器开始查找合适的 `IHttpContentProcessor`。一旦某个内容处理器的 `CanProcess` 方法返回 `true`,即表示找到匹配的内容处理器,随后将使用该处理器来构建 `HttpContent`。 --- # 6.4 自定义内容处理器(如序列化) > 原文链接:https://http.furion.net/docs/advanced-guide/custom-content-processor-eg-serialization/ 在特定场景下,当框架内置的 `IHttpContentProcessor` 内容处理器无法满足需求时,可以通过自定义 `IHttpContentProcessor` 内容处理器来解决。 如果您希望替换框架默认的 `System.Text.Json` 序列化提供程序,例如使用 `Newtonsoft.Json` 来为 `application/json` 内容类型添加特定的序列化配置选项,那么您可以通过实现 `IHttpContentProcessor` 接口来满足这一自定义需求。 > **框架建议** 但请注意,除非有充分的理由,否则通常建议使用 `System.Text.Json`,因为它与 `.NET Core` 紧密集成,且性能优异。 若需自定义这些 `JSON` 序列化选项,可通过以下方式进行调整: ```cs showLineNumbers {2-3,6,10-11,14} // 全局配置(适用于所有客户端) services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // 自定义 JSON 序列化行为,例如忽略空值 options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); // 客户端级配置(优先级更高) services.AddHttpClient("客户端名称") .ConfigureOptions(options => { // 自定义 JSON 序列化行为,例如忽略空值 options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); ``` ```cs showLineNumbers {1,4,15} public class CustomStringContentProcessor : HttpContentProcessorBase { public override bool CanProcess(HttpContentProcessorContext context) => context.ContentType == "application/json"; public override HttpContent? Process(HttpContentProcessorContext context) { if (TryProcess(context, out var httpContent)) { return httpContent; } var content = context.RawContent!.GetType().IsBasicType() || context.RawContent is JsonElement or JsonNode ? context.RawContent.ToString() : context.RawContent.ToJsonString(ResolveJsonSerializerOptions(context.HttpClientName)); var stringContent = new StringContent(content!, context.Encoding, new MediaTypeHeaderValue(context.ContentType) { CharSet = context.Encoding?.WebName ?? "utf-8" }); return stringContent; } } ``` 接下来,可以通过以下两种方式应用自定义内容处理器: - **单次请求设置**: ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net/") .AddHttpContentProcessors(() => [ new CustomStringContentProcessor() ]) .SetJsonContent(new { id = 1, name = "Furion" }); ``` - **全局配置**: 在 `Startup.cs` 或 `Program.cs` 文件中,配置并注册 `HttpRemote` 服务,以启用自定义内容处理器功能: ```cs showLineNumbers {1,3} services.AddHttpRemote(builder => { builder.AddHttpContentProcessors(() => [ new CustomStringContentProcessor() ]); }); ``` > **`HttpContentProcessorBase` 基类说明** `HttpContentProcessorBase` 基类内置了一个 `ServiceProvider` 属性,该属性允许您轻松解析并获取通过依赖注入(`DI`)注册的服务。 > **`IHttpContentProcessor` 相关知识扩展** 要了解更多关于自定义 `IHttpContentProcessor` 内容处理器的信息,请访问 [`HttpAgent` 官方仓库](https://gitee.com/dotnetchina/HttpAgent/tree/master/src/HttpAgent/src/Processors) 进行查阅。 --- # 6.5 添加 MessagePack 支持 > 原文链接:https://http.furion.net/docs/advanced-guide/adding-messagepack-support/ `MessagePack` 是一种紧凑、高效的二进制序列化格式,专为多种语言间的数据交换设计。相较于 `JSON`,`MessagePack` 提供了更高的性能和更小的数据体积。尽管是二进制格式,`MessagePack` 在设计时便充分考虑了跨语言使用的便捷性,目前已被广泛应用于 `Python`、`Ruby`、`JavaScript`、`C++` 以及 `C#` 等多种编程语言中。 要在项目中启用 `MessagePack` 支持,请按照以下步骤操作: 1. **安装 `MessagePack` 包**: ```bash showLineNumbers dotnet add package MessagePack ``` 2. **添加 `MessagePackContentProcessor` 内容处理器**: ```cs showLineNumbers {1,5,17} public class MessagePackContentProcessor : HttpContentProcessorBase { /// public override bool CanProcess(HttpContentProcessorContext context) => context.ContentType == "application/msgpack"; /// public override HttpContent? Process(HttpContentProcessorContext context) { // 尝试解析 HttpContent 类型 if (TryProcess(context, out var httpContent)) { return httpContent; } // 将原始请求内容转换为字节数组 var content = context.RawContent as byte[] ?? MessagePackSerializer.Serialize(context.RawContent); // 初始化 ByteArrayContent 实例 var byteArrayContent = new ByteArrayContent(content); byteArrayContent.Headers.ContentType = new MediaTypeHeaderValue(context.ContentType) { CharSet = context.Encoding?.WebName }; return byteArrayContent; } } ``` 3. **应用 `MessagePackContentProcessor` 内容处理器**: - **单次请求设置**: ```cs showLineNumbers {2-3} HttpRequestBuilder.Post("https://furion.net/") .AddHttpContentProcessors(() => [ new MessagePackContentProcessor() ]) .SetContent(new MessagePackModel { Id = 1, Name = "Furion" }, "application/msgpack"); ``` > **注意事项** 为使用 `MessagePack` 序列化,您的模型类需要添加 `MessagePackObject` 特性,并为属性添加 `MessagePack.Key` 特性。详细文档请参考 [MessagePack-CSharp 官方仓库](https://github.com/MessagePack-CSharp/MessagePack-CSharp)。 ```cs showLineNumbers {1,4,7} [MessagePackObject] public class MessagePackModel { [MessagePack.Key(0)] public int Id { get; set; } [MessagePack.Key(1)] public string? Name { get; set; } } ``` - **全局配置**: 在 `Startup.cs` 或 `Program.cs` 文件中,配置并注册 `HttpRemote` 服务,以启用 `MessagePackContentProcessor` 内容处理器功能: ```cs showLineNumbers {1,3} services.AddHttpRemote(builder => { builder.AddHttpContentProcessors(() => [ new MessagePackContentProcessor() ]); }); ``` 这样可以在项目中通过 `HTTP` 远程请求发送 `application/msgpack` 格式的数据。 > **小提示** 系统默认内置了 `MessagePackContentProcessor` 内容处理器,只需在项目中安装 `MessagePack` 包即可启用。不过,请注意,内置的 [`MessagePackContentProcessor`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Processors/MessagePackContentProcessor.cs) 是通过反射创建的,可能会带来一定的性能损耗。 如果对性能有极高要求,可以考虑使用上述自定义方式实现;否则,直接使用框架内置的处理器即可。 --- # 6.6 添加 Protobuf 支持 > 原文链接:https://http.furion.net/docs/advanced-guide/adding-protobuf-support/ `Protobuf`(`Protocol Buffers`)是 Google 开发的一种语言中立、平台中立、可扩展的序列化结构数据格式,用于通信协议、数据存储等。 要在项目中启用 `Protobuf` 支持,请按照以下步骤操作: 1. **安装 `protobuf-net` 包**: ```bash showLineNumbers dotnet add protobuf-net ``` 2. **添加 `ProtobufContentProcessor` 内容处理器**: ```cs showLineNumbers {1,5,24-26} public class ProtobufContentProcessor : HttpContentProcessorBase { /// public override bool CanProcess(HttpContentProcessorContext context) => context.ContentType == "application/x-protobuf"; /// public override HttpContent? Process(HttpContentProcessorContext context) { // 尝试解析 HttpContent 类型 if (TryProcess(context, out var httpContent)) { return httpContent; } byte[] content; if (context.RawContent is byte[] bytes) { content = bytes; } else { // 将原始请求内容转换为字节数组 using var ms = new MemoryStream(); Serializer.Serialize(ms, context.RawContent); content = ms.ToArray(); } // 初始化 ByteArrayContent 实例 var byteArrayContent = new ByteArrayContent(content); byteArrayContent.Headers.ContentType = new MediaTypeHeaderValue(context.ContentType) { CharSet = context.Encoding?.WebName }; return byteArrayContent; } } ``` 3. **应用 `ProtobufContentProcessor` 内容处理器**: - **单次请求设置**: ```cs showLineNumbers {2-3} HttpRequestBuilder.Post("https://furion.net/") .AddHttpContentProcessors(() => [ new ProtobufContentProcessor() ]) .SetContent(new MyProtobufMessage { Id = 1, Name = "Furion" }, "application/x-protobuf"); ``` > **注意事项** 要使用 `protobuf-net` 进行序列化,您的模型类需添加 `ProtoContract` 特性,并且其属性需添加 `ProtoMember` 特性。详细文档请查阅 [protobuf-net 官方仓库](https://github.com/protobuf-net/protobuf-net)。不过,类型通常是通过 `.proto` 文件来定义并生成的: 1. 定义 `my_message.proto` 文件如下: ```cs showLineNumbers {1,3,6-9} syntax = "proto3"; // 指定使用 proto3 语法 package mynamespace; // 可选:定义包名(对应 C# 的命名空间) // 定义 MyProtobufMessage 消息类型 message MyProtobufMessage { int32 id = 1; // 整数字段,标签号为 1 string name = 2; // 字符串字段,标签号为 2 } ``` 2. 使用 [`Google.Protobuf`](https://www.nuget.org/packages/Google.Protobuf) 命令行工具生成对应的 `C#` 类,命令如下: ```bash protoc -I=./ --csharp_out=./Generated ./my_message.proto ``` 生成的 C# 类可能类似于以下形式(已添加 `ProtoContract` 和 `ProtoMember` 特性): ```cs showLineNumbers {1,4,7} [ProtoContract] public class MyProtobufMessage { [ProtoMember(1)] public int Id { get; set; } [ProtoMember(2)] public string Name { get; set; } } ``` - **全局配置**: 在 `Startup.cs` 或 `Program.cs` 文件中,配置并注册 `HttpRemote` 服务,以启用 `ProtobufContentProcessor` 内容处理器功能: ```cs showLineNumbers {1,3} services.AddHttpRemote(builder => { builder.AddHttpContentProcessors(() => [ new ProtobufContentProcessor() ]); }); ``` 这样可以在项目中通过 `HTTP` 远程请求发送 `application/x-protobuf` 格式的数据。 --- # 6.7 IHttpContentConverter 内容转换器 > 原文链接:https://http.furion.net/docs/advanced-guide/ihttpcontentconverter-content-converter/ `IHttpContentConverter` 用于将 `HTTP` 远程请求返回的 `HttpResponseMessage` 对象转换成目标类型。如下图所示: ![httpagent](/images/httpagent.jpg) [**查看高清架构图**](https://gitee.com/dotnetchina/HttpAgent/blob/master/drawio/HttpAgent.drawio) --- # 6.8 内置内容转换器 > 原文链接:https://http.furion.net/docs/advanced-guide/built-in-content-converters/ - **`StringContentConverter` 内容转换器** 当目标接收类型为字符串时,将使用 [`StringContentConverter`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Converters/StringContentConverter.cs) 来将 `HttpResponseMessage` 对象转换为字符串。该转换过程内部调用了 `HttpResponseMessage.Content` 提供的 `ReadAsStringAsync` 方法来实现。 --- - **`StreamContentConverter` 内容转换器** 当目标接收类型为 `Stream` 时,将使用 [`StreamContentConverter`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Converters/StreamContentConverter.cs) 来将 `HttpResponseMessage` 对象转换为 `Stream`。该转换过程内部调用了 `HttpResponseMessage.Content` 提供的 `ReadAsStreamAsync` 方法来实现。 --- - **`ByteArrayContentConverter` 内容转换器** 当目标接收类型为字节数组时,将使用 [`ByteArrayContentConverter`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Converters/ByteArrayContentConverter.cs) 来将 `HttpResponseMessage` 对象转换为字节数组。该转换过程内部调用了 `HttpResponseMessage.Content` 提供的 `ReadAsByteArrayAsync` 方法来实现。 --- - **`HttpResponseMessageConverter` 内容转换器** 当目标接收类型为 `HttpResponseMessage` 时,将使用 [`HttpResponseMessageConverter`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Converters/HttpResponseMessageConverter.cs) 来将 `HttpResponseMessage` 对象直接返回。 --- - **`VoidContentConverter` 内容转换器** 当目标接收类型为 `void` 或 `VoidContent` 时,将使用 [`VoidContentConverter`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Converters/VoidContentConverter.cs) 来返回空值(无返回值)。 --- - **`IActionResultContentConverter` 内容转换器** 当目标接收类型为 `IActionResult` 时,将使用 [`IActionResultContentConverter`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent.AspNetCore/src/Converters/IActionResultContentConverter.cs) 来将 `HttpResponseMessage` 对象转换为 `IActionResultContentConverter`。 --- - **`AsyncEnumerableContentConverter` 内容转换器** 当目标接收类型为 `IAsyncEnumerable` 时,将使用 [`AsyncEnumerableContentConverter`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Converters/AsyncEnumerableContentConverter.cs) 来将 `HttpResponseMessage` 对象转换为 `IAsyncEnumerable`。该转换过程内部调用了 `HttpResponseMessage.Content` 提供的 `ReadFromJsonAsAsyncEnumerable` 方法来实现。 --- - **`HttpRemoteResult` 内容转换器** 当目标接收类型为 `HttpRemoteResult` 时,将使用 [`HttpRemoteResultContentConverter`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Converters/HttpRemoteResultContentConverter.cs) 来将 `HttpResponseMessage` 对象转换为 `HttpRemoteResult`。该转换过程内部调用了 `IHttpContentConverterFactory` 提供的 `ReadAsync` 方法来实现。 --- - **`ObjectContentConverter` 内容转换器** 当目标接收类型不为特定类型,例如自定义类型、基本数据类型(如 `int`、`bool` 等)在内的多种类型,将使用 [`ObjectContentConverter`](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Converters/ObjectContentConverter.cs) 来将 `HttpResponseMessage` 对象转换为目标接收类型。该转换过程内部调用了 `HttpResponseMessage.Content` 提供的 `ReadFromJsonAsync` 方法来实现。 --- # 6.9 IHttpContentConverterFactory 内容转换器工厂 > 原文链接:https://http.furion.net/docs/advanced-guide/ihttpcontentconverterfactory-content-converter-factory/ `IHttpContentConverterFactory` 内容转换器工厂负责根据目标接收类型,确定合适的 `IHttpContentConverter` 内容转换器,并调用其 `Read` 方法来将 `HttpResponseMessage` 对象转换为目标接收类型。该工厂服务被配置为单例模式,以确保其在应用程序生命周期中的唯一性和稳定性。 **如果未找到匹配的 `IHttpContentConverter` 内容处理器,系统会回退到使用 `IObjectContentConverterFactory` 对象内容转换器工厂来进行转换,其内部通过返回 `ObjectContentConverter()` 实例进行转换。** 以下是使用 `IHttpContentConverterFactory` 内容转换器工厂与 `HttpClient` 结合的示例,展示了如何通过其 `Read` 方法轻松将 `HttpResponseMessage` 对象转换为目标类型实例。 ```cs showLineNumbers {1,11-12} public class YourService(IHttpContentConverterFactory httpContentConverterFactory) // .NET8+ 支持主构造函数注入 { public async Task GetStringAsync() { var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "https://furion.net/"); using var httpClient = new HttpClient(); var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage); // 调用 Read 方法将 HttpResponseMessage 对象转换为目标类型实例 var context = new HttpContentConverterContext(httpResponseMessage); return await httpContentConverterFactory.GetConverter(context).ReadAsync(context); } } ``` > **关于查找 `IHttpContentConverter` 内容转换器** `IHttpContentConverterFactory` 工厂会按照最后新增的内容转换器开始查找目标接收类型匹配的 `IHttpContentConverter`。一旦某个内容处理器的泛型类型与目标接收类型相同,即表示找到匹配的内容转换器,随后将使用该转换器来读取目标类型实例。 --- # 6.10 IObjectContentConverterFactory 对象内容转换器工厂 > 原文链接:https://http.furion.net/docs/advanced-guide/iobjectcontentconverterfactory-object-content-converter-factory/ 当 `IHttpContentConverterFactory` 内容转换器工厂未找到匹配的 `IHttpContentConverter` 内容处理器,系统会回退到使用 `IObjectContentConverterFactory` 对象内容转换器工厂来进行转换,其内部通过返回 `ObjectContentConverter()` 实例进行转换。该工厂服务被配置为单例模式,以确保其在应用程序生命周期中的唯一性和稳定性。 以下是使用 `IObjectContentConverterFactory` 内容转换器工厂与 `HttpClient` 结合的示例,展示了如何通过其 `Read` 方法轻松将 `HttpResponseMessage` 对象转换为目标类型实例。 ```cs showLineNumbers {1,12-13} public class YourService(IObjectContentConverterFactory objectContentConverterFactory) // .NET8+ 支持主构造函数注入 { public async Task GetStringAsync() { var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "https://furion.net/getuser/100"); using var httpClient = new HttpClient(); var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage); // 调用 GetConverter(new HttpContentConverterContext(httpResponseMessage)) 方法获取对象内容转换器实例 // 随后调用 Read 方法将 HttpResponseMessage 对象转换为目标类型实例 var context = new HttpContentConverterContext(httpResponseMessage); return await objectContentConverterFactory.GetConverter(context).ReadAsync(context); } } ``` > **小提示** 若需手动将 `HttpResponseMessage` 对象转换为目标接收类型,建议使用 `IHttpContentConverterFactory` 内容转换器工厂,因为它内部默认调用了 `IObjectContentConverterFactory`。 --- # 6.11 自定义对象内容转换器(如序列化) > 原文链接:https://http.furion.net/docs/advanced-guide/custom-object-content-converter-eg-serialization/ `IObjectContentConverterFactory` 默认的内容转换器工厂会返回 `ObjectContentConverter` 实例,该实例利用 `HttpResponseMessage.Content` 的 `ReadFromJsonAsync` 方法,结合 `System.Text.Json` 序列化库,将 `HttpRequestMessage` 对象转换为目标接收类型。然而,这种转换方式在某些特殊类型(例如 `DataTable`)上可能会遇到反序列化失败的问题。为了应对这种情况,您可以自定义 `ObjectContentConverter`,以便选择或更换为更适合您需求的 `JSON` 序列化工具(例如 `Newtonsoft.Json`)。 > **框架建议** 但请注意,除非有充分的理由,否则通常建议使用 `System.Text.Json`,因为它与 `.NET Core` 紧密集成,且性能优异。 若需自定义这些 `JSON` 序列化选项,可通过以下方式进行调整: ```cs showLineNumbers {2-3,6,10-11,14} // 全局配置(适用于所有客户端) services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // 自定义 JSON 序列化行为,例如忽略空值 options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); // 客户端级配置(优先级更高) services.AddHttpClient("客户端名称") .ConfigureOptions(options => { // 自定义 JSON 序列化行为,例如忽略空值 options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); ``` **自定义时需同时提供泛型和非泛型版本:** ```cs showLineNumbers {2,5,11-12,15-16,19,24,28,32} // 非泛型版本 public class CustomObjectContentConverter : ObjectContentConverter { /// public override async Task ReadAsync(Type resultType, HttpContentConverterContext context, CancellationToken cancellationToken = default) { // 获取 HttpResponseMessage 实例 var httpResponseMessage = context.ResponseMessage; // 解析 HttpClient 客户端对应的 JSON 序列化上下文信息 var jsonSerializationContext = HttpRemoteUtility.ResolveJsonSerializationContext(resultType, httpResponseMessage, ServiceProvider); // 获取 JSON 反序列化的值(若需使用 Newtonsoft.Json 进行序列化或反序列化操作,请将以下代码替换为 Newtonsoft.Json 库的相应方法调用) ✅✅✅ var deserializedValue = await httpResponseMessage.Content.ReadFromJsonAsync(jsonSerializationContext.ResultType, jsonSerializationContext.JsonSerializerOptions, cancellationToken); // 获取转换的目标类型值 return jsonSerializationContext.GetResultValue(deserializedValue, httpResponseMessage); } } // 泛型版本 public class CustomObjectContentConverter : CustomObjectContentConverter, IHttpContentConverter { /// public virtual TResult? Read(HttpContentConverterContext context, CancellationToken cancellationToken = default) => (TResult?)base.Read(typeof(TResult), context, cancellationToken); /// public virtual async Task ReadAsync(HttpContentConverterContext context, CancellationToken cancellationToken = default) => (TResult?)await base.ReadAsync(typeof(TResult), context, cancellationToken); } ``` 接着,创建自定义的 `IObjectContentConverterFactory` 实现: ```cs showLineNumbers {1,4,12,16,24} public sealed class CustomObjectContentConverterFactory : IObjectContentConverterFactory { /// public IHttpContentConverter GetConverter(HttpContentConverterContext context) { // 检查 HTTP 响应的内容类型是否为 XML 媒体类型 if (context.ResponseMessage.IsXmlContent()) { return new XmlObjectContentConverter(); } return new CustomObjectContentConverter(); } /// public IHttpContentConverter GetConverter(Type resultType, HttpContentConverterContext context) { // 检查 HTTP 响应的内容类型是否为 XML 媒体类型 if (context.ResponseMessage.IsXmlContent()) { return new XmlObjectContentConverter(); } return new CustomObjectContentConverter(); } } ``` 最后,在 `Startup.cs` 或 `Program.cs` 文件中,配置并注册 `HttpRemote` 服务,以替换默认的对象内容转换器工厂功能: ```cs showLineNumbers {1,3} services.AddHttpRemote(builder => { builder.UseObjectContentConverterFactory(); }); ``` 这样,通过自定义的 `ObjectContentConverter` 和 `IObjectContentConverterFactory` 工厂,可以确保在反序列化时使用指定的 `JSON` 选项,避免潜在的反序列化问题。 > **`ObjectContentConverter` 和 `ObjectContentConverter` 基类说明** `ObjectContentConverter` 和 `ObjectContentConverter` 基类内置了一个 `ServiceProvider` 属性,该属性允许您轻松解析并获取通过依赖注入(`DI`)注册的服务。 --- # 6.12 自定义内容转换器 > 原文链接:https://http.furion.net/docs/advanced-guide/custom-content-converter/ 在特定场景下,当框架内置的 `IHttpContentConverter` 内容转换器无法满足需求时,可以通过自定义 `IHttpContentConverter` 内容转换器来解决。 例如,为 `Span` 类型添加内容转换器,可以通过实现 `IHttpContentConverter` 接口来实现自定义需求。 ```cs showLineNumbers {1,5,10-11} public class SpanCharContentConverter : HttpContentConverterBase> { /// public override byte[]? Read(HttpContentConverterContext context, CancellationToken cancellationToken = default) => AsyncUtility.RunSync(() => ReadAsync(context, cancellationToken)); /// public override async Task?> ReadAsync(HttpContentConverterContext context, CancellationToken cancellationToken = default) { var str = await context.ResponseMessage.Content.ReadAsStringAsync(cancellationToken); return str.AsSpan(); } } ``` 接下来,可以通过以下两种方式应用自定义内容转换器器: - **单次请求设置**: ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net/") .AddHttpContentConverters(() => [ new SpanCharContentConverter() ]); ``` - **全局配置**: 在 `Startup.cs` 或 `Program.cs` 文件中,配置并注册 `HttpRemote` 服务,以启用自定义内容转换器功能: ```cs showLineNumbers {1,3} services.AddHttpRemote(builder => { builder.AddHttpContentConverters(() => [ new SpanCharContentConverter() ]); }); ``` 以下示例展示了如何在发送 `HTTP` 远程请求时使用 `SpanCharContentConverter` 内容转换器: 使用 `IHttpRemoteService` 方式: ```cs showLineNumbers {2-3,6} // 单次请求添加 var span = await httpRemoteService.SendAsAsync>(HttpRequestBuilder.Post("https://furion.net/") .AddHttpContentConverters(() => [ new SpanCharContentConverter() ])); // 全局配置 var span = await httpRemoteService.GetAsAsync>("https://furion.net/"); ``` 使用 `IHttpContentConverterFactory` 方式: ```cs showLineNumbers {1,11-12} public class YourService(IHttpContentConverterFactory httpContentConverterFactory) // .NET8+ 支持主构造函数注入 { public async Task?> GetSpanAsync() { var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "https://furion.net/"); using var httpClient = new HttpClient(); var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage); // 调用 ReadAsync 方法将 HttpResponseMessage 对象转换为目标类型实例 var context = new HttpContentConverterContext(httpResponseMessage); return await httpContentConverterFactory.GetConverter>(context).ReadAsync(context); } } ``` > **`HttpContentConverterBase` 基类说明** `HttpContentConverterBase` 基类内置了一个 `ServiceProvider` 属性,该属性允许您轻松解析并获取通过依赖注入(`DI`)注册的服务。 --- # 6.13 自定义泛型内容转换器 > 原文链接:https://http.furion.net/docs/advanced-guide/custom-generic-content-converter/ 除了具体类型,框架同样支持泛型内容的转换。例如,定义如下泛型转换器: ```cs showLineNumbers {1,6,12} public class YourGenericClassContentConverter : HttpContentConverterBase> { /// public override YourGenericClass? Read(HttpContentConverterContext context, CancellationToken cancellationToken = default) { // 实现同步转换逻辑 } /// public override Task?> ReadAsync(HttpContentConverterContext context, CancellationToken cancellationToken = default) { // 实现异步转换逻辑 } } ``` 接下来在 `Startup.cs` 或 `Program.cs` 文件中,配置并注册 `HttpRemote` 服务,以启用自定义泛型内容转换器功能: ```cs showLineNumbers {3} services.AddHttpRemote(builder => { builder.AddGenericHttpContentConverters(() => [ new(typeof(YourGenericClass<>), typeArgs => (IHttpContentConverter)Activator.CreateInstance(typeof(YourGenericClassContentConverter<>).MakeGenericType(typeArgs[0]))!) ]); }); ``` 以下示例展示了如何在发送 `HTTP` 远程请求时使用 `YourGenericClassContentConverter` 内容转换器: 使用 `IHttpRemoteService` 方式: ```cs showLineNumbers {2-3,6} var str = await httpRemoteService.GetAsAsync>("https://furion.net/"); ``` 使用 `IHttpContentConverterFactory` 方式: ```cs showLineNumbers {1,11-12} public class YourService(IHttpContentConverterFactory httpContentConverterFactory) // .NET8+ 支持主构造函数注入 { public async Task?> GetStringAsync() { var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "https://furion.net/"); using var httpClient = new HttpClient(); var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage); // 调用 ReadAsync 方法将 HttpResponseMessage 对象转换为目标类型实例 var context = new HttpContentConverterContext(httpResponseMessage); return await httpContentConverterFactory.GetConverter>(context).ReadAsync(context); } } ``` > **`IHttpContentConverter` 相关知识扩展** 要了解更多关于自定义 `IHttpContentConverter` 内容处理器的信息,请访问 [`HttpAgent` 官方仓库](https://gitee.com/dotnetchina/HttpAgent/tree/master/src/HttpAgent/src/Converters) 进行查阅。 --- # 6.14 IHttpRemoteService 服务 > 原文链接:https://http.furion.net/docs/advanced-guide/ihttpremoteservice-service/ `IHttpRemoteService` 是一个用于发送 `HTTP` 远程请求的入口服务,它构成了 `HTTP` 远程请求模块的核心。简而言之,当需要发送 `HTTP` 远程请求时,应使用已注入的 `IHttpRemoteService` 服务。该服务默认以单例模式注册,因此可以在任何生存周期的服务中安全地使用。 在使用 `IHttpRemoteService` 服务前,需在 `Startup.cs` 或 `Program.cs` 文件中注册并配置 `HttpRemote` 服务。 ```cs showLineNumbers {2,5} // 在 Startup.cs 中注册: services.AddHttpRemote(); // 在 Program.cs 中,注册方式如下: // builder.Services.AddHttpRemote(); ``` > **解决 `AddHttpRemote` 二义性错误** 若遇到 `AddHttpRemote` 方法的二义性错误,可通过为其添加一个空的委托参数来解决,示例如下: ```cs showLineNumbers services.AddHttpRemote(builder => {}); ``` 随后,在您的服务、控制器或任何支持依赖注入的类中,注入 `IHttpRemoteService` 服务。 ```cs showLineNumbers {3,5} public class YourService { private readonly IHttpRemoteService _httpRemoteService; public YourService(IHttpRemoteService httpRemoteService) { _httpRemoteService = httpRemoteService; } } ``` 若您使用的是 `.NET 8` 及以上版本时,可通过[主构造函数](https://learn.microsoft.com/zh-cn/dotnet/csharp/whats-new/tutorials/primary-constructors)注入简化代码: ```cs showLineNumbers {1} public class YourService(IHttpRemoteService httpRemoteService) { // 使用 httpRemoteService 变量 } ``` 或者,您也可以在特定方法中按需注入: ```cs showLineNumbers {3} public class YourService { public Task GetResource([FromServices] IHttpRemoteService httpRemoteService) { // 您的代码逻辑 } } ``` > **在没有依赖注入的环境中的使用说明** 在 `.NET Core` 中,推荐使用依赖注入控制反转的方式来构建应用项目。因此,建议尽可能采用依赖注入的方式构建您的应用。然而,在某些特殊场景(如静态类中),依赖注入可能无法直接使用。此时,您可以采用以下方式获取服务: ```cs showLineNumbers var httpRemoteService = App.GetRequiredService(); ``` **请注意,这种方式应作为依赖注入的补充,而非替代。在可能的情况下,仍应优先考虑使用依赖注入来构建和管理应用中的服务。** --- # 6.15 HttpRemoteBuilder 构建器 > 原文链接:https://http.furion.net/docs/advanced-guide/httpremotebuilder-builder/ `HttpRemoteBuilder` 是一个构建器,用于配置和构建 `IHttpRemoteService` 服务所需的所有设置。在应用启动时,通常通过调用 `services.AddHttpRemote` 方法来指定这些配置。 以下展示了 `HttpRemoteBuilder` 提供的所有配置功能: ```cs showLineNumbers {1,4,7,9,12-13,16-17,20,23,26,29,32-33,36-37} services.AddHttpRemote(builder => { // 添加自定义内容处理器 builder.AddHttpContentProcessors(() => [ new CustomStringContentProcessor() ]); // 添加自定义内容转换器 builder.AddHttpContentConverters(() => [ new SpanCharContentConverter() ]); // 添加自定义泛型内容转换器 builder.AddGenericHttpContentConverters(() => [ new(typeof(IAsyncEnumerable<>), typeArgs => (IHttpContentConverter)Activator.CreateInstance(typeof(AsyncEnumerableContentConverter<>).MakeGenericType(typeArgs[0]))!) ]); // 设置自定义对象内容转换器工厂 builder.UseObjectContentConverterFactory(); builder.UseObjectContentConverterFactory(typeof(CustomObjectContentConverterFactory)); // 添加 HTTP 声明式服务 builder.AddHttpDeclarative(); builder.AddHttpDeclarative(typeof(IHttpService)); // 通过 requireIHttpDeclarative 参数,支持注册无需实现 IHttpDeclarative 接口的声明式代理 // 批量添加 HTTP 声明式服务 builder.AddHttpDeclaratives([typeof(IHttpService), typeof(IHttpService2), ...]); // 扫描程序集批量添加 HTTP 声明式服务 builder.AddHttpDeclarativesFromAssemblies([ assembly1, assembly2, ... ]); // 若使用 Furion 框架可直接设置 App.Assemblies // 添加自定义 HTTP 声明式提取器 builder.AddHttpDeclarativeExtractors(() => [ new AcceptDeclarativeExtractor() ]); // 扫描程序集批量添加 HTTP 声明式提取器 builder.AddHttpDeclarativeExtractorsFromAssemblies([ assembly1, assembly2, ... ]); // 若使用 Furion 框架可直接设置 App.Assemblies // 添加 HTTP 请求管道处理器服务 builder.AddPipelineHandler(); builder.AddPipelineHandler(typeof(CustomHttpRequestPipelineHandler)); // 设置自定义日志服务,可通过继承 HttpRemoteLoggerBase 实现 builder.UseLogger(); builder.UseLogger(typeof(CustomHttpRemoteLogger)); }); ``` --- # 6.16 HttpRemoteOptions 配置选项 > 原文链接:https://http.furion.net/docs/advanced-guide/httpremoteoptions-configuration-options/ 使用 `services.AddHttpRemote()` 方法添加 `HTTP` 远程请求服务时,会返回一个 `IHttpRemoteBuilder` 实例。通过该实例,可以访问并配置 `HttpRemoteOptions`,这些配置包括默认请求内容类型、`JSON` 序列化设置等属性: ```cs showLineNumbers {2,5,8,11,14,17,20,23,26,29,32,35} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // 配置默认的请求内容类型 options.DefaultContentType = "text/plain"; // 推荐配置为 "application/json" // 设置文件下载的默认保存路径 options.DefaultFileDownloadDirectory = @"C:\Workspaces\"; // 设置请求分析工具日志级别,默认 Warning options.ProfilerLogLevel = LogLevel.Warning; // 设置指示请求是否应遵循重定向响应,默认 true options.AllowAutoRedirect = true; // 设置请求所遵循的最大重定向数,默认 50 次 options.MaximumAutomaticRedirections = 50; // 设置回退请求基地址,当未配置 HttpClient 的 BaseAddress 且请求地址为相对地址时有效 options.FallbackBaseAddress = new Uri("https://localhost:5000"); // 自定义 JSON 序列化选项 options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; // 设置用于替换 URL 地址中配置模板参数的提供源 options.Configuration = builder.Configuration; // 若使用 Furion 框架可直接设置 App.Configuration // 设置 URL 参数格式化程序 options.UrlParameterFormatter = new UrlParameterFormatter(); // 日志服务或控制台输出不可用时的备用日志输出委托 options.FallbackLogger = Console.WriteLine; // 可替换为 Debug.WriteLine // 设置 HttpRequestBuilder 统一配置器 options.RequestBuilderConfigurator = null; // 默认为 null }); ``` `ConfigureOptions` 方法允许对 `HTTP` 远程请求服务进行更多自定义配置,例如调整 `JSON` 序列化行为等。此外,`ConfigureOptions` 还提供了支持服务解析的重载方法。示例如下: ```cs showLineNumbers {2,5} services.AddHttpRemote(builder => {}) .ConfigureOptions((options, serviceProvider) => { // 解析所需服务 var yourService = serviceProvider.GetRequiredService(); // 其他配置代码 }); ``` --- # 6.17 统一配置 HttpClient 客户端 > 原文链接:https://http.furion.net/docs/advanced-guide/unified-configuration-of-the-httpclient-client/ 在应用项目开发中,通常需要对所有的 `HttpClient` 客户端实例进行统一配置。为此,框架提供了 `ConfigureHttpClientDefaults` 方法,支持一键配置: ```cs showLineNumbers {1,3,8,10} services.ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()); }); // 或者使用 IHttpRemoteBuilder 扩展方法进行一键配置 services.AddHttpRemote() .ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()); }); ``` 通过这种方式,可以轻松地为所有 `HttpClient` 实例设置默认的 `HttpMessageHandler`,确保配置的一致性和可维护性。 --- # 6.18 内置属性与方法 > 原文链接:https://http.furion.net/docs/advanced-guide/http-remote-service-members/ `IHttpRemoteService` 服务类型包含多个属性以及丰富多样的方法。 ### 内置属性 ```cs showLineNumbers {2,5} // 获取 HTTP 远程请求选项,返回值类型为 HttpRemoteOptions var remoteOptions = httpRemoteService.RemoteOptions; // 获取 IServiceProvider 接口实例 var serviceProvider = httpRemoteService.ServiceProvider; ``` ### 内置方法 - **核心方法**: ```cs showLineNumbers {1,7,13,19,25,31,37} // 返回 HttpResponseMessage 对象 httpRemoteService.Send(httpRequestBuilder, cancellationToken); httpRemoteService.Send(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.SendAsync(httpRequestBuilder, cancellationToken); await httpRemoteService.SendAsync(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); // 返回目标 T 类型 httpRemoteService.SendAs(httpRequestBuilder, cancellationToken); httpRemoteService.SendAs(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.SendAsAsync(httpRequestBuilder, cancellationToken); await httpRemoteService.SendAsAsync(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); // 返回 HttpRemoteResult httpRemoteService.Send(httpRequestBuilder, cancellationToken); httpRemoteService.Send(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.SendAsync(httpRequestBuilder, cancellationToken); await httpRemoteService.SendAsync(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); // 返回 object 类型,实际类型为 resultType httpRemoteService.SendAs(resultType, httpRequestBuilder, cancellationToken); httpRemoteService.SendAs(resultType, httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.SendAsAsync(resultType, httpRequestBuilder, cancellationToken); await httpRemoteService.SendAsAsync(resultType, httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); // 返回字符串类型 httpRemoteService.SendAsString(httpRequestBuilder, cancellationToken); httpRemoteService.SendAsString(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.SendAsStringAsync(httpRequestBuilder, cancellationToken); await httpRemoteService.SendAsStringAsync(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); // 返回字节数组类型 httpRemoteService.SendAsByteArray(httpRequestBuilder, cancellationToken); httpRemoteService.SendAsByteArray(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.SendAsByteArrayAsync(httpRequestBuilder, cancellationToken); await httpRemoteService.SendAsByteArrayAsync(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); // 返回 Stream 类型 httpRemoteService.SendAsStream(httpRequestBuilder, cancellationToken); httpRemoteService.SendAsStream(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.SendAsStreamAsync(httpRequestBuilder, cancellationToken); await httpRemoteService.SendAsStreamAsync(httpRequestBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); ``` - **请求谓词方法**: ```cs showLineNumbers {1,39,77,115,153,191,229,267,305} // ============ GET ============ // 返回 HttpResponseMessage 对象 httpRemoteService.Get(requestUri, configure, cancellationToken); httpRemoteService.Get(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.GetAsync(requestUri, configure, cancellationToken); await httpRemoteService.GetAsync(requestUri, completionOption, configure, cancellationToken); // 返回目标 T 类型 httpRemoteService.GetAs(requestUri, configure, cancellationToken); httpRemoteService.GetAs(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.GetAsAsync(requestUri, configure, cancellationToken); await httpRemoteService.GetAsAsync(requestUri, completionOption, configure, cancellationToken); // 返回 HttpRemoteResult httpRemoteService.Get(requestUri, configure, cancellationToken); httpRemoteService.Get(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.GetAsync(requestUri, configure, cancellationToken); await httpRemoteService.GetAsync(requestUri, completionOption, configure, cancellationToken); // 返回字符串类型 httpRemoteService.GetAsString(requestUri, configure, cancellationToken); httpRemoteService.GetAsString(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.GetAsStringAsync(requestUri, configure, cancellationToken); await httpRemoteService.GetAsStringAsync(requestUri, completionOption, configure, cancellationToken); // 返回字节数组类型 httpRemoteService.GetAsByteArray(requestUri, configure, cancellationToken); httpRemoteService.GetAsByteArray(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.GetAsByteArrayAsync(requestUri, configure, cancellationToken); await httpRemoteService.GetAsByteArrayAsync(requestUri, completionOption, configure, cancellationToken); // 返回 Stream 类型 httpRemoteService.GetAsStream(requestUri, configure, cancellationToken); httpRemoteService.GetAsStream(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.GetAsStreamAsync(requestUri, configure, cancellationToken); await httpRemoteService.GetAsStreamAsync(requestUri, completionOption, configure, cancellationToken); // ============ PUT ============ // 返回 HttpResponseMessage 对象 httpRemoteService.Put(requestUri, configure, cancellationToken); httpRemoteService.Put(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PutAsync(requestUri, configure, cancellationToken); await httpRemoteService.PutAsync(requestUri, completionOption, configure, cancellationToken); // 返回目标 T 类型 httpRemoteService.PutAs(requestUri, configure, cancellationToken); httpRemoteService.PutAs(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PutAsAsync(requestUri, configure, cancellationToken); await httpRemoteService.PutAsAsync(requestUri, completionOption, configure, cancellationToken); // 返回 HttpRemoteResult httpRemoteService.Put(requestUri, configure, cancellationToken); httpRemoteService.Put(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PutAsync(requestUri, configure, cancellationToken); await httpRemoteService.PutAsync(requestUri, completionOption, configure, cancellationToken); // 返回字符串类型 httpRemoteService.PutAsString(requestUri, configure, cancellationToken); httpRemoteService.PutAsString(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PutAsStringAsync(requestUri, configure, cancellationToken); await httpRemoteService.PutAsStringAsync(requestUri, completionOption, configure, cancellationToken); // 返回字节数组类型 httpRemoteService.PutAsByteArray(requestUri, configure, cancellationToken); httpRemoteService.PutAsByteArray(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PutAsByteArrayAsync(requestUri, configure, cancellationToken); await httpRemoteService.PutAsByteArrayAsync(requestUri, completionOption, configure, cancellationToken); // 返回 Stream 类型 httpRemoteService.PutAsStream(requestUri, configure, cancellationToken); httpRemoteService.PutAsStream(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PutAsStreamAsync(requestUri, configure, cancellationToken); await httpRemoteService.PutAsStreamAsync(requestUri, completionOption, configure, cancellationToken); // ============ POST ============ // 返回 HttpResponseMessage 对象 httpRemoteService.Post(requestUri, configure, cancellationToken); httpRemoteService.Post(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PostAsync(requestUri, configure, cancellationToken); await httpRemoteService.PostAsync(requestUri, completionOption, configure, cancellationToken); // 返回目标 T 类型 httpRemoteService.PostAs(requestUri, configure, cancellationToken); httpRemoteService.PostAs(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PostAsAsync(requestUri, configure, cancellationToken); await httpRemoteService.PostAsAsync(requestUri, completionOption, configure, cancellationToken); // 返回 HttpRemoteResult httpRemoteService.Post(requestUri, configure, cancellationToken); httpRemoteService.Post(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PostAsync(requestUri, configure, cancellationToken); await httpRemoteService.PostAsync(requestUri, completionOption, configure, cancellationToken); // 返回字符串类型 httpRemoteService.PostAsString(requestUri, configure, cancellationToken); httpRemoteService.PostAsString(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PostAsStringAsync(requestUri, configure, cancellationToken); await httpRemoteService.PostAsStringAsync(requestUri, completionOption, configure, cancellationToken); // 返回字节数组类型 httpRemoteService.PostAsByteArray(requestUri, configure, cancellationToken); httpRemoteService.PostAsByteArray(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PostAsByteArrayAsync(requestUri, configure, cancellationToken); await httpRemoteService.PostAsByteArrayAsync(requestUri, completionOption, configure, cancellationToken); // 返回 Stream 类型 httpRemoteService.PostAsStream(requestUri, configure, cancellationToken); httpRemoteService.PostAsStream(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PostAsStreamAsync(requestUri, configure, cancellationToken); await httpRemoteService.PostAsStreamAsync(requestUri, completionOption, configure, cancellationToken); // ============ DELETE ============ // 返回 HttpResponseMessage 对象 httpRemoteService.Delete(requestUri, configure, cancellationToken); httpRemoteService.Delete(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.DeleteAsync(requestUri, configure, cancellationToken); await httpRemoteService.DeleteAsync(requestUri, completionOption, configure, cancellationToken); // 返回目标 T 类型 httpRemoteService.DeleteAs(requestUri, configure, cancellationToken); httpRemoteService.DeleteAs(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.DeleteAsAsync(requestUri, configure, cancellationToken); await httpRemoteService.DeleteAsAsync(requestUri, completionOption, configure, cancellationToken); // 返回 HttpRemoteResult httpRemoteService.Delete(requestUri, configure, cancellationToken); httpRemoteService.Delete(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.DeleteAsync(requestUri, configure, cancellationToken); await httpRemoteService.DeleteAsync(requestUri, completionOption, configure, cancellationToken); // 返回字符串类型 httpRemoteService.DeleteAsString(requestUri, configure, cancellationToken); httpRemoteService.DeleteAsString(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.DeleteAsStringAsync(requestUri, configure, cancellationToken); await httpRemoteService.DeleteAsStringAsync(requestUri, completionOption, configure, cancellationToken); // 返回字节数组类型 httpRemoteService.DeleteAsByteArray(requestUri, configure, cancellationToken); httpRemoteService.DeleteAsByteArray(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.DeleteAsByteArrayAsync(requestUri, configure, cancellationToken); await httpRemoteService.DeleteAsByteArrayAsync(requestUri, completionOption, configure, cancellationToken); // 返回 Stream 类型 httpRemoteService.DeleteAsStream(requestUri, configure, cancellationToken); httpRemoteService.DeleteAsStream(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.DeleteAsStreamAsync(requestUri, configure, cancellationToken); await httpRemoteService.DeleteAsStreamAsync(requestUri, completionOption, configure, cancellationToken); // ============ HEAD ============ // 返回 HttpResponseMessage 对象 httpRemoteService.Head(requestUri, configure, cancellationToken); httpRemoteService.Head(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.HeadAsync(requestUri, configure, cancellationToken); await httpRemoteService.HeadAsync(requestUri, completionOption, configure, cancellationToken); // 返回目标 T 类型 httpRemoteService.HeadAs(requestUri, configure, cancellationToken); httpRemoteService.HeadAs(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.HeadAsAsync(requestUri, configure, cancellationToken); await httpRemoteService.HeadAsAsync(requestUri, completionOption, configure, cancellationToken); // 返回 HttpRemoteResult httpRemoteService.Head(requestUri, configure, cancellationToken); httpRemoteService.Head(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.HeadAsync(requestUri, configure, cancellationToken); await httpRemoteService.HeadAsync(requestUri, completionOption, configure, cancellationToken); // 返回字符串类型 httpRemoteService.HeadAsString(requestUri, configure, cancellationToken); httpRemoteService.HeadAsString(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.HeadAsStringAsync(requestUri, configure, cancellationToken); await httpRemoteService.HeadAsStringAsync(requestUri, completionOption, configure, cancellationToken); // 返回字节数组类型 httpRemoteService.HeadAsByteArray(requestUri, configure, cancellationToken); httpRemoteService.HeadAsByteArray(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.HeadAsByteArrayAsync(requestUri, configure, cancellationToken); await httpRemoteService.HeadAsByteArrayAsync(requestUri, completionOption, configure, cancellationToken); // 返回 Stream 类型 httpRemoteService.HeadAsStream(requestUri, configure, cancellationToken); httpRemoteService.HeadAsStream(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.HeadAsStreamAsync(requestUri, configure, cancellationToken); await httpRemoteService.HeadAsStreamAsync(requestUri, completionOption, configure, cancellationToken); // ============ OPTIONS ============ // 返回 HttpResponseMessage 对象 httpRemoteService.Options(requestUri, configure, cancellationToken); httpRemoteService.Options(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.OptionsAsync(requestUri, configure, cancellationToken); await httpRemoteService.OptionsAsync(requestUri, completionOption, configure, cancellationToken); // 返回目标 T 类型 httpRemoteService.OptionsAs(requestUri, configure, cancellationToken); httpRemoteService.OptionsAs(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.OptionsAsAsync(requestUri, configure, cancellationToken); await httpRemoteService.OptionsAsAsync(requestUri, completionOption, configure, cancellationToken); // 返回 HttpRemoteResult httpRemoteService.Options(requestUri, configure, cancellationToken); httpRemoteService.Options(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.OptionsAsync(requestUri, configure, cancellationToken); await httpRemoteService.OptionsAsync(requestUri, completionOption, configure, cancellationToken); // 返回字符串类型 httpRemoteService.OptionsAsString(requestUri, configure, cancellationToken); httpRemoteService.OptionsAsString(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.OptionsAsStringAsync(requestUri, configure, cancellationToken); await httpRemoteService.OptionsAsStringAsync(requestUri, completionOption, configure, cancellationToken); // 返回字节数组类型 httpRemoteService.OptionsAsByteArray(requestUri, configure, cancellationToken); httpRemoteService.OptionsAsByteArray(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.OptionsAsByteArrayAsync(requestUri, configure, cancellationToken); await httpRemoteService.OptionsAsByteArrayAsync(requestUri, completionOption, configure, cancellationToken); // 返回 Stream 类型 httpRemoteService.OptionsAsStream(requestUri, configure, cancellationToken); httpRemoteService.OptionsAsStream(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.OptionsAsStreamAsync(requestUri, configure, cancellationToken); await httpRemoteService.OptionsAsStreamAsync(requestUri, completionOption, configure, cancellationToken); // ============ TRACE ============ // 返回 HttpResponseMessage 对象 httpRemoteService.Trace(requestUri, configure, cancellationToken); httpRemoteService.Trace(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.TraceAsync(requestUri, configure, cancellationToken); await httpRemoteService.TraceAsync(requestUri, completionOption, configure, cancellationToken); // 返回目标 T 类型 httpRemoteService.TraceAs(requestUri, configure, cancellationToken); httpRemoteService.TraceAs(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.TraceAsAsync(requestUri, configure, cancellationToken); await httpRemoteService.TraceAsAsync(requestUri, completionOption, configure, cancellationToken); // 返回 HttpRemoteResult httpRemoteService.Trace(requestUri, configure, cancellationToken); httpRemoteService.Trace(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.TraceAsync(requestUri, configure, cancellationToken); await httpRemoteService.TraceAsync(requestUri, completionOption, configure, cancellationToken); // 返回字符串类型 httpRemoteService.TraceAsString(requestUri, configure, cancellationToken); httpRemoteService.TraceAsString(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.TraceAsStringAsync(requestUri, configure, cancellationToken); await httpRemoteService.TraceAsStringAsync(requestUri, completionOption, configure, cancellationToken); // 返回字节数组类型 httpRemoteService.TraceAsByteArray(requestUri, configure, cancellationToken); httpRemoteService.TraceAsByteArray(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.TraceAsByteArrayAsync(requestUri, configure, cancellationToken); await httpRemoteService.TraceAsByteArrayAsync(requestUri, completionOption, configure, cancellationToken); // 返回 Stream 类型 httpRemoteService.TraceAsStream(requestUri, configure, cancellationToken); httpRemoteService.TraceAsStream(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.TraceAsStreamAsync(requestUri, configure, cancellationToken); await httpRemoteService.TraceAsStreamAsync(requestUri, completionOption, configure, cancellationToken); // ============ PATCH ============ // 返回 HttpResponseMessage 对象 httpRemoteService.Patch(requestUri, configure, cancellationToken); httpRemoteService.Patch(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PatchAsync(requestUri, configure, cancellationToken); await httpRemoteService.PatchAsync(requestUri, completionOption, configure, cancellationToken); // 返回目标 T 类型 httpRemoteService.PatchAs(requestUri, configure, cancellationToken); httpRemoteService.PatchAs(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PatchAsAsync(requestUri, configure, cancellationToken); await httpRemoteService.PatchAsAsync(requestUri, completionOption, configure, cancellationToken); // 返回 HttpRemoteResult httpRemoteService.Patch(requestUri, configure, cancellationToken); httpRemoteService.Patch(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PatchAsync(requestUri, configure, cancellationToken); await httpRemoteService.PatchAsync(requestUri, completionOption, configure, cancellationToken); // 返回字符串类型 httpRemoteService.PatchAsString(requestUri, configure, cancellationToken); httpRemoteService.PatchAsString(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PatchAsStringAsync(requestUri, configure, cancellationToken); await httpRemoteService.PatchAsStringAsync(requestUri, completionOption, configure, cancellationToken); // 返回字节数组类型 httpRemoteService.PatchAsByteArray(requestUri, configure, cancellationToken); httpRemoteService.PatchAsByteArray(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PatchAsByteArrayAsync(requestUri, configure, cancellationToken); await httpRemoteService.PatchAsByteArrayAsync(requestUri, completionOption, configure, cancellationToken); // 返回 Stream 类型 httpRemoteService.PatchAsStream(requestUri, configure, cancellationToken); httpRemoteService.PatchAsStream(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.PatchAsStreamAsync(requestUri, configure, cancellationToken); await httpRemoteService.PatchAsStreamAsync(requestUri, completionOption, configure, cancellationToken); // ============ QUERY ============ // 返回 HttpResponseMessage 对象 httpRemoteService.Query(requestUri, configure, cancellationToken); httpRemoteService.Query(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.QueryAsync(requestUri, configure, cancellationToken); await httpRemoteService.QueryAsync(requestUri, completionOption, configure, cancellationToken); // 返回目标 T 类型 httpRemoteService.QueryAs(requestUri, configure, cancellationToken); httpRemoteService.QueryAs(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.QueryAsAsync(requestUri, configure, cancellationToken); await httpRemoteService.QueryAsAsync(requestUri, completionOption, configure, cancellationToken); // 返回 HttpRemoteResult httpRemoteService.Query(requestUri, configure, cancellationToken); httpRemoteService.Query(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.QueryAsync(requestUri, configure, cancellationToken); await httpRemoteService.QueryAsync(requestUri, completionOption, configure, cancellationToken); // 返回字符串类型 httpRemoteService.QueryAsString(requestUri, configure, cancellationToken); httpRemoteService.QueryAsString(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.QueryAsStringAsync(requestUri, configure, cancellationToken); await httpRemoteService.QueryAsStringAsync(requestUri, completionOption, configure, cancellationToken); // 返回字节数组类型 httpRemoteService.QueryAsByteArray(requestUri, configure, cancellationToken); httpRemoteService.QueryAsByteArray(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.QueryAsByteArrayAsync(requestUri, configure, cancellationToken); await httpRemoteService.QueryAsByteArrayAsync(requestUri, completionOption, configure, cancellationToken); // 返回 Stream 类型 httpRemoteService.QueryAsStream(requestUri, configure, cancellationToken); httpRemoteService.QueryAsStream(requestUri, completionOption, configure, cancellationToken); await httpRemoteService.QueryAsStreamAsync(requestUri, configure, cancellationToken); await httpRemoteService.QueryAsStreamAsync(requestUri, completionOption, configure, cancellationToken); ``` ### 特定功能方法 ```cs showLineNumbers {1,10,19,26,33,40} // 下载文件 httpRemoteService.DownloadFile(requestUri, destinationPath, onProgressChanged, fileExistsBehavior, configure, cancellationToken); await httpRemoteService.DownloadFileAsync(requestUri, destinationPath, onProgressChanged, fileExistsBehavior, configure, cancellationToken); httpRemoteService.DownloadFileWithConsoleProgress(requestUri, destinationPath, fileExistsBehavior, configure, cancellationToken); await httpRemoteService.DownloadFileWithConsoleProgressAsync(requestUri, destinationPath, fileExistsBehavior, configure, cancellationToken); httpRemoteService.Send(httpFileDownloadBuilder, cancellationToken); await httpRemoteService.SendAsync(httpFileDownloadBuilder, cancellationToken); // 上传文件 httpRemoteService.UploadFile(requestUri, filePath, name, onProgressChanged, fileName, configure, cancellationToken); await httpRemoteService.UploadFileAsync(requestUri, filePath, name, onProgressChanged, fileName, configure, cancellationToken); httpRemoteService.UploadFileWithConsoleProgress(requestUri, filePath, name, fileName, configure, cancellationToken); await httpRemoteService.UploadFileWithConsoleProgressAsync(requestUri, filePath, name, fileName, configure, cancellationToken); httpRemoteService.Send(httpFileUploadBuilder, cancellationToken); await httpRemoteService.SendAsync(httpFileUploadBuilder, cancellationToken); // 发送 Server-Sent Events 请求 httpRemoteService.ServerSentEvents(requestUri, onMessage, configure, cancellationToken); await httpRemoteService.ServerSentEventsAsync(requestUri, onMessage, configure, cancellationToken); httpRemoteService.Send(httpServerSentEventsBuilder, cancellationToken); await httpRemoteService.SendAsync(httpServerSentEventsBuilder, cancellationToken); // 压力测试 httpRemoteService.StressTestHarness(requestUri, numberOfRequests, configure, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.StressTestHarnessAsync(requestUri, numberOfRequests, configure, HttpCompletionOption.ResponseContentRead, cancellationToken); httpRemoteService.Send(httpStressTestHarnessBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.SendAsync(httpStressTestHarnessBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); // 发送长轮询请求 httpRemoteService.LongPolling(requestUri, onDataReceived, configure, cancellationToken); await httpRemoteService.LongPollingAsync(requestUri, onDataReceived, configure, cancellationToken); httpRemoteService.Send(httpLongPollingBuilder, cancellationToken); await httpRemoteService.SendAsync(httpLongPollingBuilder, cancellationToken); // 发送 HTTP 声明式请求 httpRemoteService.Declarative(method, args); await httpRemoteService.DeclarativeAsync(method, args); httpRemoteService.SendAs(httpDeclarativeBuilder); await httpRemoteService.SendAsAsync(httpDeclarativeBuilder); ``` --- # 6.19 添加 IHttpRemoteService 扩展 > 原文链接:https://http.furion.net/docs/advanced-guide/http-remote-service-extensions/ 除了系统自带的 `IHttpRemoteService` 方法,您还可以为其添加自定义扩展方法,以简化代码并减少重复。例如,您可以添加一个 `SendAsSpan` 方法,用于发送 `HTTP` 远程请求返回 `Span`。具体实现如下: ```cs showLineNumbers {1,3,13} public static class HttpRemoteServiceExtensions { public static Span SendAsSpan(this IHttpRemoteService httpRemoteService, HttpRequestBuilder httpRequestBuilder, CancellationToken cancellationToken = default) { // 空检查 ArgumentNullException.ThrowIfNull(httpRequestBuilder); var str = httpRemoteService.SendAsString(httpRequestBuilder, cancellationToken); return str.AsSpan(); } public static async Task> SendAsSpanAsync(this IHttpRemoteService httpRemoteService, HttpRequestBuilder httpRequestBuilder, CancellationToken cancellationToken = default) { // 空检查 ArgumentNullException.ThrowIfNull(httpRequestBuilder); var str = await httpRemoteService.SendAsStringAsync(httpRequestBuilder, cancellationToken); return str.AsSpan(); } } ``` 之后,您可以轻松地在 `IHttpRemoteService` 实例中使用此方法: ```cs showLineNumbers httpRemoteService.SendAsSpan(HttpRequestBuilder.Get("https://furion.net")); await httpRemoteService.SendAsSpanAsync(HttpRequestBuilder.Get("https://furion.net")); ``` 利用 `C#` 扩展方法的特性,您可以极大地丰富 `IHttpRemoteService` 的功能,减少重复代码,同时提高代码的可读性和可维护性。 --- # 6.20 HttpRemoteResult 返回值 > 原文链接:https://http.furion.net/docs/advanced-guide/http-remote-result/ `HttpRemoteResult` 是一个泛型类型,专门用于 `HTTP` 远程请求模块中的响应内容。泛型参数 `TResult` 代表最终需要转换成的数据类型,除了支持常见的 `HTTP` 响应类型如 `string`、`byte[]`、`Stream`、`HttpResponseMessage`、`IAsyncEnumerable` 和 `IActionResult`,还支持自定义类型和框架内置的 `VoidContent` 类型。该类型封装了常用的 `HTTP` 响应信息和请求耗时等功能。 在 `HTTP` 远程请求模块中,所有默认的不包含 `As` 关键字的泛型请求方法返回值均为 `HttpRemoteResult` 类型。以下是通过不同方式获取 `HttpRemoteResult` 类型返回值的示例: ```cs showLineNumbers {2,5} // 请求谓词方式 var httpResult = await httpRemoteService.GetAsync("https://furion.net/"); // 构建器方式 var httpResult = await httpRemoteService.SendAsync(HttpRequestBuilder.Get("https://furion.net/")); ``` `HttpRemoteResult` 包含以下属性和方法: - **属性**: - `ResponseMessage`:响应消息(`HttpResponseMessage` 类型)。 - `ContentType`:内容类型(`string` 类型)。 - `CharSet`:字符集(`string` 类型)。 - `ContentEncoding`:内容编码(`ICollection` 类型)。 - `ContentLength`:内容大小(`long` 类型)。 - `Server`:原始响应标头 `Server`(`HttpHeaderValueCollection` 类型)。 - `RawSetCookies`:原始响应标头 `Set-Cookie` 集合(`List` 类型)。 - `SetCookies`:响应 `Cookie` 集合(`IList` 类型)。 - `StatusCode`:响应状态码(`HttpStatusCode` 类型)。 - `IsSuccessStatusCode`:是否请求成功(`bool` 类型)。 - `Result`:目标数据(`TResult` 泛型类型)。 - `RequestDuration`:请求耗时(毫秒)(`long` 类型)。 - `Headers`:响应标头(`HttpResponseHeaders` 类型)。 - `ContentHeaders`:响应内容标头(`HttpContentHeaders` 类型)。 - `Version`:`HTTP` 版本(`Version` 类型)。 - `HttpClientName`:`HttpClient` 实例的配置名称(`string?` 类型)。 - **方法**: - `ToString()`:输出带缩进的详细请求和响应信息字符串。 > **返回值类型说明** 默认情况下,当返回值类型不是 `string`、`byte[]`、`Stream`、`HttpResponseMessage`、`VoidContent`、`IAsyncEnumerable` 和 `IActionResult` 时,其他类型将使用 `System.Text.Json` 进行反序列化处理。 如果需要更改此行为,可以在后续章节中了解如何实现 `IHttpContentConverter` 内容转换器接口进行自定义。 在最新版本中,框架为 `HttpRemoteResult` 类型引入了对[解构函数](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/deconstruction)的支持,通过解构表达式简化对象解析过程,使得获取关键属性值变得更加便捷。以下是示例代码: ```cs showLineNumbers // 解构表达式用于提取必需的属性值 var (result, response) = await httpRemoteService.GetAsync("https://furion.net/"); // 可调用 ThrowIfNull()/OrDefault() 解决空引用警告问题 var (result, response, isSuccess) = await httpRemoteService.GetAsync("https://furion.net/"); // 可调用 ThrowIfNull()/OrDefault() 解决空引用警告问题 var (result, response, isSuccess, statusCode) = await httpRemoteService.GetAsync("https://furion.net/"); // 可调用 ThrowIfNull()/OrDefault() 解决空引用警告问题 ``` 在这几个例子中,`result` 是 `TResult` 类型,`response` 是 `HttpResponseMessage` 类型,`isSuccess` 是 `bool` 类型,而 `statusCode` 则是 `HttpStatusCode` 类型。 通过使用解构表达式,不仅提升了代码的可读性,也让开发过程更加高效。这种改进允许开发者直接访问所需的数据,减少了手动获取各个属性值的步骤,从而使代码更简洁、直观。 --- 此外,`HttpRemoteResult` 类型还内置了一个 `ToString()` 方法,该方法能够以缩进格式清晰地打印出请求标头和响应标头的详细信息,如下所示: ```cs showLineNumbers Console.WriteLine(httpResult.ToString()); // 或使用 Console.WriteLine(httpResult); ``` 终端控制台输出如下: ```bash showLineNumbers Request Headers: User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0 traceparent: 00-602c9070b85da9bd73fc1eac36fdb3cb-14dded89e0f5266b-00 General: Request URL: https://furion.net/ Request Method: GET Status Code: 200 OK HTTP Version: 1.1 HTTP Content: Content Type: HttpClient Name: Request Duration (ms): 133.00 Response Headers: Server: nginx/1.22.1 Date: Mon, 18 Nov 2024 21:26:06 GMT Connection: keep-alive Vary: Accept-Encoding ETag: "67091697-f32f" Cache-Control: max-age=315360000 Accept-Ranges: bytes Content-Type: text/html Content-Length: 62255 Last-Modified: Fri, 11 Oct 2024 12:14:15 GMT Expires: Thu, 31 Dec 2037 23:55:55 GMT ``` --- # 6.21 下载网络资源 > 原文链接:https://http.furion.net/docs/advanced-guide/downloading-network-resources/ `HTTP` 远程请求最常见的应用场景之一是下载网络资源并将其保存到本地磁盘,这包括下载网页内容、图片、压缩包以及安装软件等。下载网络资源有多种方式,其中最常见的是发送 `HTTP` 请求,接收返回的 `Stream` 流,然后将其写入本地磁盘并保存为相应文件。 ## 使用常规接收 `Stream` 流方式进行下载 ```cs showLineNumbers {2,5-6} // 获取响应 Stream 流 var stream = await httpRemoteService.GetAsStreamAsync("https://furion.net/img/furionlogo.png"); // 创建文件流并写入 using var fileStream = new FileStream(@"C:\Workspaces\furionlogo.png", FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true); await contentStream.CopyToAsync(fileStream); ``` 然而,这种下载网络资源的方式在面对多种复杂场景时显得不够灵活,例如无法实时追踪下载进度、妥善处理文件已存在的情况,以及实现分片下载等。此外,它还可能需要开发者编写更多的额外代码。因此,框架中集成了专门设计用于下载网络资源的功能,以应对这些问题。 ## 利用框架内置的专用下载功能进行下载 以下示例展示了如何利用框架内置的下载功能来下载 `ASP.NET Core` 运行时: ```cs showLineNumbers {3} // 从指定 URL 下载 ASP.NET Core 运行时,并保存到 C:\Workspaces\ 目录中 // 如果未指定文件名,框架将自动从下载地址中解析出文件名,例如:aspnetcore-runtime-8.0.10-win-x64.exe var fileTransferResult = await httpRemoteService.DownloadFileAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\"); // 如需指定文件名可设置为 C:\Workspaces\aspnetcore-runtime.exe ``` > **下载文件保存路径说明** - 如果未指定下载文件的名称,框架将自动从下载地址中解析出文件名。 - 如果提供了自定义文件名,则该名称将被用于保存最终下载的文件。 - 此外,如果您仅提供了一个目标文件夹(目录)用于存放下载文件,请确保该文件夹(目录)路径以斜杠(`/`)结尾。 文件下载完成后,框架将返回一个 `FileTransferResult` 对象,包含以下属性: - `IsSuccess`:传输是否成功完成(`bool` 类型)。**注意:因文件存在而跳过也被视为成功。** - `RequestUri`:文件传输 `URL`(`string` 类型)。 - `FilePath`:文件的路径(`string` 类型)。 - `FileSize`:文件的大小(以字节为单位的 `long` 类型)。 - `ElapsedMilliseconds`:传输耗时(`以毫秒为单位的 `long` 类型)。 - `StatusCode`:响应状态(`HttpStatusCode` 类型)。 若本地文件已存在,将会抛出 `InvalidOperationException` 异常,`System.InvalidOperationException: The destination path 'C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe' already exists.`。此时,您可以通过 `fileExistsBehavior` 参数来指定文件存在时的行为: ```cs showLineNumbers {3} var fileTransferResult = await httpRemoteService.DownloadFileAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\" , fileExistsBehavior: FileExistsBehavior.Overwrite); // 若文件存在时则覆盖 ``` `FileExistsBehavior` 枚举包含以下选项: - `CreateNew`(默认值):若文件已存在,则抛出异常;否则,创建新文件。 - `Overwrite`:覆盖现有文件。 - `Skip`:保留现有文件,并跳过下载操作。 在下载文件时,您还可以获取实时的下载进度。以下示例展示了如何打印下载进度: ```cs showLineNumbers {3-6} var fileTransferResult = await httpRemoteService.DownloadFileAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\" , async progress => { Console.WriteLine(await progress.ToSummaryStringAsync()); // 输出简要进度字符串 } , fileExistsBehavior: FileExistsBehavior.Overwrite); ``` 下载进度的控制台输出示例(使用 `progress.ToSummaryString()`): ```bash showLineNumbers Transferred 0.26MB of 10.09MB (2.63% complete, Speed: 3.86MB/s, Time: 0.07s, ETA: 2.55s), File: aspnetcore-runtime-8.0.10-win-x64.exe, Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe. Transferred 10.09MB of 10.09MB (100.00% complete, Speed: 9.99MB/s, Time: 1.01s, ETA: 0.00s), File: aspnetcore-runtime-8.0.10-win-x64.exe, Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe. ``` 若需在控制台中实时显示文件下载进度,推荐使用 `UpdateConsoleProgressAsync()` 方法。示例如下: ```cs showLineNumbers {3,7} var fileTransferResult = await httpRemoteService.DownloadFileAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\" , progress => progress.UpdateConsoleProgressAsync() // 在控制台中更新文件传输进度条 , fileExistsBehavior: FileExistsBehavior.Overwrite); // ✅ 或使用 DownloadFileWithConsoleProgressAsync 方法(带控制台进度打印) var fileTransferResult = await httpRemoteService.DownloadFileWithConsoleProgressAsync("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe" , @"C:\Workspaces\" , fileExistsBehavior: FileExistsBehavior.Overwrite); ``` 执行后,控制台将显示如下进度信息: ```bash showLineNumbers {2} File: aspnetcore-runtime-8.0.10-win-x64.exe, Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe [############################## ] 61.35% (6.19MB/10.09MB) Speed: 5.81MB/s, Time: 1.07s, ETA: 0.67s. ``` 若使用 `progress.ToString()`,则控制台输出将包含更详细的进度信息: ```bash showLineNumbers Transfer Progress: File Name: aspnetcore-runtime-8.0.10-win-x64.exe File Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe File Size: 10.09MB Transferred: 0.12MB Percentage Complete: 1.23% Transfer Rate: 2.20MB/s Time Elapsed (s): 0.06 Estimated Time Remaining (s): 4.52 Transfer Progress: File Name: aspnetcore-runtime-8.0.10-win-x64.exe File Path: C:\Workspaces\aspnetcore-runtime-8.0.10-win-x64.exe File Size: 10.09MB Transferred: 10.09MB Percentage Complete: 100.00% Transfer Rate: 9.77MB/s Time Elapsed (s): 1.03 Estimated Time Remaining (s): 0.00 ``` `progress` 参数的类型为 `FileTransferProgress`,包含以下属性和方法: - **属性**: - `FilePath`:文件的路径(`string` 类型)。 - `FileName`:文件的名称(`string` 类型)。 - `FileSize`:文件的大小(以字节为单位的 `long` 类型)。 - `Transferred`:已传输的数据量(以字节为单位的 `long` 类型)。 - `PercentageComplete`:已完成的传输百分比(`double` 类型)。 - `TransferRate`:当前的传输速率(以字节/秒为单位的 `double` 类型)。 - `TimeElapsed`:从开始传输到现在的持续时间(`TimeSpan` 类型)。 - `EstimatedTimeRemaining`:预估的剩余传输时间(`TimeSpan` 类型)。 - **方法**: - `ToString()`:输出带缩进的详细进度字符串。 - `ToStringAsync()`:输出带缩进的详细进度字符串。 - `ToSummaryString()`:输出简要的进度字符串。 - `ToSummaryStringAsync()`:输出简要的进度字符串。 - `UpdateConsoleProgress()`:在控制台中更新(打印)文件传输进度条。 - `UpdateConsoleProgressAsync()`:在控制台中更新(打印)文件传输进度条。 ## 并行下载多个文件 框架原生支持并行下载多个文件。借助 `ParallelUtility.ForEachAsync` 工具方法,可以轻松实现并发下载,并自动启用**多行进度条模式**——每个文件独占两行(文件头和进度条),所有进度条同屏实时刷新,互不干扰: ```cs showLineNumbers {10,12} var urls = new[] { "https://img-s.msn.cn/tenant/amp/entityid/AA296jTM.img?w=640&h=1068&m=6", "https://img-s.msn.cn/tenant/amp/entityid/AA297bnQ.img?w=640&h=1240&m=6&x=236&y=233&s=64&d=64", "https://img-s.msn.cn/tenant/amp/entityid/AA296Rr4.img?w=640&h=821&m=6" }; const string saveDir = @"C:\Workspaces\"; // 并行下载,默认最大并发数为 4 await ParallelUtility.ForEachAsync(urls, async (url, token) => { await _httpRemoteService.DownloadFileWithConsoleProgressAsync(url, saveDir, FileExistsBehavior.Overwrite, cancellationToken: token); }); ``` 执行后,控制台将同时显示所有文件的下载进度,每个文件的进度条独立刷新: ```bash showLineNumbers File: AA296jTM.img, Path: C:\Workspaces\AA296jTM.img [########............] 40.12% (0.05MB/0.12MB) Speed: 1.20MB/s, Time: 42ms, ETA: 58ms. File: AA297bnQ.img, Path: C:\Workspaces\AA297bnQ.img [######..............] 30.05% (0.04MB/0.12MB) Speed: 0.95MB/s, Time: 38ms, ETA: 84ms. File: AA296Rr4.img, Path: C:\Workspaces\AA296Rr4.img [##########..........] 50.33% (0.06MB/0.12MB) Speed: 1.55MB/s, Time: 45ms, ETA: 39ms. ``` 所有文件下载完成后,进度条将逐一显示 `Done!` 状态: ```bash showLineNumbers File: AA296jTM.img, Path: C:\Workspaces\AA296jTM.img [####################] 100.00% (0.12MB/0.12MB) Speed: 2.61MB/s, Time: 167ms. Done! File: AA297bnQ.img, Path: C:\Workspaces\AA297bnQ.img [####################] 100.00% (0.12MB/0.12MB) Speed: 1.81MB/s, Time: 203ms. Done! File: AA296Rr4.img, Path: C:\Workspaces\AA296Rr4.img [####################] 100.00% (0.12MB/0.12MB) Speed: 1.61MB/s, Time: 204ms. Done! ``` 您还可以通过 `maxDegreeOfParallelism` 参数控制最大并发数: ```cs showLineNumbers {5} await ParallelUtility.ForEachAsync(urls, async (url, token) => { await _httpRemoteService.DownloadFileWithConsoleProgressAsync(url, saveDir, FileExistsBehavior.Overwrite, cancellationToken: token); }, maxDegreeOfParallelism: 2); // 最多同时下载 2 个文件 ``` --- # 6.22 HttpFileDownloadBuilder 构建器 > 原文链接:https://http.furion.net/docs/advanced-guide/the-httpfiledownloadbuilder-builder/ 除了上述方法,您还可以使用 `HttpFileDownloadBuilder` 构建器来配置下载网络资源所需的各项设置。 ```cs showLineNumbers {1} var fileTransferResult = await httpRemoteService.SendAsync(HttpRequestBuilder.DownloadFile("https://furion.net/img/furionlogo.png", @"C:\Workspaces\")); ``` `HttpFileDownloadBuilder` 构建器是框架提供专门用来下载网络资源所需的各项设置。`HttpFileDownloadBuilder` 的构造函数是私有的,因此无法直接使用 `new` 关键字进行实例化,不过,框架提供了 `HttpRequestBuilder.DownloadFile` 的多个静态重载方法创建 `HttpFileDownloadBuilder` 的实例。 ```cs showLineNumbers HttpRequestBuilder.DownloadFile(httpMethod, requestUri, destinationPath, onProgressChanged, fileExistsBehavior, configure); HttpRequestBuilder.DownloadFile(requestUri, destinationPath, onProgressChanged, fileExistsBehavior, configure); // 默认 GET 请求 ``` 此外,`HttpFileDownloadBuilder` 包含以下配置功能: ```cs showLineNumbers {1,5,8,11,14,17,20,23,26,29,32-33,36,39,42,45,48} // 默认为 GET 请求。不指定保存的文件名将自动解析文件名,如最终下载路径为 C:\Workspaces\furionlogo.png HttpRequestBuilder.DownloadFile("https://furion.net/img/furionlogo.png", @"C:\Workspaces\") // 设置用于传输操作的缓冲区大小,以字节为单位,默认值为 80 KB .SetBufferSize(80 * 1024) // 设置文件保存的目标路径,可设置为 null,若为 null 时将获取 HttpRemoteOptions 的 DefaultFileDownloadDirectory 属性或应用程序执行目录进行设置 .SetDestinationPath(@"C:\Workspaces\") // 设置当目标文件已存在时的行为 .SetFileExistsBehavior(FileExistsBehavior.Overwrite) // 设置文件传输进度(通知)的间隔时间 .SetProgressInterval(TimeSpan.FromSeconds(1)) // 设置在文件开始传输时的操作 .SetOnTransferStarted(() => {}) // 设置用于传输进度发生变化时执行的委托 .SetOnProgressChanged(async progress => { }) // 设置在文件传输完成时的操作,委托参数为文件传输总花费时间(毫秒) .SetOnTransferCompleted(duration => {}) // 设置在文件传输发生异常时的操作 .SetOnTransferFailed(exception => {}) // 设置在文件存在且配置为跳过时的操作 .SetOnFileExistAndSkip(() => {}) // 设置 HTTP 文件传输事件处理程序,CustomFileTransferEventHandler 为实现 IHttpFileTransferEventHandler 接口的类型 .SetEventHandler() .SetEventHandler(typeof(CustomFileTransferEventHandler)) // 设置 HttpRequestBuilder 实例 .With(builder => {}) // 支持扩展更多 // 设置下载最大线程数 .SetMaxThreads(4) // 设置单次读取数据的最大空闲等待时间(滑动窗口超时) .SetChunkTimeout(TimeSpan.FromSeconds(100)) // 设置多线程分块下载的最大重试次数 .SetChunkMaxRetries(3) // 开启高速下载模式 .EnableHighSpeedMode(); // 支持传入下载最大线程数 ``` 在通过 `HttpRequestBuilder.DownloadFile` 方法成功构建 `HttpFileDownloadBuilder` 实例后,您可以利用 `Send` 方法或异步的 `SendAsync` 方法来执行发送操作。 ```cs showLineNumbers var fileTransferResult = httpRemoteService.Send(httpFileDownloadBuilder, cancellationToken); var fileTransferResult = await httpRemoteService.SendAsync(httpFileDownloadBuilder, cancellationToken); ``` --- # 6.23 文件传输事件处理程序 > 原文链接:https://http.furion.net/docs/advanced-guide/file-transfer-event-handler/ `IHttpFileTransferEventHandler` 接口允许您定义下载或上传文件的预处理操作。通过实现该接口,您可以创建自定义的文件传输事件处理程序,例如 `CustomFileTransferEventHandler` 类: ```cs showLineNumbers {1} public class CustomFileTransferEventHandler : IHttpFileTransferEventHandler { // 在文件开始传输时的操作 public void OnTransferStarted() {} // 传输进度发生变化时的操作 public Task OnProgressChangedAsync(FileTransferProgress fileTransferProgress) {} // 在文件传输完成时的操作 public void OnTransferCompleted(long duration) {} // 在文件传输发生异常时的操作 public void OnTransferFailed(Exception exception) {} } ``` 要在应用程序中启用此处理程序,请在 `Startup.cs` 或 `Program.cs` 文件中注册 `CustomFileTransferEventHandler` 服务: ```cs showLineNumbers services.TryAddSingleton(); ``` 接下来,您可以在构建 `HTTP` 请求时指定此处理程序: ```cs showLineNumbers {2,5} HttpRequestBuilder.DownloadFile("https://furion.net/img/furionlogo.png", @"C:\Workspaces\") .SetEventHandler(); HttpRequestBuilder.DownloadFile("https://furion.net/img/furionlogo.png", @"C:\Workspaces\") .SetEventHandler(typeof(CustomFileTransferEventHandler)); // 使用类型方式设置 ``` > **复用提示** 您可以创建自定义的 `IHttpFileTransferEventHandler` 接口实现类型,并在多个 `HttpFileDownloadBuilder` 实例中复用该实现。 > **触发时机说明** 当 `HttpFileDownloadBuilder` 实例配置了 `SetOnTransferStarted`、`SetOnProgressChanged`、`OnTransferCompleted` 或 `OnTransferFailed` 方法时,这些回调方法将会被触发。 如果同时实现了 `IHttpFileTransferEventHandler` 接口,其方法(`OnTransferStarted`、`OnProgressChangedAsync`、`OnTransferCompleted` 和 `OnTransferFailed`)的调用时机将晚于 `HttpFileDownloadBuilder` 实例设置的系列方法。 --- # 6.24 上传文件资源 > 原文链接:https://http.furion.net/docs/advanced-guide/uploading-file-resources/ 在互联网应用中,用户上传文件是一项常见需求,涵盖设置头像、发布图文动态、上传相册至网盘、分享 `Vlog` 到视频社区等场景。以下展示了多种文件上传的实现方式。 ## 使用 `Form` 表单方式上传 ```cs showLineNumbers {2-3} await httpRemoteService.PostAsync("https://localhost:7044/HttpRemote/AddFile", builder => builder .SetMultipartContent(multipart => multipart .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"))); ``` 若需上传多个文件,只需在 `multipart` 中继续添加(需保持表单名一致,如 `files`): ```cs showLineNumbers {3-4} await httpRemoteService.PostAsync("https://localhost:7044/HttpRemote/AddFiles", builder => builder .SetMultipartContent(multipart => multipart .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "files") .AddFileFromRemote("https://furion.net/img/furionlogo.png", "files"))); ``` 此外,还支持使用构建器模式,以及获取上传文件的返回值。更多详情可参考第 2.1 节。 ```cs showLineNumbers {2} // 使用构建器模式 await httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://localhost:7044/HttpRemote/AddFile") .SetMultipartContent(multipart => multipart .AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file"))); // 更多详细用法可参考第 2.1 节 ``` 然而,这种上传文件资源的方式在面对多种复杂场景时显得不够灵活,例如无法实时追踪上传进度、限制上传文件类型和大小的情况,以及实现断点续传等。此外,它还可能需要开发者编写更多的额外代码。因此,框架中集成了专门设计用于上传文件资源的功能,以应对这些问题。 ## 利用框架内置的专用上传功能进行上传(表单方式) 在视频分享等应用中,用户上传文件时通常需要查看实时进度。为此,可使用 `UploadFile` 扩展方法,该方法支持实时进度获取,并允许对文件类型和大小进行限制。 以下示例展示了如何打印上传进度: ```cs showLineNumbers {2-5} await httpRemoteService.UploadFileAsync("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file" , async progress => { Console.WriteLine(await progress.ToSummaryStringAsync()); // 输出简要进度信息 }); ``` 控制台输出示例: ```bash showLineNumbers Transferred 0.01MB of 0.01MB (100.00% complete, Speed: 0.86MB/s, Time: 0.01s, ETA: 0.00s), File: httptest.jpg, Path: C:\Workspaces\httptest.jpg. ``` 若需在控制台中实时显示文件上传进度,推荐使用 `UpdateConsoleProgressAsync()` 方法。示例如下: ```cs showLineNumbers {2,5} await httpRemoteService.UploadFileAsync("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file" , progress => progress.UpdateConsoleProgressAsync()); // 在控制台中更新文件传输进度条 // ✅ 或使用 UploadFileWithConsoleProgressAsync 方法(带控制台进度打印) await httpRemoteService.UploadFileWithConsoleProgressAsync("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file"); ``` 执行后,控制台将显示如下进度信息: ```bash showLineNumbers {2} File: httptest.jpg, Path: C:\Workspaces\httptest.jpg. [##################################################] 61.35% (0.01MB/0.01MB) Speed: 0.86MB/s, Time: 0.01s, ETA: 0.00s. ``` 若需限制文件类型和大小,可如下操作: ```cs showLineNumbers {1,6-7} await httpRemoteService.SendAsync(HttpRequestBuilder.UploadFile("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file" , async progress => { Console.WriteLine(await progress.ToSummaryStringAsync()); // 输出简要进度信息 }) .SetAllowedFileExtensions(".jpg;.png") // 仅允许 jpg 和 png 类型 .SetMaxFileSizeInBytes(5 * 1024 * 1024)); // 限制文件大小为 5MB ``` 若需在上传文件时附加额外的表单参数,可如下操作: ```cs showLineNumbers {6-9} await httpRemoteService.SendAsync(HttpRequestBuilder.UploadFile("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file" , async progress => { Console.WriteLine(await progress.ToSummaryStringAsync()); // 输出简要进度信息 }) .WithMultipart(multipart => { multipart.AddText("Furion", "name"); }); ``` 通过上述方式,可以灵活满足各类文件上传需求。 > **关于多文件上传** `UploadFile` 扩展方式仅支持单个文件上传,无法同时处理多个文件的上传需求。 --- # 6.25 HttpFileUploadBuilder 构建器 > 原文链接:https://http.furion.net/docs/advanced-guide/the-httpfileuploadbuilder-builder/ 除了上述方法,您还可以使用 `HttpFileUploadBuilder` 构建器来配置上传文件资源所需的各项设置。 ```cs showLineNumbers {1} await httpRemoteService.SendAsync(HttpRequestBuilder.UploadFile("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file"); ``` `HttpFileUploadBuilder` 构建器是框架提供专门用来上传文件资源所需的各项设置。`HttpFileUploadBuilder` 的构造函数是私有的,因此无法直接使用 `new` 关键字进行实例化,不过,框架提供了 `HttpRequestBuilder.UploadFile` 的多个静态重载方法创建 `HttpFileUploadBuilder` 的实例。 ```cs showLineNumbers HttpRequestBuilder.UploadFile(httpMethod, requestUri, filePath, name, onProgressChanged, fileName, configure); HttpRequestBuilder.UploadFile(requestUri, filePath, name, onProgressChanged, fileName, configure); // 默认 POST 请求 ``` 此外,`HttpFileUploadBuilder` 包含以下配置功能: ```cs showLineNumbers {1,5,8-9,12,15,18,21,24,27,30-31,34,37} // 默认为 POST 请求,默认表单名为 file HttpRequestBuilder.UploadFile("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file") // 设置内容类型(文件类型) .SetContentType("image/jpeg") // 设置允许的文件扩展名 .SetAllowedFileExtensions([".jpg", ".png"]) .SetAllowedFileExtensions(".jpg;.png") // 设置允许的文件大小,字节单位 .SetMaxFileSizeInBytes(5 * 1024 * 1024) // 设置文件传输进度(通知)的间隔时间 .SetProgressInterval(TimeSpan.FromSeconds(1)) // 设置在文件开始传输时的操作 .SetOnTransferStarted(() => {}) // 设置用于传输进度发生变化时执行的委托 .SetOnProgressChanged(async progress => { }) // 设置在文件传输完成时的操作,委托参数为文件传输总花费时间(毫秒) .SetOnTransferCompleted(duration => {}) // 设置在文件传输发生异常时的操作 .SetOnTransferFailed(exception => {}) // 设置 HTTP 文件传输事件处理程序,CustomFileTransferEventHandler 为实现 IHttpFileTransferEventHandler 接口的类型 .SetEventHandler() .SetEventHandler(typeof(CustomFileTransferEventHandler)) // 追加多部分表单内容 .WithMultipart(multipart => {}); // 设置 HttpRequestBuilder 实例 .With(builder => {}); // 支持扩展更多 ``` 在通过 `HttpRequestBuilder.UploadFile` 方法成功构建 `HttpFileUploadBuilder` 实例后,您可以利用 `Send` 方法或异步的 `SendAsync` 方法来执行发送操作。 ```cs showLineNumbers httpRemoteService.Send(httpFileUploadBuilder, cancellationToken); await httpRemoteService.SendAsync(httpFileUploadBuilder, cancellationToken); ``` --- # 6.26 文件传输事件处理程序 > 原文链接:https://http.furion.net/docs/advanced-guide/file-transfer-event-handler-2/ `IHttpFileTransferEventHandler` 接口允许您定义下载或上传文件的预处理操作。通过实现该接口,您可以创建自定义的文件传输事件处理程序,例如 `CustomFileTransferEventHandler` 类: ```cs showLineNumbers {1} public class CustomFileTransferEventHandler : IHttpFileTransferEventHandler { // 在文件开始传输时的操作 public void OnTransferStarted() {} // 传输进度发生变化时的操作 public Task OnProgressChangedAsync(FileTransferProgress fileTransferProgress) {} // 在文件传输完成时的操作 public void OnTransferCompleted(long duration) {} // 在文件传输发生异常时的操作 public void OnTransferFailed(Exception exception) {} } ``` 要在应用程序中启用此处理程序,请在 `Startup.cs` 或 `Program.cs` 文件中注册 `CustomFileTransferEventHandler` 服务: ```cs showLineNumbers services.TryAddSingleton(); ``` 接下来,您可以在构建 `HTTP` 请求时指定此处理程序: ```cs showLineNumbers {2,5} HttpRequestBuilder.UploadFile("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file") .SetEventHandler(); HttpRequestBuilder.UploadFile("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file") .SetEventHandler(typeof(CustomFileTransferEventHandler)); // 使用类型方式设置 ``` > **复用提示** 您可以创建自定义的 `IHttpFileTransferEventHandler` 接口实现类型,并在多个 `HttpFileUploadBuilder` 实例中复用该实现。 > **触发时机说明** 当 `HttpFileUploadBuilder` 实例配置了 `SetOnTransferStarted`、`SetOnProgressChanged`、`OnTransferCompleted` 或 `OnTransferFailed` 方法时,这些回调方法将会被触发。 如果同时实现了 `IHttpFileTransferEventHandler` 接口,其方法(`OnTransferStarted`、`OnProgressChangedAsync`、`OnTransferCompleted` 和 `OnTransferFailed`)的调用时机将晚于 `HttpFileUploadBuilder` 实例设置的系列方法。 > **禁用请求分析工具** 在打印请求内容时,`Stream` 对象可能会被重复读取或变得不可读。这是因为流会被提前读取到内存中,其位置指针会移动到尾部。这会导致无法准确获取上传进度。 因此,在使用框架提供的专门上传功能时,建议禁用请求分析工具,以确保能够获取准确的上传进度信息。 --- # 6.27 压力与模拟测试 > 原文链接:https://http.furion.net/docs/advanced-guide/stress-and-simulation-testing/ 在开发面向互联网或需承受多人并发访问的应用系统时,性能压测和接口自动化模拟测试成为部署前的关键环节。通过这两项测试获取的报告指标,我们能在系统上线前对代码进行优化,确保其满足最低上线要求。 以 `Furion` 框架官网为例,进行压力测试: ```cs showLineNumbers {1-2} var stressTestHarnessResult = await httpRemoteService.StressTestHarnessAsync("https://furion.net/"); Console.WriteLine(stressTestHarnessResult.ToString()); // 打印压力测试结果 ``` 测试结果概览: ```bash showLineNumbers Stress Test Harness Result: Total Requests: 100 // 总请求次数 Total Time (s): 7.95 // 总用时(秒) Successful Requests: 100 // 成功请求次数 Failed Requests: 0 // 失败请求次数 QPS: 12.58 // 每秒查询率 (QPS) Min RT (ms): 676.38 // 最小响应时间(毫秒) Max RT (ms): 7,419.72 // 最大响应时间(毫秒) Avg RT (ms): 3,314.94 // 平均响应时间(毫秒) P10 RT (ms): 1,288.82 // P10 响应时间(毫秒) P25 RT (ms): 2,057.10 // P25 响应时间(毫秒) P50 RT (ms): 3,064.56 // P50 响应时间(毫秒) P75 RT (ms): 4,100.03 // P75 响应时间(毫秒) P90 RT (ms): 5,026.08 // P90 响应时间(毫秒) P95 RT (ms): 7,330.71 // P95 响应时间(毫秒) P99 RT (ms): 7,416.20 // P99 响应时间(毫秒) P99.99 RT (ms): 7,419.72 // P99.99 响应时间(毫秒) ``` `stressTestHarnessResult` 变量类型为 `StressTestHarnessResult`,包含以下属性和方法: - **属性**: - `TotalRequests`:总请求次数(`long` 类型)。 - `TotalTimeInSeconds`:总用时(秒)(`double` 类型)。 - `SuccessfulRequests`:成功请求次数(`long` 类型)。 - `FailedRequests`:失败请求次数(`long` 类型)。 - `QueriesPerSecond`:每秒查询率 (`QPS`)(`double` 类型)。 - `MinResponseTime`:最小响应时间(毫秒)(`double` 类型)。 - `MaxResponseTime`:最大响应时间(毫秒)(`double` 类型)。 - `AverageResponseTime`:平均响应时间(毫秒)(`double` 类型)。 - `Percentile10ResponseTime`:`P10` 响应时间(毫秒)(`double` 类型)。 - `Percentile25ResponseTime`:`P25` 响应时间(毫秒)(`double` 类型)。 - `Percentile50ResponseTime`:`P50` 响应时间(毫秒)(`double` 类型)。 - `Percentile75ResponseTime`:`P75` 响应时间(毫秒)(`double` 类型)。 - `Percentile90ResponseTime`:`P90` 响应时间(毫秒)(`double` 类型)。 - `Percentile95ResponseTime`:`P95` 响应时间(毫秒)(`double` 类型)。 - `Percentile99ResponseTime`:`P99` 响应时间(毫秒)(`double` 类型)。 - `Percentile9999ResponseTime`:`P99.99` 响应时间(毫秒)(`double` 类型)。 - **方法**: - `ToString()`:输出带缩进的详细报告字符串。 默认情况下,压力测试执行 `1` 轮,每次包含 `100` 个并发请求,最大并发度为 `100`。为获取更精确的测试结果,可按需调整这些参数: ```cs showLineNumbers {2-4,7,9} var stressTestHarnessResult = await httpRemoteService.SendAsync(HttpRequestBuilder.StressTestHarness("https://furion.net/") .SetNumberOfRequests(1000) // 设置并发请求数量 .SetNumberOfRounds(5) // 设置压测轮次 .SetMaxDegreeOfParallelism(500)); // 设置最大并发度 // 在大多数情况下,只需要设置并发请求数量即可 var stressTestHarnessResult = await httpRemoteService.StressTestHarnessAsync("https://furion.net/", 500); var stressTestHarnessResult = await httpRemoteService.SendAsync(HttpRequestBuilder.StressTestHarness("https://furion.net/", 500)); ``` > **快速生成测试报告** 进行压力测试时,默认使用 `GET` 请求并下载完整响应内容(`HttpCompletionOption.ResponseContentRead`)。若无需完整响应内容,可选择 `HEAD` 请求,并将 `completionOption` 设置为 `ResponseHeadersRead`,以快速生成压力测试报告。 > **滥用说明** **在进行压力测试时,会自动添加 `X-Stress-Test: Harness` 请求标头,以防止滥用对目标系统造成损害。** 同时,由于测试结果受硬件设备、操作系统及代码实现等多种因素影响,仅供参考。 此外,**为获取更准确的数据,请求分析工具默认被禁用**。 --- # 6.28 HttpStressTestHarnessBuilder 构建器 > 原文链接:https://http.furion.net/docs/advanced-guide/httpstresstestharnessbuilder-builder/ `HttpStressTestHarnessBuilder` 构建器是框架提供专门用来进行压力与模拟测试所需的各项设置。`HttpStressTestHarnessBuilder` 的构造函数是私有的,因此无法直接使用 `new` 关键字进行实例化,不过,框架提供了 `HttpRequestBuilder.StressTestHarness` 的多个静态重载方法创建 `HttpStressTestHarnessBuilder` 的实例。 ```cs showLineNumbers HttpRequestBuilder.StressTestHarness(httpMethod, requestUri, numberOfRequests, configure); HttpRequestBuilder.StressTestHarness(requestUri, numberOfRequests, configure); // 默认 GET 请求 ``` 此外,`HttpStressTestHarnessBuilder` 包含以下配置功能: ```cs showLineNumbers {1,5,8,11,14,17} // 默认为 GET 请求,默认并发请求数量为 100 HttpRequestBuilder.StressTestHarness("https://furion.net/") // 设置并发请求数量,默认值 100 .SetNumberOfRequests(500) // 设置最大并发度,默认值 100 .SetMaxDegreeOfParallelism(500) // 设置压测轮次,默认值为 1 轮 .SetNumberOfRounds(5) // 设置禁用 HTTP 缓存 .DisableCache() // 设置 HttpRequestBuilder 实例 .With(builder => {}); ``` 在通过 `HttpRequestBuilder.StressTestHarness` 方法成功构建 `HttpStressTestHarnessBuilder` 实例后,您可以利用 `Send` 方法或异步的 `SendAsync` 方法来执行发送操作。 ```cs showLineNumbers httpRemoteService.Send(httpStressTestHarnessBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); await httpRemoteService.SendAsync(httpStressTestHarnessBuilder, HttpCompletionOption.ResponseContentRead, cancellationToken); ``` --- # 6.29 长轮询 Long Polling > 原文链接:https://http.furion.net/docs/advanced-guide/long-polling/ 长轮询(`Long Polling`)是一种实现服务器向客户端推送数据的技术。它通过保持 `HTTP` 连接打开直到有新数据发送给客户端,或者直到超时为止,从而模拟了服务器推送的效果。长轮询是传统轮询(即客户端定期向服务器发送请求以检查是否有新的数据)的一种改进,可以减少不必要的请求,提高效率。 长轮询的工作原理: 1. 客户端向服务器发起一个请求。 2. 如果服务器上没有新数据,服务器不会立即响应这个请求,而是将请求挂起。 3. 一旦服务器上有新数据可供发送,或达到了预设的超时时间,服务器就会响应请求,并发送数据给客户端。 4. 客户端处理完数据后,再次向服务器发起一个新的请求,重复上述过程。 ![long-polling](/images/long-polling.png) 长轮询的应用场景: - **实时通知**:例如,在线聊天应用中,当用户收到新消息时,服务器可以通过长轮询及时推送消息给客户端。 - **在线协作工具**:如多人同时编辑文档的应用,长轮询可以用来实时同步用户的编辑操作。 - **游戏更新**:在网络游戏中,长轮询可用于实时更新游戏状态,比如玩家位置、得分等信息。 - **股票市场更新**:金融应用程序中使用长轮询来实时显示股票价格变动。 - **配置中心**:在微服务架构中,配置中心使用长轮询技术来确保各个服务能够即时接收到最新的配置变更。当配置发生更改时,配置中心可以迅速将更新推送到所有相关的服务实例,确保配置的一致性和时效性。 以下示例展示了如何使用长轮询请求: ```cs showLineNumbers {1-2,9-11} await httpRemoteService.LongPollingAsync("https://localhost:7044/HttpRemote/LongPolling" , async (responseMessage, token) => { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(token)); await Task.CompletedTask; }, cancellationToken: cancellationToken); // 使用构建器模式 await httpRemoteService.SendAsync(HttpRequestBuilder .LongPolling("https://localhost:7044/HttpRemote/LongPolling" , async (responseMessage, token) => { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(token)); await Task.CompletedTask; }), cancellationToken: cancellationToken); ``` 长轮询也支持以 `IAsyncEnumerable` 的方式消费数据,让你可以使用 `await foreach` 来迭代每个轮询响应: ```cs showLineNumbers {1,4,6,11,13,15} await foreach (var responseMessage in httpRemoteService.LongPollingAsAsyncEnumerable("https://localhost:7044/HttpRemote/LongPolling", cancellationToken: cancellationToken)) { // 注意:每个响应在使用后需要手动释放(或使用 using) using (responseMessage) { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(cancellationToken)); } } // 使用构建器模式 await foreach (var responseMessage in httpRemoteService.SendAsAsyncEnumerable(HttpRequestBuilder.LongPolling("https://localhost:7044/HttpRemote/LongPolling"), cancellationToken)) { using (responseMessage) { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(cancellationToken)); } } ``` 虽然长轮询在一定程度上解决了实时通信的需求,但它也有一些缺点,比如在高并发情况下可能会对服务器造成较大压力,以及长时间的连接可能会影响服务器的性能。随着 `Web` 技术的发展,`Server-Sent Events` 或 `WebSocket` 等更先进的技术逐渐成为实现实时双向通信的首选方案。然而,在某些受限环境中,长轮询仍然是一个可行的选择。 --- # 6.30 HttpLongPollingBuilder 构建器 > 原文链接:https://http.furion.net/docs/advanced-guide/httplongpollingbuilder-builder/ `HttpLongPollingBuilder` 构建器是框架提供专门用来发送长轮询请求所需的各项设置。`HttpLongPollingBuilder` 的构造函数是私有的,因此无法直接使用 `new` 关键字进行实例化,不过,框架提供了 `HttpRequestBuilder.LongPolling` 的多个静态重载方法创建 `HttpLongPollingBuilder` 的实例。 ```cs showLineNumbers HttpRequestBuilder.LongPolling(httpMethod, requestUri, onDataReceived, configure); HttpRequestBuilder.LongPolling(requestUri, onDataReceived, configure); // 默认 GET 请求 HttpRequestBuilder.LongPolling(httpMethod, requestUri, configure); HttpRequestBuilder.LongPolling(requestUri, configure); // 默认 GET 请求 ``` 此外,`HttpLongPollingBuilder` 包含以下配置功能: ```cs showLineNumbers {1,10,13,16,19,22,25-26,29} // 默认为 GET 请求 HttpRequestBuilder.LongPolling("https://localhost:7044/HttpRemote/LongPolling" , async (responseMessage, token) => { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(cancellationToken)); await Task.CompletedTask; }) // 设置轮询重试间隔,默认为 2 秒 .SetRetryInterval(TimeSpan.FromSeconds(2)) // 设置最大重试次数,默认为 100 次 .SetMaxRetries(500) // 设置在接收服务器返回 200~299 状态码的数据的操作 // .SetOnDataReceived(async responseMessage => {}) // 可通过初始时传入 // 设置在接收服务器返回【非】 200~299 状态码的数据的操作 .SetOnError(async responseMessage => {}) // 设置在响应标头包含 X-End-Of-Stream 时触发的操作 .SetOnEndOfStream(async ResponseMessage => {}) // 设置长轮询事件处理程序 .SetEventHandler() .SetEventHandler(typeof(CustomLongPollingEventHandler)) // 设置 HttpRequestBuilder 实例 .With(builder => {})); // 支持更多扩展 ``` 在通过 `HttpRequestBuilder.LongPolling` 方法成功构建 `HttpLongPollingBuilder` 实例后,您可以利用 `Send`、`SendAsync` 或 `SendAsAsyncEnumerable` 来执行发送操作。 ```cs showLineNumbers httpRemoteService.Send(httpLongPollingBuilder, cancellationToken); await httpRemoteService.SendAsync(httpLongPollingBuilder, cancellationToken); await foreach (var response in httpRemoteService.SendAsAsyncEnumerable(httpLongPollingBuilder, cancellationToken)) { // 处理每个响应 } ``` --- # 6.31 长轮询事件处理程序 > 原文链接:https://http.furion.net/docs/advanced-guide/long-polling-event-handler/ `IHttpLongPollingEventHandler` 接口允许您定义发送长轮询请求的预处理操作。通过实现该接口,您可以创建自定义的长轮询事件处理程序,例如 `CustomLongPollingEventHandler` 类: ```cs showLineNumbers {1} public class CustomLongPollingEventHandler : IHttpLongPollingEventHandler { // 用于接收服务器返回 200~299 状态码的数据的操作 public Task OnDataReceivedAsync(HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken) {} // 用于接收服务器返回【非】 200~299 状态码的数据的操作 public Task OnErrorAsync(HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken) {} // 用于响应标头包含 X-End-Of-Stream 时触发的操作 public Task OnEndOfStreamAsync(HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken) {} } ``` 要在应用程序中启用此处理程序,请在 `Startup.cs` 或 `Program.cs` 文件中注册 `CustomLongPollingEventHandler` 服务: ```cs showLineNumbers services.TryAddSingleton(); ``` 接下来,您可以在构建 `HTTP` 请求时指定此处理程序: ```cs showLineNumbers {7,15} HttpRequestBuilder.LongPolling("https://localhost:7044/HttpRemote/LongPolling" , async (responseMessage, token) => { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(cancellationToken)); await Task.CompletedTask; }) .SetEventHandler(); HttpRequestBuilder.LongPolling("https://localhost:7044/HttpRemote/LongPolling" , async (responseMessage, token) => { Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(cancellationToken)); await Task.CompletedTask; }) .SetEventHandler(typeof(CustomLongPollingEventHandler)); // 使用类型的方式 ``` > **复用提示** 您可以创建自定义的 `IHttpLongPollingEventHandler` 接口实现类型,并在多个 `HttpLongPollingBuilder` 实例中复用该实现。 > **触发时机说明** 当 `HttpLongPollingBuilder` 实例配置了 `SetOnDataReceived`、`SetOnError` 或 `SetOnEndOfStream` 方法时,这些回调方法将会被触发。 如果同时实现了 `IHttpLongPollingEventHandler` 接口,其方法(`OnDataReceivedAsync`、`OnErrorAsync` 和 `OnEndOfStreamAsync`)的调用时机将晚于 `HttpLongPollingBuilder` 实例设置的系列方法。 --- # 6.32 终止长轮询请求 > 原文链接:https://http.furion.net/docs/advanced-guide/terminating-a-long-polling-request/ 除了使用 `CancellationToken` 来取消长轮询请求,框架还会检查响应标头中的 `X-End-Of-Stream`,若存在该标头,则终止长轮询请求。 --- # 6.33 Server-Sent Events 单向通信 > 原文链接:https://http.furion.net/docs/advanced-guide/server-sent-events-unidirectional-communication/ 随着人工智能聊天机器人 `ChatGPT` 的快速流行,其用户界面中模拟打字机效果的对话设计给人留下了深刻印象。这种生动逼真的交互体验,实际上是通过一种称为“服务器发送事件”(`Server-Sent Events`, `SSE`)的技术实现的。 `Server-Sent Events` 是一种允许服务器主动向客户端(通常是浏览器)发送实时更新数据的通信技术。**与传统的客户端请求-服务器响应模式不同,`SSE` 实现了服务器到客户端的单向、异步通信,从而无需客户端不断轮询服务器以获取最新数据。** 这种技术极大地减轻了服务器的负担,并提高了数据传输的效率和实时性。 `Server-Sent Events` 的应用场景: 1. **实时通知**:可以用来实现实时的消息提醒或通知系统,如社交网络上的新消息提示或邮件到达通知。 2. **数据流更新**:对于需要持续更新的数据,如股票价格、天气信息或体育比赛结果,`SSE` 能够提供即时的数据更新。 3. **进度报告**:在执行耗时较长的任务时,比如文件上传或复杂计算过程中,`SSE` 可以用来向客户端报告任务的进度。 4. **日志和监控**:在开发和运维领域,`SSE` 可用于实时显示日志文件的变化或监控系统的健康状态。 以下示例展示了如何使用 `Server-Sent Events` 向服务器获取数据: ```cs showLineNumbers {1,3,11,13} await httpRemoteService.ServerSentEventsAsync("https://localhost:7044/HttpRemote/Events" // 接收到数据时的操作 , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }, cancellationToken: cancellationToken); // 使用构建器模式 await httpRemoteService.SendAsync(HttpRequestBuilder .ServerSentEvents("https://localhost:7044/HttpRemote/Events" // 接收到数据时的操作 , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }), cancellationToken: cancellationToken); ``` `Server-Sent Events` 也支持以 `IAsyncEnumerable` 的方式消费数据,让你可以使用 `await foreach` 来迭代每个轮询响应: ```cs showLineNumbers {1,3,7,9} await foreach (var data in httpRemoteService.ServerSentEventsAsAsyncEnumerable("https://localhost:7044/HttpRemote/Events", cancellationToken: cancellationToken)) { Console.WriteLine(data.Data); } // 使用构建器模式 await foreach (var data in httpRemoteService.SendAsAsyncEnumerable(HttpRequestBuilder.ServerSentEvents("https://localhost:7044/HttpRemote/Events"), cancellationToken)) { Console.WriteLine(data.Data); } ``` `data` 参数的类型为 `ServerSentEventsData`,包含以下属性: - **属性**: - `Event`:事件类型(`string` 类型)。 - `Data`:消息(`string` 类型)。 - `RawLine`:原始消息行(`string` 类型)。 - `Id`:事件 `ID`(`string` 类型)。 - `Retry`:重新连接的时间(以毫秒为单位的 `int` 类型)。 - `CustomFields`:自定义的字段数据(`IReadOnlyCollection>` 类型)。 您还可以监听连接成功和发送异常时的事件: ```cs showLineNumbers {9,14,29,34} await httpRemoteService.ServerSentEventsAsync("https://localhost:7044/HttpRemote/Events" // 接收到数据时的操作 , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }, builder => builder // 连接打开时操作 .SetOnOpen(() => { Console.WriteLine("连接成功。"); }) // 连接未打开时操作 .SetOnError((ex) => { Console.WriteLine("连接错误。" + ex.Message); }), cancellationToken: cancellationToken); // 使用构建器模式 await httpRemoteService.SendAsync(HttpRequestBuilder .ServerSentEvents("https://localhost:7044/HttpRemote/Events" // 接收到数据时的操作 , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }) // 连接打开时操作 .SetOnOpen(() => { Console.WriteLine("连接成功。"); }) // 连接未打开时操作 .SetOnError((ex) => { Console.WriteLine("连接错误。" + ex.Message); }), cancellationToken: cancellationToken); ``` `Server-Sent Events` 特别适合那些需要服务器向客户端发送更新,但客户端不需要频繁向服务器发送请求的应用场景。无论是用于实时更新数据、提供进度报告还是实现简单的通知系统,`SSE` 都是一个值得考虑的选择。 > **禁用请求分析工具** 在发送 `Server-Sent Events`(服务器发送事件)时,由于它采用 `Stream` 流式返回数据,如果启用请求分析工具,会导致流式数据的每个部分被提前加载到内存中读取。这不仅会严重影响流式数据的实时显示效果,还可能在返回大量数据时引发内存过高的问题。 因此,建议在发送 `Server-Sent Events` 请求时关闭请求分析工具。 > **请求谓词说明** 标准化的 `Server-Sent Events (SSE)` 仅支持通过 `GET` 方法接收服务器推送的事件。但框架提供支持通过任意请求谓词(如示例中的 `POST`)来配置 `SSE`: ```cs showLineNumbers {} HttpRequestBuilder .ServerSentEvents(HttpMethod.Post, new Uri("https://localhost:7044/HttpRemote/Events")); ``` --- # 6.34 HttpServerSentEventsBuilder 构建器 > 原文链接:https://http.furion.net/docs/advanced-guide/httpserversenteventsbuilder-builder/ `HttpServerSentEventsBuilder` 构建器是框架提供专门用来接收服务器 `Server-Sent Events` 推送事件所需的各项设置。`HttpServerSentEventsBuilder` 的构造函数是私有的,因此无法直接使用 `new` 关键字进行实例化,不过,框架提供了 `HttpRequestBuilder.ServerSentEvents` 的多个静态重载方法创建 `HttpServerSentEventsBuilder` 的实例。 ```cs showLineNumbers HttpRequestBuilder.ServerSentEvents(requestUri, onMessage, configure); // 默认 GET 请求 HttpRequestBuilder.ServerSentEvents(httpMethod, requestUri, onMessage, configure); HttpRequestBuilder.ServerSentEvents(requestUri, configure); // 默认 GET 请求 HttpRequestBuilder.ServerSentEvents(httpMethod, requestUri, configure); ``` 此外,`HttpServerSentEventsBuilder` 包含以下配置功能: ```cs showLineNumbers {1,10,13,16,19,22,25-26,30,33} // 默认为 GET 请求 HttpRequestBuilder.ServerSentEvents("https://localhost:7044/HttpRemote/Events" , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }) // 设置默认重新连接的间隔时间,默认为 2 秒 .SetDefaultRetryInterval(2000) // 单位毫秒 // 设置最大重试次数,默认为 100 次 .SetMaxRetries(500) // 设置用于在与事件源的连接打开时的操作 .SetOnOpen(() => {}) // 设置用于在从事件源接收到数据时的操作 // .SetOnMessage(async (data, token) => {}) // 可通过初始时传入 // 设置用于在事件源连接未能打开时的操作 .SetOnError(exception => {}) // 设置 Server-Sent Events 事件处理程序 .SetEventHandler() .SetEventHandler(typeof(CustomServerSentEventsEventHandler)) // 设置是否自动修正请求方法 // 若为 true 则当请求为 GET 或 HEAD 且包含请求内容时,自动将方法改为 POST;默认值为 true .SetAutoCorrectMethod(true) // 设置 HttpRequestBuilder 实例 .With(builder => {})); // 支持更多扩展 ``` 在通过 `HttpRequestBuilder.ServerSentEvents` 方法成功构建 `HttpServerSentEventsBuilder` 实例后,您可以利用 `Send`、`SendAsync` 或 `SendAsAsyncEnumerable` 来执行发送操作。 ```cs showLineNumbers httpRemoteService.Send(httpServerSentEventsBuilder, cancellationToken); await httpRemoteService.SendAsync(httpServerSentEventsBuilder, cancellationToken); await foreach (var data in httpRemoteService.SendAsAsyncEnumerable(httpServerSentEventsBuilder, cancellationToken)) { // 处理每个数据 } ``` --- # 6.35 Server-Sent Events 事件处理程序 > 原文链接:https://http.furion.net/docs/advanced-guide/server-sent-events-event-handler/ `IHttpServerSentEventsEventHandler` 接口允许您定义接收服务器 `Server-Sent Events` 推送事件的预处理操作。通过实现该接口,您可以创建自定义的 `Server-Sent Events` 事件处理程序,例如 `CustomServerSentEventsEventHandler` 类: ```cs showLineNumbers {1} public class CustomServerSentEventsEventHandler : IHttpServerSentEventsEventHandler { // 用于在与事件源的连接打开时的操作 void OnOpen(); // 用于在从事件源接收到数据时的操作 Task OnMessageAsync(ServerSentEventsData serverSentEventsData, CancellationToken cancellationToken); // 用于在事件源连接未能打开时的操作 void OnError(Exception exception); } ``` 要在应用程序中启用此处理程序,请在 `Startup.cs` 或 `Program.cs` 文件中注册 `CustomServerSentEventsEventHandler` 服务: ```cs showLineNumbers services.TryAddSingleton(); ``` 接下来,您可以在构建 `HTTP` 请求时指定此处理程序: ```cs showLineNumbers {7,15} HttpRequestBuilder.ServerSentEvents("https://localhost:7044/HttpRemote/Events" , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }) .SetEventHandler(); HttpRequestBuilder.ServerSentEvents("https://localhost:7044/HttpRemote/Events" , async (data, token) => { Console.WriteLine(data.Data); await Task.CompletedTask; }) .SetEventHandler(typeof(CustomServerSentEventsEventHandler)); // 使用类型的方式 ``` > **复用提示** 您可以创建自定义的 `IHttpServerSentEventsEventHandler` 接口实现类型,并在多个 `HttpServerSentEventsBuilder` 实例中复用该实现。 > **触发时机说明** 当 `HttpServerSentEventsBuilder` 实例配置了 `SetOnOpen`、`SetOnMessage` 或 `SetOnError` 方法时,这些回调方法将会被触发。 如果同时实现了 `IHttpServerSentEventsEventHandler` 接口,其方法(`OnOpen`、`OnMessageAsync` 和 `OnError`)的调用时机将晚于 `HttpServerSentEventsBuilder` 实例设置的系列方法。 --- # 6.36 WebSocket 双工通信 > 原文链接:https://http.furion.net/docs/advanced-guide/websocket-duplex-communication/ `WebSocket` 是一种在单个 `TCP` 连接上进行全双工通信的协议。`WebSocket `使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 `WebSocket API` 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。 `WebSocket` 的应用场景: - **实时聊天应用**:`WebSocket` 可以实现实时的消息传递,使得用户间的交流几乎无延迟。 - **在线游戏**:对于需要快速响应的游戏,`WebSocket` 能够提供低延迟的数据传输。 - **股票市场更新**:实时更新股票价格和其他金融信息。 - **协同编辑工具**:允许多个用户同时编辑同一个文档,并实时看到其他人的更改。 - **实时地图应用**:例如导航应用中实时交通状况的更新。 以下示例展示了如何使用 `WebSocketClient` 连接服务器: ```cs showLineNumbers {1,4,10,16,22,29,32,38,45,49} using var webSocketClient = new WebSocketClient("wss://ws.postman-echo.com/raw"); // 支持 ws:// 和 wss:// // 连接成功事件 webSocketClient.Connected += (sender, s) => { Console.WriteLine("连接成功"); return Task.CompletedTask; }; // 连接关闭事件 webSocketClient.Closed += (sender, args) => { Console.WriteLine("连接关闭"); return Task.CompletedTask; }; // 接收文本消息 webSocketClient.TextReceived += (sender, s) => { Console.WriteLine(s.Message); return Task.CompletedTask; }; // 接收二进制消息 webSocketClient.BinaryReceived += (sender, s) => { Console.WriteLine(s.Message); return Task.CompletedTask; }; // 连接服务器 await webSocketClient.ConnectAsync(); // 开启一个任务,循环发送消息 _ = Task.Run(async () => { var i = 0; while (i < 5) { // 发送文本消息 await webSocketClient.SendAsync("Hello, WebSocket!"); await Task.Delay(1000); i++; } // 关闭连接 await webSocketClient.CloseAsync(); }); // 等待接收消息和关闭事件(阻塞) await webSocketClient.WaitAsync(); ``` `WebSocket` 与 `Server-Sent Events (SSE)` 的区别: - **通信方向**:`WebSocket` 支持全双工双向通信,`SSE` 仅支持服务器向客户端单向推送数据。 - **协议**:`WebSocket` 使用独立的 `WebSocket` 协议 (`ws://` 或 `wss://`),`SSE` 基于 `HTTP` 协议。 - **握手过程**:`WebSocket` 需要特殊的 `HTTP` 升级头来转换协议,`SSE` 无需特殊握手,直接通过 `HTTP` 请求建立连接。 - **连接保持**:`WebSocket` 连接保持直到显式关闭,`SSE` 可能因网络问题断开,但浏览器会自动重连。 - **数据格式**:`WebSocket` 支持多种数据格式,包括二进制数据,`SSE` 数据格式较固定,通常是简单的文本消息。 - **跨域支持**:`WebSocket` 建立连接时检查跨域策略,连接后不受限,`SSE` 依赖于 `CORS` 策略。 选择使用 `WebSocket` 还是 `SSE` 主要取决于具体的应用需求: - 如果需要实现双向通信或处理大量数据流,`WebSocket` 是更好的选择; - 如果只是需要服务器向客户端推送更新,且对数据格式要求不高,`SSE` 可能更加轻量和易于实现。 --- # 6.37 WebSocketClient 客户端 > 原文链接:https://http.furion.net/docs/advanced-guide/the-websocketclient-client/ 框架内置了 `WebSocketClient` 类型,便于用户通过 `ws` 或 `wss` 协议与 `WebSocket` 服务器建立连接。要使用 `WebSocket` 功能,首先需要创建并初始化 `WebSocketClient` 的实例。以下详尽列出了 `WebSocketClient` 实例所具备的全部功能配置选项: - **创建 `WebSocketClient` 客户端** 以下是创建 `WebSocketClient` 实例的三种方式,它们分别通过不同的构造函数重载实现,但实质上最终都调用了带有 `WebSocketClientOptions` 参数的构造函数来配置连接: ```cs showLineNumbers {2,5,8,11} // 直接使用 URL 字符串(支持 ws:// 和 wss://) using var webSocketClient = new WebSocketClient("wss://localhost:7044/ws"); // 使用 Uri 对象 using var webSocketClient = new WebSocketClient(new Uri("wss://localhost:7044/ws")); // 使用 WebSocketClientOptions 对象进行详细配置 using var webSocketClient = new WebSocketClient(new WebSocketClientOptions("wss://localhost:7044/ws")); // 配置内部 ClientWebSocketOptions 实例 using var webSocketClient = new WebSocketClient("wss://localhost:7044/ws", options => {}); ``` `WebSocketClientOptions` 类型包含了多种配置属性,用于定制 [`ClientWebSocket`](https://learn.microsoft.com/zh-cn/dotnet/api/system.net.websockets.clientwebsocket?view=net-9.0) 连接的详细设置。`WebSocketClientOptions`,包含以下属性: - **属性**: - `ServerUri`:服务器地址(`Uri` 类型)。 - `ReconnectInterval`:重连的间隔时间(毫秒),默认值为 `2` 秒。(`TimeSpan` 类型)。 - `MaxReconnectRetries`:最大重连次数,默认 `10` 次。(`int` 类型)。 - `Timeout`:超时时间(`TimeSpan` 类型)。 - `ReceiveBufferSize`:接收服务器新消息缓冲区大小(以字节为单位的 `int` 类型)。 - `Configure`:用户配置内部 `ClientWebSocketOptions` 实例(`Action` 类型)。 --- - **`WebSocketClient` 客户端事件** `WebSocketClient` 客户端提供了多种事件,允许开发者在 `WebSocket` 通信的各个阶段插入自定义逻辑。以下示例展示了如何订阅这些事件: ```cs showLineNumbers {4,7,10,13,16,19,22,25,28,31} using var webSocketClient = new WebSocketClient("wss://localhost:7044/ws"); // 支持 ws:// 和 wss:// // 开始连接时触发事件 webSocketClient.Connecting += (s, e) => {}; // 连接成功时触发事件 webSocketClient.Connected += (s, e) => { }; // 开始重新连接时触发事件 webSocketClient.Reconnecting += (s, e) => { }; // 重新连接成功时触发事件 webSocketClient.Reconnected += (s, e) => { }; // 开始关闭连接时触发事件 webSocketClient.Closing += (s, e) => { }; // 关闭连接成功时触发事件 webSocketClient.Closed += (s, e) => { }; // 开始接收消息时触发事件 webSocketClient.ReceivingStarted += (s, e) => { }; // 停止接收消息时触发事件 webSocketClient.ReceivingStopped += (s, e) => { }; // 接收文本消息事件,result 为 WebSocketTextReceiveResult 类型 webSocketClient.TextReceived += (s, result) => { }; // 接收二进制消息事件,result 为 WebSocketBinaryReceiveResult 类型 webSocketClient.BinaryReceived += (s, result) => { }; ``` `WebSocketTextReceiveResult` 类型派生自 [`WebSocketReceiveResult`](https://learn.microsoft.com/zh-cn/dotnet/api/system.net.websockets.websocketreceiveresult?view=net-9.0) ,包含以下属性: - **属性**: - `Message`:文本消息(`string` 类型)。 - 其他属性参考 [WebSocketReceiveResult](https://learn.microsoft.com/zh-cn/dotnet/api/system.net.websockets.websocketreceiveresult?view=net-9.0) `WebSocketBinaryReceiveResult` 类型派生自 [`WebSocketReceiveResult`](https://learn.microsoft.com/zh-cn/dotnet/api/system.net.websockets.websocketreceiveresult?view=net-9.0) ,包含以下属性: - **属性**: - `Message`:二进制消息(`byte[]` 类型)。 - 其他属性参考 [WebSocketReceiveResult](https://learn.microsoft.com/zh-cn/dotnet/api/system.net.websockets.websocketreceiveresult?view=net-9.0) --- **`WebSocketClient` 客户端方法** `WebSocketClient` 类封装了与 `WebSocket` 服务器进行交互的关键操作,具体包括连接、发送消息和关闭连接三种方法。 ```cs showLineNumbers {4,7-9,12,15,16} using var webSocketClient = new WebSocketClient("wss://localhost:7044/ws"); // 支持 ws:// 和 wss:// // 连接服务器 await webSocketClient.ConnectAsync(cancellationToken); // 向服务器发送消息 await webSocketClient.SendAsync(message, endOfMessage, cancellationToken); // 发送字符串消息 await webSocketClient.SendAsync(byteArray, endOfMessage, cancellationToken); // 发送二进制消息 await webSocketClient.SendAsync(message, webSocketMessageType, endOfMessage, cancellationToken); // 发送指定类型的消息(文本或二进制) // 等待消息(阻塞) await webSocketClient.WaitAsync(cancellationToken); // 关闭连接 await webSocketClient.CloseAsync(cancellationToken); // 无附加信息关闭 await webSocketClient.CloseAsync(closeStatus, closeDescription, cancellationToken); // 提供关闭状态和描述信息关闭 ``` --- # 6.38 HttpContext 转发和代理 > 原文链接:https://http.furion.net/docs/advanced-guide/httpcontext-forwarding-and-proxying/ `HttpContext` 转发是指在 `ASP.NET Core` 应用程序中,将一个 `HTTP` 请求的上下文信息(包括请求标头、请求内容、查询字符串、响应标头、响应内容等)从一个请求转发到另一个内部请求或服务的过程。这种技术允许开发者在不改变客户端请求的情况下,将请求重定向到另一个处理点,从而实现请求的代理或路由功能。 `HttpContext` 转发的应用场景: - **`API Gateway` 模式**:作为所有外部请求的入口点,将请求路由到正确的后端服务。 - **负载均衡和故障转移**:将请求转发到其他可用的服务实例,确保系统的稳定性和可靠性。 - **请求日志记录和审计**:将请求信息记录到日志系统或审计服务,便于监控和调试。 - **安全过滤和验证**:在转发过程中检查请求的认证信息和权限,确保请求的合法性。 - **A/B 测试和蓝绿部署**:将部分流量路由到新版本的服务,逐步验证新功能。 - **跨域请求处理**:处理跨域请求,确保请求能够成功执行。 在使用 `HttpContext` 进行转发操作之前,请确保已完成以下两个步骤: > **独立库说明** 若您使用 `HttpAgent` 独立库,请安装 `HttpAgent.AspNetCore` 以替代 `HttpAgent`。 1. 注册并启用 `IHttpContextAccessor` 服务。 在 `Startup.cs` 或 `Program.cs` 文件中注册并启用 `IHttpContextAccessor` 服务,并配置转发目标白名单。 ```cs showLineNumbers {1,4,7} services.AddHttpContextAccessor(); // 若使用 Furion 框架无需注入(已默认注入) // 全局配置 HttpContext 转发配置选项 services.Configure(options => { // 允许转发的目标主机白名单,必须显式配置;若未配置或为空,任何通过 X-Forward-To 头的转发都会被拒绝 options.AllowedHosts = ["*"]; // "*" 表示允许所有主机和协议(高风险,仅推荐在信任环境使用) }); ``` **`AllowedHosts` 白名单规则详解:** - `"furion.net"` — 仅主机名,匹配任意协议(`http/https`)的默认端口(`80/443`)。 - `"furion.net:8080"` — 主机+端口,匹配任意协议的指定端口。 - `"furion.net:*"` — 主机+端口通配符,匹配任意协议下的任意端口。 - `"https://furion.net"` — 协议+主机,只匹配指定协议的默认端口。 - `"http://furion.net:8080"` — 协议+主机+端口,精确匹配。 - `"https://furion.net:*"` — 协议+主机+端口通配符,只匹配指定协议的任意端口。 - `"[::1]"` — `IPv6` 主机(方括号包裹),匹配任意协议的默认端口。 - `"[::1]:8080"` — `IPv6` 主机+端口,匹配任意协议的指定端口。 - `"[::1]:*"` — `IPv6` 主机+端口通配符,匹配任意协议下的任意端口。 - `"http://[2001:db8::1]:8080"` — 协议+IPv6 主机+端口,精确匹配。 - `"*"` — 全局通配符,允许任意主机和协议(完全绕过所有主机验证)。 > **安全风险提示** - **务必显式配置 `AllowedHosts`**,留空或未配置将拒绝所有 `X-Forward-To` 转发请求,防止 `SSRF` 攻击。 - 使用全局通配符 `*` 会使应用完全暴露在 `SSRF` 风险之下,请仅在完全信任请求来源(如内网管理服务)且了解风险时启用。 - 建议尽可能使用最严格的规则(如指定协议和端口),并配合网络防火墙限制出站流量。 - 所有主机名和协议匹配均不区分大小写,防止大小写混淆绕过。 2. 启用请求正文缓存中间件,以支持请求内容的重复读取。 ```cs showLineNumbers app.UseEnableBuffering(); ``` 3. **(可选)** 若在转发过程中出现 `The SSL connection could not be established, see inner exception.` 的证书错误问题,您可以通过添加以下配置来忽略 `SSL` 证书验证: ```cs showLineNumbers {3,6-7,12,14,17-18} // 默认客户端配置 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { // 忽略 SSL 证书验证 ServerCertificateCustomValidationCallback = HttpRemoteUtility.IgnoreSslErrors, SslProtocols = HttpRemoteUtility.AllSslProtocols }); // 若使用 SocketsHttpHandler,可以通过以下配置来忽略 SSL 证书验证 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler() { SslOptions = new SslClientAuthenticationOptions { // 忽略 SSL 证书验证 RemoteCertificateValidationCallback = HttpRemoteUtility.IgnoreSocketSslErrors, EnabledSslProtocols = HttpRemoteUtility.AllSslProtocols }, }); ``` 以下是一个简单的示例,展示了如何在 `ASP.NET Core` 中实现 `HttpContext` 转发: ```cs showLineNumbers {3,10,18-19,27-28,35-35} [ApiController] [Route("[controller]/[action]")] public class GetStartController(IHttpRemoteService httpRemoteService, IHttpContextAccessor httpContextAccessor) : ControllerBase { // 转发代理到网站 [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // 禁用浏览器缓存 public Task ForwardToWebSite() { return httpContextAccessor.HttpContext.ForwardAsResultAsync("https://github.com"); } // 转发代理到图片 [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // 禁用浏览器缓存 public Task ForwardToImage() { return httpContextAccessor.HttpContext.ForwardAsResultAsync( "https://img-s-msn-com.akamaized.net/tenant/amp/entityid/AA1u7RJI.img?w=584&h=326&m=6"); } // 转发代理到下载 [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // 禁用浏览器缓存 public Task ForwardToDownload() { return httpContextAccessor.HttpContext.ForwardAsResultAsync( "https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe"); } // 转发代理到表单 [HttpPost] public Task ForwardToForm(int id, [FromForm] YourRemoteFormModel model) { return httpContextAccessor.HttpContext.ForwardAsAsync( "https://localhost:7044/HttpRemote/AddForm"); } } ``` > **`X-Forward-To` 请求标头** 除了可以手动配置转发目标地址外,系统还支持通过解析 `X-Forward-To` 请求标头来自动设置目标地址。**注意**:使用该标头时,目标主机必须在 `AllowedHosts` 白名单中,否则转发将被拒绝。 通过 `HttpContext` 转发,可以在 `ASP.NET Core` 应用程序中结合 `Middleware` 中间件技术实现灵活的请求路由和处理机制,适用于多种应用场景,如 `API Gateway`、负载均衡、请求日志记录、安全验证等。 > **`GET` 请求转发失败的可能原因** 在某些特殊应用场景中,例如通过 `GET` 请求转发至特定文件或图片时,可能会出现转发失败的情况。这可能是由于 `TLS/SSL` 证书问题导致的。此时,请确保用于转发的目标应用通过 `HTTPS` 协议部署网站。 --- # 6.39 HttpContextForwardBuilder 构建器 > 原文链接:https://http.furion.net/docs/advanced-guide/the-httpcontextforwardbuilder-builder/ `HttpContextForwardBuilder` 构建器是框架提供专门用来转换 `HttpContext` 请求上下文所需的各项设置。`HttpContextForwardBuilder` 的构造函数是私有的,因此无法直接使用 `new` 关键字进行实例化,不过,框架提供了 `HttpContext.CreateForwardBuilder` 的多个扩展重载方法创建 `HttpContextForwardBuilder` 的实例。 ```cs showLineNumbers httpContext.CreateForwardBuilder(httpMethod, requestUri, forwardOptions); // forwardOptions 参数的类型为 HttpContextForwardOptions httpContext.CreateForwardBuilder(requestUri, forwardOptions); // 自动获取 HttpContext.Request.Method 方法进行设置 ``` 框架内部尚未提供手动构建 `HttpContextForwardBuilder` 并使用 `Send/SendAsync` 方法进行转发的功能,请改用 `ForwardAsync` 相关方法。 --- # 6.40 HttpContextForwardOptions 配置选项 > 原文链接:https://http.furion.net/docs/advanced-guide/httpcontextforwardoptions-configuration-options/ `HttpContextForwardOptions` 类型专门用于配置 `HttpContext` 的转发行为。您可以在项目的 `Startup.cs` 或 `Program.cs` 文件中注册并配置该服务: ```cs showLineNumbers {2-3,9} // 在 HttpRemote 服务中注册 services.AddHttpRemote(builder => {}) .ConfigureForwardOptions(options => // .ConfigureForwardOptions((options, serviceProvider) => { // 在此处添加自定义配置 }); // 在 services 中注册 services.Configure(options => { // 在此处添加自定义配置 }); ``` 此外,您也可以手动创建 `HttpContextForwardOptions` 实例,并在进行转发时将其传入: ```cs showLineNumbers {1} httpContext.ForwardAsResult("https://furion.net", forwardOptions: new HttpContextForwardOptions { // 在此处添加自定义配置 }); ``` `HttpContextForwardOptions` 包含以下属性: - **属性**: - **`AllowedHosts`**:允许转发的目标主机白名单(`ICollection?` 类型)。 用于防范 **服务端请求伪造(`SSRF`)** 攻击。仅当目标地址的主机(含端口、协议)与列表中的某一项匹配时,转发才会被允许。 **支持的格式**(匹配均忽略大小写): - `"furion.net"` – 仅主机名,匹配任意协议(`http/https`)的默认端口(`80/443`)。 - `"furion.net:8080"` – 主机+端口,匹配任意协议的指定端口。 - `"furion.net:*"` – 主机+端口通配符,匹配任意协议下的任意端口。 - `"https://furion.net"` – 协议+主机,只匹配指定协议的默认端口。 - `"http://furion.net:8080"` – 协议+主机+端口,精确匹配。 - `"https://furion.net:*"` – 协议+主机+端口通配符,只匹配指定协议的任意端口。 - `"*"` – 全局通配符,允许任意主机和协议(完全绕过验证,**高风险**)。 **若未配置或为空,所有通过 `X-Forward-To` 请求头指定的目标地址都将被拒绝**,以防止未经授权的转发。建议尽可能使用精确规则,仅对完全可信的来源开放通配符。 - `WithQueryParameters`:是否转发查询参数(`URL` 参数),默认值为 `true`(`bool` 类型)。 - `WithRequestHeaders`:是否转发请求标头,默认值为 `true`(`bool` 类型)。 - `WithResponseStatusCode`:是否转发响应状态码,默认值为 `true`(`bool` 类型)。 - `WithResponseHeaders`:是否转发响应标头,默认值为 `true`(`bool` 类型)。 - `WithResponseContentHeaders`:是否转发响应内容标头,默认值为 `true`(`bool` 类型)。 - `ResetHostRequestHeader`:是否重新设置 `Host` 请求标头,默认值为 `false`(`bool` 类型)。 - `IgnoreQueryParameters`:忽略在转发时需要跳过的查询参数(`URL` 参数)列表(`string[]?` 类型)。 - `IgnoreRequestHeaders`:忽略在转发时需要跳过的请求标头列表(`string[]?` 类型)。 - `IgnoreResponseHeaders`:忽略在转发时需要跳过的响应标头列表(`string[]?` 类型)。 - `OnForward`:用于在转发响应之前执行自定义操作(`Action` 类型)。 > **安全风险提示** - `AllowedHosts` 是防止 `SSRF` 攻击的核心配置,务必在生产环境中显式设置,且避免使用 `"*"` 通配符。 - `ResetHostRequestHeader` 在某些目标服务器要求校验 `Host` 头时可能需要开启,其默认值为 `false`,请根据实际情况调整。 --- # 6.41 HttpContext 转发扩展方法 > 原文链接:https://http.furion.net/docs/advanced-guide/httpcontext-forwarding-extension-methods/ 框架为 `HttpContext` 提供了多种扩展方法,旨在满足各种场景下的 `HTTP` 请求转发需求。 ```cs showLineNumbers {1,7,13,19,25,31,37,43} // 返回 HttpResponseMessage 对象 httpContext.Forward(requestUri, configure, completionOption, forwardOptions); // 自动获取 HttpContext.Request.Method 方法进行设置 httpContext.Forward(httpMethod, requestUri, configure, completionOption, forwardOptions); await httpContext.ForwardAsync(requestUri, configure, completionOption, forwardOptions); // 自动获取 HttpContext.Request.Method 方法进行设置 await httpContext.ForwardAsync(httpMethod, requestUri, configure, completionOption, forwardOptions); // 返回 HttpRemoteResult httpContext.Forward(requestUri, configure, completionOption, forwardOptions); // 自动获取 HttpContext.Request.Method 方法进行设置 httpContext.Forward(httpMethod, requestUri, configure, completionOption, forwardOptions); await httpContext.ForwardAsync(requestUri, configure, completionOption, forwardOptions); // 自动获取 HttpContext.Request.Method 方法进行设置 await httpContext.ForwardAsync(httpMethod, requestUri, configure, completionOption, forwardOptions); // 返回目标 T 类型 httpContext.ForwardAs(requestUri, configure, completionOption, forwardOptions); // 自动获取 HttpContext.Request.Method 方法进行设置 httpContext.ForwardAs(httpMethod, requestUri, configure, completionOption, forwardOptions); await httpContext.ForwardAsAsync(requestUri, configure, completionOption, forwardOptions); // 自动获取 HttpContext.Request.Method 方法进行设置 await httpContext.ForwardAsAsync(httpMethod, requestUri, configure, completionOption, forwardOptions); // 返回字符串类型 httpContext.ForwardAsString(requestUri, configure, completionOption, forwardOptions); // 自动获取 HttpContext.Request.Method 方法进行设置 httpContext.ForwardAsString(httpMethod, requestUri, configure, completionOption, forwardOptions); await httpContext.ForwardAsStringAsync(requestUri, configure, completionOption, forwardOptions); // 自动获取 HttpContext.Request.Method 方法进行设置 await httpContext.ForwardAsStringAsync(httpMethod, requestUri, configure, completionOption, forwardOptions); // 返回字节数组类型 httpContext.ForwardAsByteArray(requestUri, configure, completionOption, forwardOptions); // 自动获取 HttpContext.Request.Method 方法进行设置 httpContext.ForwardAsByteArray(httpMethod, requestUri, configure, completionOption, forwardOptions); await httpContext.ForwardAsByteArrayAsync(requestUri, configure, completionOption, forwardOptions); // 自动获取 HttpContext.Request.Method 方法进行设置 await httpContext.ForwardAsByteArrayAsync(httpMethod, requestUri, configure, completionOption, forwardOptions); // 返回 Stream 类型 httpContext.ForwardAsStream(requestUri, configure, completionOption, forwardOptions); // 自动获取 HttpContext.Request.Method 方法进行设置 httpContext.ForwardAsStream(httpMethod, requestUri, configure, completionOption, forwardOptions); await httpContext.ForwardAsStreamAsync(requestUri, configure, completionOption, forwardOptions); // 自动获取 HttpContext.Request.Method 方法进行设置 await httpContext.ForwardAsStreamAsync(httpMethod, requestUri, configure, completionOption, forwardOptions); // 返回 IActionResult 类型 httpContext.ForwardAsResult(requestUri, configure, completionOption, forwardOptions); // 自动获取 HttpContext.Request.Method 方法进行设置 httpContext.ForwardAsResult(httpMethod, requestUri, configure, completionOption, forwardOptions); await httpContext.ForwardAsResultAsync(requestUri, configure, completionOption, forwardOptions); // 自动获取 HttpContext.Request.Method 方法进行设置 await httpContext.ForwardAsResultAsync(httpMethod, requestUri, configure, completionOption, forwardOptions); // 返回 object 类型 httpContext.ForwardAs(resultType, requestUri, configure, completionOption, forwardOptions); // 自动获取 HttpContext.Request.Method 方法进行设置 httpContext.ForwardAs(resultType, httpMethod, requestUri, configure, completionOption, forwardOptions); await httpContext.ForwardAsAsync(resultType, requestUri, configure, completionOption, forwardOptions); // 自动获取 HttpContext.Request.Method 方法进行设置 await httpContext.ForwardAsAsync(resultType, httpMethod, requestUri, configure, completionOption, forwardOptions); ``` > **转发结果为 `IActionResult` 类型** 使用转发返回 `IActionResult` 类型时,您可以灵活地转发各种内容,涵盖网页、图片、资源下载与上传、视频、音频、`JSON` 数据以及文件等。 --- # 6.42 HttpContext 转发的优势 > 原文链接:https://http.furion.net/docs/advanced-guide/advantages-of-httpcontext-forwarding/ 在对接第三方 `API` 接口时,通常的做法是创建一个入口程序,并在其中调用 `HTTP` 远程请求服务来发送请求到指定的第三方接口。假设第三方接口的控制器定义如下: ```cs showLineNumbers {5-9} [ApiController] [Route("[controller]/[action]")] public class VendorController : ControllerBase { [HttpPost] public VendorModel Add(VendorModel model) { return model; } } ``` 传统的做法是使用 `HTTP` 远程请求服务来发送请求,例如: ```cs showLineNumbers {3,8-11} [ApiController] [Route("[controller]/[action]")] public class YourController(IHttpRemoteService httpRemoteService) : ControllerBase { [HttpPost] public async Task AddVendorAsync() { return await httpRemoteService.SendAsync( HttpRequestBuilder.Post("https://www.furion.net/vendor/add") .SetJsonContent(new VendorModel()) ); } } ``` 然而,使用 `HttpContext` 转发功能后,我们可以简化代码,只需创建与第三方接口一致的接口控制器声明,例如 `VendorController` 的 `Add` 方法,如下所示: ```cs showLineNumbers {3,9-10} [ApiController] [Route("[controller]/[action]")] public class YourController(IHttpContextAccessor httpContextAccessor) : ControllerBase { [HttpPost] public async Task AddAsync(VendorModel model) // 同步和异步都行 { // 自动转发 model,无需任何设置 return await httpContextAccessor.Context .ForwardAsync("https://www.furion.net/vendor/add"); } } ``` 通过这种方式,代码变得更加简洁明了,无需手动构建和发送 `HTTP` 请求,而是利用 `HttpContext` 转发功能直接调用第三方接口。 > **利用 `X-Forward-To` 请求标头提升代码灵活性** 除了显式地在代码中设置目标地址外,我们还可以通过客户端请求的 `X-Forward-To` 标头来自动获取转发地址。假设客户端已经设置了请求头 `X-Forward-To: https://www.furion.net/vendor/add`,那么我们的代码可以进一步简化,如下所示: ```cs showLineNumbers {3,9} [ApiController] [Route("[controller]/[action]")] public class YourController(IHttpContextAccessor httpContextAccessor) : ControllerBase { [HttpPost] public async Task AddAsync(VendorModel model) // 同步和异步都行 { // 自动从请求中解析 X-Forward-To 标头地址 return await httpContextAccessor.Context.ForwardAsync(); } } ``` 通过这种方式,代码变得更加简洁且灵活,能够实现动态地将任何内容转发到目标地址。这种方法的潜力巨大,你可以根据自己的需求进行探索和尝试。 --- # 6.43 HttpContext 转发在微服务中的应用 > 原文链接:https://http.furion.net/docs/advanced-guide/applying-httpcontext-forwarding-in-microservices/ 在微服务架构中,`HttpContext` 转发功能展现出了极大的价值。微服务间的通信通常依赖于 `HTTP` 或 `gRPC`,其中 `HTTP` 由于其出色的兼容性而被广泛应用。采用 `HttpContext` 转发不仅能够简化 `HTTP` 请求的代码量,还能使代码结构更加清晰,便于维护。对于大型项目或团队协作项目,这一优势尤为明显。 借助 `HttpContext` 转发功能,我们可以实现动态的请求分发。通过应用某种权重算法,系统能够自动将请求转发至不同的服务器,从而实现负载均衡和故障转移。例如: ```cs showLineNumbers {3,13-26} [ApiController] [Route("[controller]/[action]")] public class YourController(IHttpContextAccessor httpContextAccessor) : ControllerBase { [HttpPost] public async Task AddAsync(VendorModel model) { string targetUrl = "https://furion.net/"; // 默认服务器地址 // 根据某种算法(如负载均衡策略)选择目标服务器地址 // 在微服务架构中,这通常由服务注册与发现机制来确定 // 下面的代码仅为模拟示例 if (条件1) { targetUrl = "https://s1.furion.net/"; // s1 服务器地址 } else if (条件2) { targetUrl = "https://s2.furion.net/"; // s2 服务器地址 } else if (条件3) { targetUrl = "https://s3.furion.net/"; // s3 服务器地址 } return await _httpContextAccessor.ForwardAsync(targetUrl); } } ``` 此外,**`HttpContext` 转发功能还使得构建网关中心成为可能。所有外部请求都可以先发送到网关中心,由网关进行鉴权、限流等处理后,再转发至目标服务**。这极大地提升了系统的安全性和可管理性。总之,`HttpContext` 转发是微服务架构中一个不可或缺的组件,它为实现高效、灵活的微服务通信提供了有力支持。 --- # 6.44 转发忽略标头说明 > 原文链接:https://http.furion.net/docs/advanced-guide/notes-on-headers-ignored-during-forwarding/ 在使用 `HttpContext` 转发功能时,系统会自动忽略以下请求与响应标头,以确保转发的有效性和准确性: - **请求标头中将被忽略的**: - `X-Forward-To` - `Host` - `Content-Length` - **响应标头中将被忽略的**: - `Content-Type` - `Transfer-Encoding` - `Keep-Alive` - `Upgrade` - `Proxy-Connection` 若需添加更多忽略请求或响应标头的设置,可通过 `HttpContextForwardOptions` 进行配置。以下是两种配置方式: - **全局设置**: 在项目的 `Startup.cs` 或 `Program.cs` 文件中,注册并配置该服务: ```cs showLineNumbers {1,4,7} services.Configure(options => { // 忽略在转发时需要跳过的请求标头列表 options.IgnoreRequestHeaders = ["Framework"]; // 忽略在转发时需要跳过的响应标头列表 options.IgnoreResponseHeaders = ["Content-Length"]; }); ``` - **单次转发设置**: 在单次转发时,可传递 `HttpContextForwardOptions` 进行配置: ```cs showLineNumbers {1,4,7} httpContext.ForwardAsResult("https://furion.net", forwardOptions: new HttpContextForwardOptions { // 忽略在转发时需要跳过的请求标头列表 options.IgnoreRequestHeaders = ["Framework"]; // 忽略在转发时需要跳过的响应标头列表 options.IgnoreResponseHeaders = ["Content-Length"]; }); ``` > **关于 `Content-Length` 响应内容标头的转发** 若响应标头中包含 `Content-Length`,且其值与实际响应内容大小不一致,可能会导致 `“Error while copying content to a stream.”` 错误。忽略此标头有助于避免因长度不匹配引起的错误。 --- # 6.45 ForwardAttribute 转发特性 > 原文链接:https://http.furion.net/docs/advanced-guide/the-forwardattribute-forwarding-attribute/ 为了简化转发操作,框架提供了便捷的 `[Forward]` 控制器操作转发特性。相较于手动调用 `HttpContext` 的 `Forward` 扩展方法,该特性显著减少了重复的硬编码工作。以下是使用 `[Forward]` 特性的示例代码: ```cs showLineNumbers {11,14,23,26,35,38,48,52,59,62,70,73} [ApiController] [Route("[controller]/[action]")] public class GetStartController : ControllerBase { /// /// 转发代理到网站 /// /// [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // 禁用浏览器缓存 [Forward("https://github.com", AllowedHosts = ["*"])] public Task ForwardToWebSite() { throw new NotImplementedException(); } /// /// 转发代理到图片 /// /// [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // 禁用浏览器缓存 [Forward("https://img-s-msn-com.akamaized.net/tenant/amp/entityid/AA1u7RJI.img?w=584&h=326&m=6", AllowedHosts = ["*"])] public Task ForwardToImage() { throw new NotImplementedException(); } /// /// 转发代理到文件 /// /// [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // 禁用浏览器缓存 [Forward("https://download.visualstudio.microsoft.com/download/pr/a17b907f-8457-45a8-90db-53f2665ee49e/49bccd33593ebceb2847674fe5fd768e/aspnetcore-runtime-8.0.10-win-x64.exe", AllowedHosts = ["*"])] public Task ForwardToDownload() { throw new NotImplementedException(); } /// /// 转发代理到表单 /// /// /// /// [HttpPost] [Forward("https://localhost:7044/HttpRemote/AddForm", AllowedHosts = ["*"])] public Task ForwardToForm(int id, [FromForm] YourRemoteFormModel model) { throw new NotImplementedException(); } /// /// 转发代理到字符串 /// /// [HttpGet] [Forward("https://localhost:7044/GetStart/PostRawString", AllowedHosts = ["*"])] public Task ForwardToString() { throw new NotImplementedException(); } /// /// 转发代理到无返回值 /// /// [HttpGet] [Forward("https://localhost:7044/GetStart/PostRawString", AllowedHosts = ["*"])] public Task ForwardToVoid() { throw new NotImplementedException(); } } ``` 在上述代码中,我们只需为需要转发的控制器操作添加 `[Forward]` 特性,并指定目标 `URL`。框架会自动处理转发逻辑,因此方法体内无需编写任何实现代码(通常抛出 `NotImplementedException` 以表明这是一个由框架自动处理的转发操作)。这种方式在微服务应用中尤为便捷,极大地简化了代码编写和维护工作。 > **`ForwardAttribute` 特性作用范围** `ForwardAttribute` 特性适用于方法。 `ForwardAttribute` 包含以下属性: - **属性**: - `RequestUri`:转发地址(`string` 类型)。 - `Method`:转发方式,若未设置,则自动采用当前请求方式作为转发方式(`HttpMethod` 类型)。 - `HttpClientName`:`HttpClient` 实例的配置名称,默认值为 `null`(`string` 类型)。 - `CompletionOption`:指示响应内容操作方式,默认值为 `ResponseHeadersRead`(`HttpCompletionOption` 类型)。 - **`AllowedHosts`**:允许转发的目标主机白名单(`string[]?` 类型)。 用于防范 **服务端请求伪造(`SSRF`)** 攻击。仅当目标地址的主机(含端口、协议)与列表中的某一项匹配时,转发才会被允许。 **支持的格式**(匹配均忽略大小写): - `"furion.net"` – 仅主机名,匹配任意协议(`http/https`)的默认端口(`80/443`)。 - `"furion.net:8080"` – 主机+端口,匹配任意协议的指定端口。 - `"furion.net:*"` – 主机+端口通配符,匹配任意协议下的任意端口。 - `"https://furion.net"` – 协议+主机,只匹配指定协议的默认端口。 - `"http://furion.net:8080"` – 协议+主机+端口,精确匹配。 - `"https://furion.net:*"` – 协议+主机+端口通配符,只匹配指定协议的任意端口。 - `"*"` – 全局通配符,允许任意主机和协议(完全绕过验证,**高风险**)。 **若未配置或为空,所有通过 `X-Forward-To` 请求头指定的目标地址都将被拒绝**,以防止未经授权的转发。建议尽可能使用精确规则,仅对完全可信的来源开放通配符。 - `WithQueryParameters`:是否转发查询参数(`URL` 参数),默认值为 `true`(`bool` 类型)。 - `WithRequestHeaders`:是否转发请求标头,默认值为 `true`(`bool` 类型)。 - `WithResponseStatusCode`:是否转发响应状态码,默认值为 `true`(`bool` 类型)。 - `WithResponseHeaders`:是否转发响应标头,默认值为 `true`(`bool` 类型)。 - `WithResponseContentHeaders`:是否转发响应内容标头,默认值为 `true`(`bool` 类型)。 - `ResetHostRequestHeader`:是否重新设置 `Host` 请求标头,默认值为 `false`(`bool` 类型)。 - `IgnoreQueryParameters`:忽略在转发时需要跳过的查询参数(`URL` 参数)列表(`string[]?` 类型)。 - `IgnoreRequestHeaders`:忽略在转发时需要跳过的请求标头列表(`string[]?` 类型)。 - `IgnoreResponseHeaders`:忽略在转发时需要跳过的响应标头列表(`string[]?` 类型)。 > **安全风险提示** - `AllowedHosts` 是防止 `SSRF` 攻击的核心配置,务必在生产环境中显式设置,且避免使用 `"*"` 通配符。 - `ResetHostRequestHeader` 在某些目标服务器要求校验 `Host` 头时可能需要开启,其默认值为 `false`,请根据实际情况调整。 --- # 6.46 HTTP 请求管道处理器 > 原文链接:https://http.furion.net/docs/advanced-guide/pipeline/ `HTTP` 请求管道处理器是框架发送 `HTTP` 远程请求的核心机制,实际发出请求的最终执行逻辑即由这一系列处理器协作完成。几乎所有关键特性——如自动重定向、请求分析、超时管理、重试策略、异常抑制、请求断言及 `Access Token` 自动管理等——均由独立的管道处理器实现。每个处理器只关注单一职责,便于扩展和维护。 若要自定义管道处理器,只需实现 `IHttpRequestPipelineHandler` 接口。例如,以下示例实现了一个简单的打印处理器,在请求前后输出日志: ```cs showLineNumbers {1,4,6,9,11} internal sealed class PrintPipelineHandler : IHttpRequestPipelineHandler { /// public async Task HandleAsync(HttpRequestPipelineContext context, Func> next) { Console.WriteLine("请求之前"); // 调用下一个处理器的委托 var httpResponseMessage = await next(); Console.WriteLine("接收响应"); return httpResponseMessage; } } ``` 接着,在 `Startup.cs` 或 `Program.cs` 中配置 `HttpRemote` 服务时,通过 `AddPipelineHandler` 注册该处理器即可启用: ```cs showLineNumbers {3} services.AddHttpRemote(builder => { builder.AddPipelineHandler(); }); ``` 注册后,每个远程请求在发出前会打印“请求之前”,收到响应后会打印“接收响应”,从而直观地观察请求生命周期。 > **请求管道处理器的执行顺序** 注册时,**最后一个**注册的处理器会被放置在处理器集合的**头部**,即管道的最外层;**第一个**注册的则位于最内层。整体执行顺序遵循**从外到内**的原则:列表中的第一个类型最先执行(最外层),最后一个类型最后执行(最内层)。 这种设计使得外层处理器可以统一处理横切关注点(如日志、鉴权),而内层处理器专注于核心请求逻辑。 `HttpRequestPipelineContext` 包含以下属性: - **属性**: - `OriginalBuilder`:原始的 `HttpRequestBuilder`(`HttpRequestBuilder` 类型)。 - `Builder`:当前使用的 `HttpRequestBuilder`(`HttpRequestBuilder` 类型)。 - `HttpClient`:`HttpClient` 实例(`HttpClient` 类型)。 - `CompletionOption`:指示响应内容操作方式(`HttpCompletionOption` 类型)。 - `CancellationToken`:当前有效的取消操作标识(`CancellationToken` 类型)。 - `SendAsync`:发送委托(`Func>` 类型)。**真正发出 `HTTP` 请求的委托。** - `RequestMessage`:最近一次构建的 `HttpRequestMessage`(`HttpRequestMessage?` 类型)。 - `ResponseMessage`:最近一次构建的 `HttpResponseMessage`(`HttpResponseMessage?` 类型)。 - `RequestDuration`:请求耗时(毫秒)(`long` 类型)。 - `Items`:共享数据字典(`IDictionary` 类型) 若需了解框架内置的所有请求管道处理器,可参阅仓库源码:[https://gitee.com/dotnetchina/HttpAgent/tree/master/src/HttpAgent/src/Pipelines/Handlers](https://gitee.com/dotnetchina/HttpAgent/tree/master/src/HttpAgent/src/Pipelines/Handlers) --- # 6.47 FTP 客户端功能展望 > 原文链接:https://http.furion.net/docs/advanced-guide/ftp/ > **`FTP` 客户端功能说明** 自 2019 年谷歌 Chrome 浏览器宣布停止对 `FTP` 协议的支持后,2022 年微软也紧随其后,在其 Edge 浏览器中移除了对 `FTP` 协议的支持。 尽管当前我们的框架尚未正式集成 `FTP` 客户端功能,但相关开发工作已初步完成。若未来开发者对 `FTP` 协议功能的需求日益增长,我们将认真考虑将其纳入框架,以满足广大开发者的需求。 --- # 6.48 CancellationToken 取消 HTTP 请求 > 原文链接:https://http.furion.net/docs/advanced-guide/cancellation/ 框架为所有发送 `HTTP` 远程请求的方法提供了取消功能,只需通过 `cancellationToken` 参数即可实现。以下是三种取消请求的方法示例: - **在控制器 `Action` 中定义 `CancellationToken` 参数**: 当用户关闭浏览器或中断请求时,可以取消 `HTTP` 远程请求操作。 ```cs showLineNumbers {3,6,8} [ApiController] [Route("[controller]/[action]")] public class HttpRemoteController(IHttpRemoteService httpRemoteService) : ControllerBase { [HttpGet] public async Task GetContent(CancellationToken cancellationToken) { await httpRemoteService.GetAsAsync("https://furion.net/", cancellationToken: cancellationToken); } } ``` - **利用 `HttpContext.RequestAborted` 属性**: 当用户关闭浏览器或中断请求时,可以取消 `HTTP` 远程请求操作。 ```cs showLineNumbers {4,10} [ApiController] [Route("[controller]/[action]")] public class HttpRemoteController(IHttpRemoteService httpRemoteService, IHttpContextAccessor httpContextAccessor) : ControllerBase { [HttpGet] public async Task GetContent() { await httpRemoteService.GetAsAsync("https://furion.net/" , cancellationToken: httpContextAccessor.HttpContext.RequestAborted); } } ``` - **创建 `CancellationTokenSource` 对象以手动取消请求**: 创建 `CancellationTokenSource` 实例可以精准的控制取消 `HTTP` 请求的时机。 ```cs showLineNumbers {8-9,12} [ApiController] [Route("[controller]/[action]")] public class HttpRemoteController(IHttpRemoteService httpRemoteService) : ControllerBase { [HttpGet] public async Task GetContent() { using var cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource.CancelAfter(100); await httpRemoteService.GetAsAsync("https://furion.net/" , cancellationToken: cancellationTokenSource.Token); } } ``` --- # 6.49 HttpClient 实例配置 > 原文链接:https://http.furion.net/docs/advanced-guide/httpclient-instance-configuration/ `HTTP` 远程请求服务内部采用 `HttpClient` 发送请求。通过配置 `HttpClient` 客户端,可以调整框架发送 `HTTP` 远程请求的行为。您可以通过以下两种方式配置客户端行为: **全局配置** 在 `Startup.cs` 或 `Program.cs` 中配置所有请求或特定名称的客户端: ```cs showLineNumbers {2,5,8} // 为默认客户端启用 services.AddHttpClient(string.Empty, client => {}); // 为特定客户端启用 services.AddHttpClient("weixin", client => {}); // 若需解析服务,可使用以下重载方法 services.AddHttpClient(string.Empty, (serviceProvider, client) => {}); ``` **单请求配置** 使用 `SetHttpClientProvider` 为特定请求自定义客户端: ```cs showLineNumbers {2-5} HttpRequestBuilder.Get("https://furion.net/") .SetHttpClientProvider(() => (new HttpClient(new HttpClientHandler { // ... }), client => client.Dispose())); ``` --- # 6.50 常见属性配置 > 原文链接:https://http.furion.net/docs/advanced-guide/common-property-configuration/ ```cs showLineNumbers {2,5,8,11,14,17,20,24} // 为默认客户端启用 services.AddHttpClient(string.Empty, client => { // 配置基地址 client.BaseAddress = new Uri("http://localhost:5000"); // 配置超时 client.Timeout = TimeSpan.FromMinutes(10); // 配置响应内容的最大缓存字节数 client.MaxResponseContentBufferSize = 5 * 1024; // 配置默认 HTTP 版本 client.DefaultRequestVersion = HttpVersion.Version10; // 配置默认请求标头,如 "User-Agent" client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0"); // 配置默认建立 HTTP 连接时使用的版本策略 client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; }); // 为特定客户端启用 services.AddHttpClient("weixin", client => { // 配置 HttpClient 属性 }); ``` --- # 6.51 配置 IHttpClientBuilder > 原文链接:https://http.furion.net/docs/advanced-guide/configuring-ihttpclientbuilder/ `.AddHttpClient(name, configure)` 方法返回一个 `IHttpClientBuilder` 实例,允许进行进一步的配置,比如设置 `HttpMessageHandler`。 ### 配置默认和特定客户端的示例 ```cs showLineNumbers {2-3,16-17} // 配置默认客户端 services.AddHttpClient(string.Empty, client => {}) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { // 允许自动重定向 AllowAutoRedirect = true, // 使用默认凭据 UseDefaultCredentials = true, // 启用 Cookie UseCookies = true, }); // 配置名为 "weixin" 的特定客户端 services.AddHttpClient("weixin", client => {}) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()); ``` ### 统一配置所有 `HttpClient` 实例 除了单独配置每个客户端,您还可以统一配置所有 `HttpClient` 实例: ```cs showLineNumbers {1,3,8,10} services.ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()); }); // 或使用 IHttpRemoteBuilder 扩展方法进行一键配置 services.AddHttpRemote() .ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()); }); ``` 这样的配置方式使代码更加简洁和模块化,便于维护和管理。 --- # 6.52 配置 SSL 证书 > 原文链接:https://http.furion.net/docs/advanced-guide/httpclient-ssl/ 使用 `HttpClient` 发起 `HTTPS` 请求时,如果需要配置自定义的 `SSL/TLS` 证书,通常涉及到使用 `HttpClientHandler` 类,并通过 `ServerCertificateCustomValidationCallback` 属性来指定一个回调方法,该方法用于验证服务器的证书。如果需要使用客户端证书,可以通过 `HttpClientHandler.ClientCertificates` 属性添加。 ### 客户端证书认证 ```cs showLineNumbers {5-8} services.AddHttpClient(string.Empty, client => {}) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { ClientCertificates = { X509CertificateLoader.LoadPkcs12FromFile("path/to/client_certificate.pfx", "password") } }); ``` ### 自定义服务器证书验证 如果您需要自定义服务器证书的验证逻辑,可以设置 `ServerCertificateCustomValidationCallback` 属性: ```cs showLineNumbers {5-15} services.AddHttpClient(string.Empty, client => {}) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { // 如果证书是预期的自签名证书,则接受它 if (cert.Subject == "CN=YourExpectedSubject") { return true; // 接受证书 } // 否则,使用默认的验证逻辑 return errors == System.Net.Security.SslPolicyErrors.None; } }); ``` ### 忽略 `SSL` 证书验证 除了配置 `SSL` 证书,您还可以通过添加以下配置来忽略 `SSL` 证书验证: ```cs showLineNumbers {3,6-7,12,14,17-18} // 默认客户端配置 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { // 忽略 SSL 证书验证 ServerCertificateCustomValidationCallback = HttpRemoteUtility.IgnoreSslErrors, SslProtocols = HttpRemoteUtility.AllSslProtocols }); // 若使用 SocketsHttpHandler,可以通过以下配置来忽略 SSL 证书验证 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler() { SslOptions = new SslClientAuthenticationOptions { // 忽略 SSL 证书验证 RemoteCertificateValidationCallback = HttpRemoteUtility.IgnoreSocketSslErrors, EnabledSslProtocols = HttpRemoteUtility.AllSslProtocols }, }); ``` ### 单次请求设置 `SSL` 证书 除了可以在全局配置中 `SSL` 证书验证外,您还可以通过 `SetHttpClientProvider` 方法为单次请求单独设置 `SSL` 证书。示例代码如下: ```cs showLineNumbers {2-7} HttpRequestBuilder.Get("https://furion.net/") .SetHttpClientProvider(() => (new HttpClient(new HttpClientHandler { // 忽略 SSL 证书验证 ServerCertificateCustomValidationCallback = HttpRemoteUtility.IgnoreSslErrors, SslProtocols = HttpRemoteUtility.AllSslProtocols }), client => client.Dispose())); ``` --- # 6.53 配置 Proxy 代理服务器 > 原文链接:https://http.furion.net/docs/advanced-guide/configuring-the-proxy-server/ 如果需要通过代理服务器发送请求,可以通过配置 `HttpClientHandler` 的 `Proxy` 属性来实现。下面是一个示例,展示了如何配置代理服务器,包括基本的身份认证。 ```cs showLineNumbers {5-9} services.AddHttpClient(string.Empty, client => {}) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler // 或使用 new SocketsHttpHandler {} { Proxy = new WebProxy("http://proxyserver:8080", false) { Credentials = new NetworkCredential("username", "password") // 如果需要身份认证 }, UseProxy = true // 默认情况下 UseProxy 是 true,确保它被启用 }); ``` --- # 6.54 配置 HTTP/3 支持 > 原文链接:https://http.furion.net/docs/advanced-guide/configuring-http3-support/ `HTTP/3` 是 `HTTP` 协议的最新版本,基于 `QUIC`(Quick UDP Internet Connections)协议之上,旨在提供更低的延迟、更高的吞吐量和改进的多路复用能力。您可以添加以下配置来启用 `HTTP/3` 支持: ```cs showLineNumbers {2,5,8} // 默认客户端配置 services.AddHttpClient(string.Empty, client => { // 设置默认请求版本为 HTTP/3 client.DefaultRequestVersion = HttpVersion.Version30; // 指定版本策略为精确匹配请求版本,确保使用 HTTP/3 client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; }); ``` 通过上述配置,我们能够确保在发送 `HTTP` 远程请求时严格使用指定的 `HTTP` 版本(即 `HTTP/3`),从而充分利用 `HTTP/3` 提供的性能改进和功能优势。 --- # 6.55 HttpClientOptions 扩展配置(JSON 序列化) > 原文链接:https://http.furion.net/docs/advanced-guide/httpclientoptions-extended-configuration-json-serialization/ 框架通过 `IHttpClientBuilder.ConfigureOptions()` 扩展方法,支持为 `HttpClient` 自定义选项(如 `JSON` 序列化行为),并提供可注入服务解析的重载。示例如下: ```cs showLineNumbers {3,10} // 配置默认客户端 services.AddHttpClient(string.Empty) .ConfigureOptions(options => // 或使用重载:.ConfigureOptions((options, serviceProvider) => { options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); // 配置特定客户端 services.AddHttpClient("github") .ConfigureOptions(options => // 或使用重载:.ConfigureOptions((options, serviceProvider) => { options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); ``` 通过此配置,可为不同的 `HTTP` 客户端实例定制 `JSON` 序列化行为,满足特定远程请求的需求。 `HttpClientOptions` 包含以下属性: - **属性**: - `JsonSerializerOptions`:`JSON` 序列化配置(`JsonSerializerOptions` 类型)。 - `JsonResponseWrapper`:指定 `JSON` 响应反序列化包装器(`JsonResponseWrapper?` 类型)。 - `UseJsonResponseWrapper`:是否全局启用 `JSON` 响应反序列化包装器(`bool` 类型)。 - `AccessTokenProvider`:`Access Token` 提供器配置(`IHttpAccessTokenProvider?` 类型)。 - `RequestEventHandler`:`HTTP` 事件处理程序提供器配置(`IHttpRequestEventHandler?` 类型)。 - `QuotaLimits`:接口调用配额限制配置(`Dictionary?` 类型)。 --- # 6.56 服务解析配置 > 原文链接:https://http.furion.net/docs/advanced-guide/service-resolution-configuration/ 在全局配置 `HttpClient` 时,有时需要先解析依赖服务,再进行客户端配置。例如,以下示例通过解析配置服务来设置请求基地址: ```cs showLineNumbers {1,4} services.AddHttpClient(string.Empty, (serviceProvider, client) => { // 解析配置服务 var configuration = serviceProvider.GetRequiredService(); client.BaseAddress = new Uri(configuration["your-base-address"]); }); ``` --- # 6.57 了解更多配置 > 原文链接:https://http.furion.net/docs/advanced-guide/learn-more-about-configuration/ 欲了解更多关于 `HttpMessageHandler` 和 `SocketsHttpHandler` 的配置,请查阅微软官方文档: - [`HttpClientHandler` 类](https://learn.microsoft.com/zh-cn/dotnet/api/system.net.http.httpclienthandler?view=net-9.0) - [`SocketsHttpHandler` 类](https://learn.microsoft.com/zh-cn/dotnet/api/system.net.http.socketshttphandler?view=net-9.0) 此外,框架底层通过 `IHttpClientFactory` 创建 `HttpClient` 实例,有关 `IHttpClientFactory` 的详细信息,请参阅微软官方文档: - [使用 `.NET` 的 `IHttpClientFactory`](https://learn.microsoft.com/zh-cn/dotnet/core/extensions/httpclient-factory) - [在 `ASP.NET Core` 中使用 `IHttpClientFactory` 发出 `HTTP` 请求](https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/http-requests?view=aspnetcore-8.0) --- # 6.58 DelegatingHandler 请求处理委托(拦截) > 原文链接:https://http.furion.net/docs/advanced-guide/delegatinghandler-request-processing-delegation-interception/ `DelegatingHandler` 是 C# 中 `HttpClient` 类的一个重要组成部分,它允许你以一种链式的方式处理 `HTTP` 请求和响应。`DelegatingHandler` 是一个抽象类,通常用于创建自定义的消息处理器,这些处理器可以被插入到 `HTTP` 消息处理管道中。通过继承 `DelegatingHandler` 并重写其 `Send` 和 `SendAsync` 方法,你可以实现自定义的行为,比如添加请求头、日志记录、身份认证、错误处理等。 **简单来说,`DelegatingHandler` 可以被视为一个拦截 `HTTP` 远程请求的“中间件”。** --- # 6.59 DelegatingHandler 应用场景 > 原文链接:https://http.furion.net/docs/advanced-guide/delegatinghandler-use-cases/ - **请求前处理**:在请求发送到服务器之前,可以对请求进行修改或增强,例如添加认证信息、设置特定的请求头等。 - **响应后处理**:在从服务器接收到响应之后但在最终用户看到结果之前,可以对响应进行处理,如解压、解密、缓存响应等。 - **错误处理**:统一处理所有 `HTTP` 请求可能发生的异常,提供一致的错误处理机制。 - **日志记录**:记录请求和响应的信息,便于调试和监控。 框架内置的 `ProfilerDelegatingHandler` 是一个请求分析工具,它通过实现 `DelegatingHandler` 接口来实现其功能。您可以[点击此处查看源码](https://gitee.com/dotnetchina/HttpAgent/blob/master/src/HttpAgent/src/Delegates/ProfilerDelegatingHandler.cs),以深入了解其实现细节。 --- # 6.60 自定义 DelegatingHandler > 原文链接:https://http.furion.net/docs/advanced-guide/custom-delegatinghandler/ 以下是一个简单的 `DelegatingHandler` 示例,展示了如何在请求发送前添加自定义请求头,并在响应接收后记录响应状态码: ```csharp showLineNumbers {1,3,9,12,18} public class CustomHandler : DelegatingHandler { protected override HttpResponseMessage Send(HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken) { // 参考异步 SendAsync 方法 return base.Send(httpRequestMessage, cancellationToken); } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // 请求预处理:添加自定义请求头 request.Headers.Add("Custom-Header", "Value"); // 发送请求并获取响应 var response = await base.SendAsync(request, cancellationToken); // 响应后处理:记录响应状态码 Console.WriteLine($"Response status code: {response.StatusCode}"); return response; } } ``` > **关于依赖注入** `DelegatingHandler` 的派生类支持通过构造函数进行依赖注入。 接下来,在 `Program.cs` 或 `Startup.cs` 文件中注册 `CustomHandler`: ```csharp showLineNumbers {2,6,10} // 注册 CustomHandler 为服务 services.TryAddSingleton(); // 为默认 HttpClient 客户端启用 CustomHandler services.AddHttpClient() .AddHttpMessageHandler(); // 若为特定命名的 HttpClient 客户端启用,可如下配置 // services.AddHttpClient("weixin") // .AddHttpMessageHandler(); ``` `.AddHttpMessageHandler` 方法支持多次调用,以添加多个处理器。例如: ```csharp showLineNumbers {2-4} services.AddHttpClient(string.Empty) .AddHttpMessageHandler() .AddHttpMessageHandler() .AddHttpMessageHandler(); ``` **注意**:处理器的执行顺序为注册顺序,即先注册的处理器先执行。 --- # 6.61 实现自动刷新授权 Token > 原文链接:https://http.furion.net/docs/advanced-guide/implementing-automatic-authorization-token-refresh/ > **更优方案** 推荐优先使用 **2.22 `Access Token` 自动管理** 章节介绍的方式,该方式内置了更完善的刷新机制和异常处理,无需手动编写 `DelegatingHandler`。以下内容保留作为传统实现参考,适合需要完全自定义刷新逻辑的场景。 在与第三方 `API` 接口对接时,通常需要在请求头中携带授权 `Token`。由于 `Token` 具有时效性,开发者需要定期刷新 `Token`。以下示例展示了如何通过 `DelegatingHandler` 实现自动刷新 `Token` 的逻辑。当响应返回 `401` 状态码时,系统会自动重新获取授权 `Token` 并更新请求头。 ```cs showLineNumbers {13,16,19-22,25,29,32,36} public class AuthorizationDelegatingHandler : DelegatingHandler { protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { // 参考 SendAsync 代码 return base.Send(request, cancellationToken); } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // 克隆原始请求(解决 StreamContent 只能读取一次的问题) var clonedRequest = await request.CloneAsync(cancellationToken); // 首次发送请求 var response = await base.SendAsync(clonedRequest, cancellationToken); // 检测 401 状态码 if (response.StatusCode != HttpStatusCode.Unauthorized) { return response; } // 刷新 Token var newToken = await GetNewTokenAsync(); // 实现获取新 Token 的逻辑 // 重新克隆请求并添加新 Token clonedRequest = await clonedRequest.CloneAsync(cancellationToken); clonedRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken); // 重试请求 response = await base.SendAsync(clonedRequest, cancellationToken); return response; } private async Task GetNewTokenAsync() { // 这里实现获取新 Token 的逻辑 // 例如:调用身份认证服务获取新的 Token return "newToken"; } } ``` 接下来,在 `Program.cs` 或 `Startup.cs` 文件中注册 `AuthorizationDelegatingHandler`: ```csharp showLineNumbers {2,6,10} // 注册 AuthorizationDelegatingHandler 为服务 services.TryAddSingleton(); // 为默认 HttpClient 客户端启用 AuthorizationDelegatingHandler services.AddHttpClient() .AddHttpMessageHandler(); // 若为特定命名的 HttpClient 客户端启用,可如下配置 // services.AddHttpClient("weixin") // .AddHttpMessageHandler(); ``` 通过上述代码,我们实现了一个自动刷新 `Token` 的机制。当 `Token` 过期时,系统会自动获取新的 `Token` 并重试请求,从而确保与第三方 `API` 的持续有效通信。 --- # 6.62 HttpClientHandler 发送请求底层处理器 > 原文链接:https://http.furion.net/docs/advanced-guide/httpclienthandler-underlying-handler-for-sending-requests/ `HttpClientHandler` 是 `.NET` 中用于配置和执行实际 `HTTP` 请求的核心消息处理器。它是 `HttpMessageHandler` 的具体实现类,通常作为 `HttpClient` 消息处理管道的**最末端**,负责与操作系统网络层交互,完成真正的 `HTTP` 请求发送与响应接收。 虽然 `DelegatingHandler` 和 `HttpClientHandler` 都继承自 `HttpMessageHandler`,但它们在职责上有明显区分: - **`DelegatingHandler`**:用于构建可插拔、链式的**中间件逻辑**(如日志、认证、重试等),本身不直接发起网络请求,而是将请求委托给下一个处理器。 - **`HttpClientHandler`**:是**最终执行网络 `I/O` 的处理器**,封装了平台相关的底层实现(如 `WinHTTP`、`libcurl` 或 `SocketsHttpHandler`),并提供丰富的配置选项来控制连接行为、安全策略、代理设置等。 --- # 6.63 HttpClientHandler 的核心功能 > 原文链接:https://http.furion.net/docs/advanced-guide/core-features-of-httpclienthandler/ `HttpClientHandler` 提供了对 `HTTP` 客户端行为的精细控制,常见配置包括: - **自动重定向**(`AllowAutoRedirect`) - **`Cookie` 容器管理**(`UseCookies` + `CookieContainer`) - **代理设置**(`Proxy` + `UseProxy`) - **证书验证与 TLS 配置**(`ServerCertificateCustomValidationCallback`) - **连接超时与请求超时**(`Timeout`、`ConnectTimeout`) - **是否使用默认凭据**(`UseDefaultCredentials`) - **压缩支持**(`AutomaticDecompression`) --- # 6.64 与 DelegatingHandler 的协作关系 > 原文链接:https://http.furion.net/docs/advanced-guide/collaboration-with-delegatinghandler/ 当你通过 `services.AddHttpClient()` 创建 `HttpClient` 时,`.NET` 默认会为你提供一个 `HttpClientHandler` 作为管道的“终点”。所有通过 `.AddHttpMessageHandler()` 添加的 `DelegatingHandler` 实例,都会被**包装在 `HttpClientHandler` 之外**,形成如下调用链: ``` CustomHandler3 → CustomHandler2 → CustomHandler1 → HttpClientHandler → 网络 ``` 也就是说: - 请求从最外层的 `DelegatingHandler` 开始,逐层向内传递; - 响应则从 `HttpClientHandler` 返回后,逐层向外回传; - **`HttpClientHandler` 是唯一真正发出网络请求的组件。** --- # 6.65 自定义 HttpClientHandler > 原文链接:https://http.furion.net/docs/advanced-guide/custom-httpclienthandler/ 虽然大多数拦截逻辑应通过 `DelegatingHandler` 实现,但 `HttpClientHandler` 本身也是可继承的,允许你在**最接近网络 `I/O` 的层级**插入自定义行为。这适用于需要深度控制底层请求/响应流程的场景,例如: - 模拟特定网络错误 - 在 `TLS` 握手前后注入监控 - 绕过默认的 `Cookie` 或代理处理逻辑 - 捕获或转换底层网络异常 以下是一个继承 `HttpClientHandler` 的示例,展示了如何在请求发送前添加自定义请求头,并在响应接收后记录状态码: ```cs showLineNumbers {1,4,11,14,20} public class CustomHttpHandler : HttpClientHandler { /// protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { // 参考异步 SendAsync 方法 return base.Send(request, cancellationToken); } /// protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // 请求预处理:添加自定义请求头 request.Headers.Add("Custom-Header", "Value"); // 发送请求并获取响应 var response = await base.SendAsync(request, cancellationToken); // 响应后处理:记录响应状态码 Console.WriteLine($"Response status code: {response.StatusCode}"); return response; } } ``` 接下来,在 `Program.cs` 或 `Startup.cs` 文件中配置并初始化 `CustomHttpHandler`: ```csharp showLineNumbers {3,7} // 为默认 HttpClient 客户端启用 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new CustomHttpHandler()); // 若为特定命名的 HttpClient 客户端启用,可如下配置 // services.AddHttpClient("weixin") // .ConfigurePrimaryHttpMessageHandler(() => new CustomHttpHandler()); ``` **注意**:`ConfigurePrimaryHttpMessageHandler` 设置的是整个管道的**底层处理器**,它必须是 `HttpMessageHandler` 的具体实现(如 `HttpClientHandler`、`SocketsHttpHandler`),不能是 `DelegatingHandler`。 --- # 6.66 最佳实践建议 > 原文链接:https://http.furion.net/docs/advanced-guide/best-practices/ - **不要轻易替换默认的 `HttpClientHandler`**,除非你需要特殊的安全或连接行为。 - 若需添加业务逻辑(如日志、重试、认证),优先使用 `DelegatingHandler`。 - 在需要精细控制 `TLS`、代理、`Cookie` 或超时策略等**底层网络行为**时,才考虑自定义 `HttpClientHandler`。 --- # 6.67 RateLimitedStream 带速率限制的流 > 原文链接:https://http.furion.net/docs/advanced-guide/ratelimitedstream-rate-limited-stream/ 在 `SaaS/PaaS` 应用平台中,用户常根据资源使用量(如带宽和流量)付费。特别是在用户下载或上传资源时,平台会进行速率限制。此时,框架提供的 `RateLimitedStream` 流非常有用。该流能够根据设定的速率限制调整读写速度,非常适合用于资源控制。 使用 `RateLimitedStream` 非常简单,只需通过构造函数传入需速率控制的 `Stream` 对象及每秒允许的最大字节数 `bytesPerSecond`。例如: ```cs showLineNumbers {4,6} var stream = await httpRemoteService.GetAsStreamAsync("https://furion.net/", HttpCompletionOption.ResponseHeadersRead); // 使用 RateLimitedStream 包装流并返回新的流,后续操作使用 rateLimitedStream 代替 stream 即可 var rateLimitedStream = new RateLimitedStream(stream, 1024 * 1024 * 1); // 限制最大读写速率为 1MB/s // 此时对 rateLimitedStream 的读写操作将被控制在 1MB/s 以内。✅ ``` > **小提示** - `RateLimitedStream` 特别适用于上传和下载资源的速率控制。 - 它基于[令牌桶算法](https://baike.baidu.com/item/令牌桶算法/6597000)实现,确保速率控制的有效性。 - 请注意,由于实现机制和系统负载等因素,实际速率可能会有约 `5%` 的误差,这是正常现象。 总的来说,`RateLimitedStream` 是一个强大的流包装器,能够帮助开发者在 `SaaS/PaaS` 平台上更好地管理资源使用,确保用户操作符合付费条件,同时提升系统稳定性和性能。 --- # 6.68 FileTypeMapper 文件 MIME 类型映射 > 原文链接:https://http.furion.net/docs/advanced-guide/filetypemapper-file-mime-type-mapping/ 在文件上传和下载过程中,通常需要依据文件扩展名来获取或设置其 `MIME` 类型。这一步骤往往要求开发者手动进行正确设置。为了减轻开发者负担,框架提供了 `FileTypeMapper` 类型,它能够根据文件扩展名自动返回对应的 `MIME` 类型。例如: ```cs showLineNumbers {1,3-4} var fileTypeMapper = new FileTypeMapper(); fileTypeMapper.TryGetContentType("image.jpg", out var mimeType); // mimeType 为 "image/jpeg" fileTypeMapper.TryGetContentType("image.png", out mimeType); // mimeType 为 "image/png" ``` 框架内置了一个包含 `389` 种文件扩展名及其对应 `MIME` 类型的字典,几乎覆盖了所有主流和非主流的文件类型。 此外,框架提供的所有添加文件内容的方法(如 `AddFile`、`AddFileAsStream`、`AddFileAsByteArray`、`AddFileFromRemote` 等)在用户未指定 `contentType` 参数时,会自动解析文件的 `MIME` 类型。若无法解析,则默认设置为 `application/octet-stream`。例如: ```cs showLineNumbers {5} var content = await httpRemoteService.PostAsStringAsync("https://localhost:7044/HttpRemote/AddForm?id=1", builder => builder.SetMultipartContent(multipart => { // 不传递 contentType,自动解析为 "image/jpeg" multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file") })); ``` 借助框架的自动解析功能,开发者在上传文件时无需再手动查找文件的 `MIME` 类型,从而大大简化了操作流程。 --- # 6.69 JwtTokenUtility 解析 JWT Token > 原文链接:https://http.furion.net/docs/advanced-guide/jwttokenutility-parsing-jwt-token/ `JwtTokenUtility` 是一个轻量级的 `JWT` 实用工具类,无需依赖外部 `JWT` 库即可解析 `JWT` 的 `Payload` 部分并提取常见的声明(`Claims`),适用于需要快速获取 `JWT` 中过期时间、签发者、主题等信息的场景。 **解析 `JWT` 的 `Payload`** 通过 `JwtTokenUtility.Parse` 方法可以解析完整 `JWT` 字符串或单独的 `Payload` 片段,返回一个 `JwtPayload` 实例: ```cs showLineNumbers {2,5} var jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; var jwtPayload = JwtTokenUtility.Parse(jwt); // 获取过期时间(UTC) var exp = jwtPayload.GetExpirationTimeUtc(); ``` `Parse` 方法内部会自动处理标准 `Base64Url` 编码并补齐 `=`,无需手动处理。 **读取标准声明** `JwtPayload` 提供了完整的标准 `JWT` 声明读取方法: ```cs showLineNumbers {1,3-9} var jwtPayload = JwtTokenUtility.Parse(jwt); string? issuer = jwtPayload.GetIssuer(); // iss string? subject = jwtPayload.GetSubject(); // sub string? audience = jwtPayload.GetAudience(); // aud long? expiration = jwtPayload.GetExpiration(); // exp (Unix 秒) long? issuedAt = jwtPayload.GetIssuedAt(); // iat (Unix 秒) long? notBefore = jwtPayload.GetNotBefore(); // nbf (Unix 秒) string? jwtId = jwtPayload.GetJwtId(); // jti // 更多便捷方法... ``` **检查 `JWT` 是否过期或有效** ```cs showLineNumbers {2,5} // 是否已过期 bool expired = jwtPayload.IsExpired(); // 是否当前有效(已生效且未过期) bool active = jwtPayload.IsActive(); ``` **读取自定义声明** 除了标准声明,`JwtPayload` 还支持读取任意自定义声明的值: ```cs showLineNumbers {4,7-8,11} var jwtPayload = JwtTokenUtility.Parse(jwt); // 读取字符串声明 string? name = jwtPayload.GetString("name"); // 读取整数声明 int? age = jwtPayload.GetInt32("age"); long? timestamp = jwtPayload.GetInt64("timestamp"); // 检查是否存在某声明 bool hasEmail = jwtPayload.Contains("email"); ``` **获取原始 `JSON` 字符串** `JwtPayload` 对象暴露了原始 `JSON` 字符串,方便进行自定义解析: ```cs showLineNumbers {2} var jwtPayload = JwtTokenUtility.Parse(jwt); string rawJson = jwtPayload.RawJson; ``` **与 `Access Token` 管理结合使用** 该工具常用于 `FurionAccessTokenProvider` 中,从服务端返回的刷新令牌中解析过期时间并更新 `HttpAccessToken` 的 `ExpiresAt`: ```cs showLineNumbers {7} // 在 Configure 方法的响应后回调中 httpRequestBuilder.SetOnPostReceiveResponse(httpResponseMessage => { var newRefreshToken = httpResponseMessage.Headers.GetValues("x-access-token").FirstOrDefault(); if (!string.IsNullOrWhiteSpace(newRefreshToken)) { httpAccessToken.ExpiresAt = JwtTokenUtility.Parse(newRefreshToken).GetExpirationTimeUtc()!.Value; } }); ``` 通过 `JwtTokenUtility`,您可以轻松地解析 `JWT`、提取声明并进行有效性判断,无需引入笨重的第三方 `JWT` 库,尤其适合在客户端侧或轻量级 `SDK` 中使用。 --- # 6.70 DigestCredentials 摘要身份认证 > 原文链接:https://http.furion.net/docs/advanced-guide/digestcredentials-digest-authentication/ `Digest` 摘要身份认证是一种在 `HTTP` 协议中用于验证用户身份的增强安全性的方法,相较于基本的 `HTTP Basic` 认证,它更为安全。在 `Digest` 认证过程中,用户的密码不会以明文形式在网络上传输,而是通过加密处理后再发送,从而提升了安全性。框架提供了 `DigestCredentials` 类型,便于生成摘要授权凭证: ```cs showLineNumbers {2,5,7} // 向指定服务器发起请求,并生成摘要认证所需的授权凭证字符串 var digestCredentials = DigestCredentials.GetDigestCredentials("https://furion.net/digest", "admin", "a123456789", HttpMethod.Get); // 将生成的授权凭证字符串设置为 Authorization 请求标头的值 httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Digest", digestCredentials); // 再次发起请求 ``` 这样,通过 `DigestCredentials` 类型,可以方便地实现 `Digest` 摘要身份认证,确保用户身份的安全验证。 --- # 6.71 HTTP 请求日志(关闭) > 原文链接:https://http.furion.net/docs/advanced-guide/logging/ 默认情况下,系统会在发送 `HTTP` 远程请求时打印相关日志信息,如下所示: ```bash showLineNumbers {2,4,6,8} info: System.Net.Http.HttpClient.Default.LogicalHandler[100] Start processing HTTP request GET https://furion.net/ info: System.Net.Http.HttpClient.Default.ClientHandler[100] Sending HTTP request GET https://furion.net/ info: System.Net.Http.HttpClient.Default.ClientHandler[101] Received HTTP response headers after 93.0553ms - 200 info: System.Net.Http.HttpClient.Default.LogicalHandler[101] End processing HTTP request after 122.2355ms - 200 ``` 若您希望关闭这些日志信息,可以在项目的 `appsettings.json` 以及 `appsettings.Development.json` 配置文件中进行如下设置: ```cs showLineNumbers {2,3,7} { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning", "Microsoft.EntityFrameworkCore": "Information", "System.Net.Http.HttpClient": "Warning" // 将日志级别设置为 Warning 以关闭 Info 级别日志 } } } ``` 通过将 `System.Net.Http.HttpClient` 的日志级别设置为 `Warning`,您可以有效地关闭 `Info` 级别的 `HTTP` 请求日志信息。请注意,在 `appsettings.Development.json` 中的设置将覆盖 `appsettings.json` 中的相应设置(如果两者都存在的话),这通常用于在开发环境中提供不同的日志记录策略。 除了上述配置方式,您还可以在程序中关闭 `HTTP` 远程请求的日志记录。具体操作如下: ```cs showLineNumbers {2-3,6-7,10-13,17-20} // 为默认客户端关闭日志 services.AddHttpClient(string.Empty) .RemoveAllLoggers(); // 为特定客户端关闭日志 //services.AddHttpClient("weixin") // .RemoveAllLoggers(); // 还可以一键为所有客户端配置关闭日志 services.ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.RemoveAllLoggers(); }); // 或使用 IHttpRemoteBuilder 扩展方法进行一键配置 services.AddHttpRemote() .ConfigureHttpClientDefaults(clientBuilder => { clientBuilder.RemoveAllLoggers(); }); ``` ### 自定义日志服务(继承 `HttpRemoteLoggerBase`) `HttpRemoteBuilder` 提供了 `UseLogger` 方法,允许您使用自定义日志服务替换内置的日志实现。自定义日志服务需继承 `HttpRemoteLoggerBase` 抽象类,并实现(重写)`Log` 方法,如下所示: ```cs showLineNumbers {2,5,8,11,13,18} // 自定义日志服务:继承 HttpRemoteLoggerBase 实现 internal sealed class CustomHttpRemoteLogger( ILogger logger, IOptionsMonitor httpRemoteOptions, bool isLoggingRegistered) : HttpRemoteLoggerBase { /// public override void Log(LogLevel logLevel, Exception? exception, string? message, params object?[] args) { // 检查是否注册了日志输出程序 if (isLoggingRegistered) { logger.Log(logLevel, exception, message, args); } else { // 调用备用日志输出委托 httpRemoteOptions.CurrentValue.FallbackLogger?.Invoke(LogMessageFormatter.Value(message, args)); } } } ``` > **构造函数参数顺序要求** 构造函数中的 `bool isLoggingRegistered` 参数**必须放在依赖注入服务参数之后**(即最后一个参数)。该参数用于告知当前是否配置(注册)了日志程序,框架在解析服务时会自动传入;若您的自定义日志服务还需要注入其他服务,同样需要声明在 `isLoggingRegistered` 参数**之前**。如下所示: ```cs showLineNumbers {2} // 其他依赖注入服务必须声明在 isLoggingRegistered 之前 public class CustomHttpRemoteLogger(IMyService myService, ILogger logger, IOptionsMonitor httpRemoteOptions, bool isLoggingRegistered) : HttpRemoteLoggerBase { // 省略实现 } ``` 完成定义后,通过 `UseLogger` 方法进行注册即可: ```cs showLineNumbers {1,4} services.AddHttpRemote(builder => { // 注册自定义日志服务 builder.UseLogger(); // 或使用 Type 注册 // builder.UseLogger(typeof(CustomHttpRemoteLogger)); }); ``` **不使用 `UseLogger` 手动注册:** 若您希望完全自行控制,例如需要向自定义构造函数传递更多参数(而非仅 `isLoggingRegistered`),可以不使用 `UseLogger` 方法,在外部自行注册 `IHttpRemoteLogger` 服务,如下所示: ```cs showLineNumbers {2,5,8-10} // 检查是否配置(注册)了日志程序 var isLoggingRegistered = services.Any(u => u.ServiceType == typeof(ILoggerProvider)); // 移除所有已存在的 IHttpRemoteLogger 注册 services.RemoveAll(); // 手动注册自定义日志服务,可传递更多参数 services.AddSingleton(provider => (IHttpRemoteLogger)ActivatorUtilities.CreateInstance(provider, typeof(CustomHttpRemoteLogger), isLoggingRegistered, 额外参数1, 额外参数2 /*... 这里可以传递更多参数 */)); ``` 这样您便可以完全掌控构造函数的参数传递,按需注入任何所需服务或参数。 --- # 7.1 应用案例 > 原文链接:https://http.furion.net/docs/cases/use-cases/ 这里汇总了 `HTTP` 远程请求在应用开发中的常见案例。 --- # 7.2 使用流变对象 Clay 构建和接收请求数据 > 原文链接:https://http.furion.net/docs/cases/using-the-dynamic-object-clay-to-build-and-receive-request-data/ 流变对象(`Clay`)在 `HTTP` 远程请求中的应用场景非常广泛,尤其是在与第三方 `API` 接口对接时。通常,这些接口需要传递或接收 `JSON` 格式的数据,而流变对象可以简化数据的构建和解析过程。以下是如何在 `HTTP` 远程请求模块中使用流变对象的配置步骤: ### 1. 配置流变对象 `JSON` 序列化转换器 在使用流变对象进行 `HTTP` 远程请求时,**首先需要配置 `AddClayConverters()`,以便将流变对象序列化为 `JSON` 格式字符串**。配置示例如下: ```cs showLineNumbers {2-3,5,9-10,12} // 全局配置(适用于所有客户端) services.AddHttpRemote(options => {}) .ConfigureOptions(options => { options.JsonSerializerOptions.AddClayConverters(); }); // 客户端级配置(优先级更高) services.AddHttpClient("客户端名称") .ConfigureOptions(options => { options.JsonSerializerOptions.AddClayConverters(); }); ``` ### 2. 发送和接收 `JSON` 数据 配置完成后,可以使用流变对象构建请求内容并发送 `HTTP` 请求,同时将响应内容转换为流变对象进行处理。以下是一个示例: ```cs showLineNumbers {2-4,8,11} // 构建请求内容 dynamic payload = new Clay(); payload.id = 1; payload.name = "furion"; // 发送 HTTP 远程请求 var content = await httpRemoteService.PostAsStringAsync("https://localhost:7044/HttpRemote/AddModel", builder => builder.SetJsonContent(payload)); // 将响应内容转化为流变对象 dynamic clay = Clay.Parse(content); ``` --- ### 自定义 `Clay` 内容转换器简化手动转换 ✅ 为了简化代码,避免手动将 `JSON` 格式字符串转换为流变对象(如 `dynamic clay = Clay.Parse(content);`),可以**自定义 `ClayContentConverter` 内容转换器**。通过这种方式,您可以直接在 `HTTP` 请求中使用 `Clay` 类型作为泛型接收参数。以下是自定义转换器的实现: **[【一键下载 `ClayContentConverter.cs` 文件】✅](/img/ClayContentConverter.cs)** ```cs showLineNumbers {1,5,10-11,16,20,25-26} public class ClayContentConverter : HttpContentConverterBase { /// public override Clay? Read(HttpContentConverterContext context, CancellationToken cancellationToken = default) => AsyncUtility.RunSync(() => ReadAsync(context, cancellationToken)); /// public override async Task ReadAsync(HttpContentConverterContext context, CancellationToken cancellationToken = default) { var str = await context.ResponseMessage.Content.ReadAsStringAsync(cancellationToken); return Clay.Parse(str, ClayOptions.Flexible); // 或使用 Clay.Parse(str, ClayOptions.Flexible); // 忽略属性大小写 } } // 支持 dynamic 类型转流变对象(可选,但推荐!!!) public class DynamicContentConverter : HttpContentConverterBase { /// public override dynamic? Read(HttpContentConverterContext context, CancellationToken cancellationToken = default) => AsyncUtility.RunSync(() => ReadAsync(context, cancellationToken)); /// public override async Task ReadAsync(HttpContentConverterContext context, CancellationToken cancellationToken = default) { var str = await context.ResponseMessage.Content.ReadAsStringAsync(cancellationToken); return Clay.Parse(str, ClayOptions.Flexible); // 或使用 Clay.Parse(str, ClayOptions.Flexible); // 忽略属性大小写 } } ``` 随后,在 `Startup.cs` 或 `Program.cs` 文件中,配置并注册 `HttpRemote` 服务,以启用自定义内容转换器功能: ```cs showLineNumbers {1,3} services.AddHttpRemote(options => { options.AddHttpContentConverters(() => [ new ClayContentConverter(), new DynamicContentConverter()]); // new DynamicContentConverter()(可选,但推荐!!) }); ``` 配置完成后,可以直接使用流变对象类型 `Clay` 作为泛型接收参数: ```cs showLineNumbers {2,6} // 发送 HTTP 远程请求,将响应内容转化为流变对象 dynamic clay = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddModel", builder => builder.SetJsonContent(payload)); // 如果配置了 DynamicContentConverter,那么也可以使用 dynamic 接收 dynamic clay = await httpRemoteService.PostAsAsync("https://localhost:7044/HttpRemote/AddModel", builder => builder.SetJsonContent(payload)); ``` > **`ClayJsonConverter` 与 `ClayContentConverter` 说明** - **`ClayJsonConverter/AddClayConverters()`**:用于将流变对象序列化为 `JSON` 格式字符串,通常作为**输入**参数使用。 - **`ClayContentConverter/DynamicContentConverter`**:用于将 `JSON` 格式字符串反序列化为流变对象,通常作为**输出**参数使用。 通过结合流变对象与 `HTTP` 远程请求,开发者可以更高效地处理动态 `JSON` 数据,简化第三方 `API` 的对接流程。流变对象的动态特性使得数据的构建和解析更加灵活,同时自定义内容转换器进一步提升了开发效率。 --- # 7.3 在 Blazor WebAssembly 应用中使用 > 原文链接:https://http.furion.net/docs/cases/blazor/ > **`Blazor WebAssembly` 的网络栈限制** `Blazor WebAssembly` 应用运行在浏览器的沙箱环境中,无法使用操作系统底层的 `TCP` 套接字。整个网络栈被替换为基于浏览器 `fetch` `API` 的实现,因此依赖于 `System.Net.Sockets` 的 `SocketsHttpHandler` 和会调用平台原生网络 `API` 的 `HttpClientHandler` 均不可用。任何对套接字或平台专属处理器属性的设置(例如 `AutomaticDecompression`)都会直接抛出 `PlatformNotSupportedException`。开发时应避免配置这些 `Handler`,完全依赖浏览器提供的网络能力。 `HTTP` 远程请求支持在 `Blazor WebAssembly` 客户端应用中使用。以下是配置和使用的详细步骤: **1. 注册 `HttpRemote` 服务** 在 `Startup.cs` 或 `Program.cs` 文件中,注册 `HttpRemote` 服务并配置 `FallbackBaseAddress`: ```cs showLineNumbers {1,2,4-5} builder.Services.AddHttpRemote() .ConfigureOptions((options, serviceProvider) => { var navigation = serviceProvider.GetRequiredService(); options.FallbackBaseAddress = new Uri(navigation.BaseUri); }); ``` **2. 在 `_Imports.razor` 中引入命名空间** 在 `_Imports.razor` 文件中,添加 `HttpRemote` 服务的命名空间: ```cs showLineNumbers @using HttpAgent; // 如果使用 Furion 框架,则使用 @using Furion.HttpRemote; ``` **3. 在 `*.razor` 文件中使用** 在 `*.razor` 文件中注入 `IHttpRemoteService` 并发送 `HTTP` 远程请求: ```cs showLineNumbers {2,11} @page "/weather" @inject IHttpRemoteService Http // 其他代码... @code { private WeatherForecast[] forecasts; protected override async Task OnInitializedAsync() { forecasts = await Http.GetAsAsync("sample-data/weather.json"); } } ``` 通过以上步骤,您可以在 `Blazor WebAssembly` 应用中轻松使用 `HTTP` 远程请求功能,动态获取数据并渲染到页面中。 --- # 7.4 在 File-Based Apps 应用中使用 > 原文链接:https://http.furion.net/docs/cases/file-based-apps/ 从 `.NET 10` `SDK` 开始,`.NET` 提供了 `File-Based Apps`(基于文件的应用)能力:无需创建项目文件(`.csproj`)和解决方案,只需一个 `.cs` 源文件即可构建、运行和发布应用程序,依赖与配置通过文件顶部的 `#:` 指令声明(支持 `#:include`、`#:package`、`#:project`、`#:property` 和 `#:sdk` 指令)。 > 关于 `File-Based Apps` 的完整指令说明与更多用法,请参阅微软官方文档「[基于文件的应用(File-based apps)](https://learn.microsoft.com/zh-cn/dotnet/core/sdk/file-based-apps)」。 由于 `File-Based Apps` 没有项目文件,无法使用 `dotnet add package` 命令安装包,需要通过 `#:package` 指令引用 `HttpAgent` 包。`HTTP` 远程请求自 `1.53` 版本开始完整支持 `File-Based Apps` 应用类型,请使用该版本及以上版本(`@*` 表示引用最新版本,当前最新版本为 `2.1.5`): **1. 编写 `app.cs` 文件** ```cs showLineNumbers {1,5} #:package HttpAgent@* using HttpAgent; var result = await HttpRemoteClient.Service.GetAsStringAsync("https://furion.net/"); Console.WriteLine(result); ``` > 说明:`#:package` 指令后省略版本号目前仅在使用 `Directory.Packages.props` 中央包管理时有效,因此这里使用 `@*` 引用最新版本;也可以显式指定版本,如 `#:package HttpAgent@2.1.5`。 **2. 运行应用程序** 在终端中执行以下命令即可直接运行: ```bash showLineNumbers {1} dotnet run app.cs ``` 也可以使用 `--file` 选项:`dotnet run --file app.cs`,或使用简写语法:`dotnet app.cs`。如需向应用程序传递参数,将参数放在 `--` 之后:`dotnet run app.cs -- arg1 arg2`。 **3. 启用请求分析工具(可选)** 如果需要在请求时打印完整的请求/响应报文,可以在文件中调用 `HttpRemoteClient.Configure` 方法配置默认的 `HttpClient`: ```cs showLineNumbers {1,5-8,10} #:package HttpAgent@* using HttpAgent; HttpRemoteClient.Configure(services => { services.AddHttpClient(string.Empty).AddProfilerDelegatingHandler(); }); var result = await HttpRemoteClient.Service.GetAsStringAsync("https://furion.net/"); Console.WriteLine(result); ``` > **提示** `File-Based Apps` 默认启用本机 `AOT` 发布,还支持通过 `#:property` 指令设置 `MSBuild` 属性、通过 `#:sdk` 指令切换 `SDK`(如 `#:sdk Microsoft.NET.Sdk.Web`)、通过 `#:include` 指令包含其他源文件等。更多用法请参阅微软官方文档「[基于文件的应用](https://learn.microsoft.com/zh-cn/dotnet/core/sdk/file-based-apps)」(`File-based apps`)。 --- # 7.5 在 MAUI 应用中使用 > 原文链接:https://http.furion.net/docs/cases/maui/ `.NET MAUI` 内置了基于 `Microsoft.Extensions.DependencyInjection` 的依赖注入支持:在应用入口 `MauiProgram.CreateMauiApp()` 方法中向 `builder.Services` 注册服务,随后即可在页面(`Page`)或视图模型(`ViewModel`)的构造函数中注入使用。更多依赖注入细节(注册方式、服务生存期等)请参阅微软官方文档「[依赖注入(.NET MAUI)](https://learn.microsoft.com/zh-cn/dotnet/maui/fundamentals/dependency-injection)」。 **1. 在 `MauiProgram.cs` 中注册服务** 在 `MauiProgram.cs` 文件的 `CreateMauiApp()` 方法中调用 `AddHttpRemote()` 注册 `HTTP` 远程请求服务: ```cs showLineNumbers {1,16} using HttpAgent; public static class MauiProgram { public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder .UseMauiApp() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); }); builder.Services.AddHttpRemote(); // 注册 HTTP 远程请求服务 return builder.Build(); } } ``` **2. 在页面中注入使用** 在页面(或视图模型)的构造函数中注入 `IHttpRemoteService` 即可发送 `HTTP` 远程请求: ```cs showLineNumbers {1,7,15} using HttpAgent; public partial class MainPage : ContentPage { private readonly IHttpRemoteService _httpRemoteService; public MainPage(IHttpRemoteService httpRemoteService) { InitializeComponent(); _httpRemoteService = httpRemoteService; } private async Task LoadContentAsync() { var result = await _httpRemoteService.GetAsStringAsync("https://furion.net/"); // 渲染 result ... } } ``` > 说明:在 `Shell` 应用中,如果页面构造函数需要依赖注入,请同时把页面注册到容器(如 `builder.Services.AddTransient();`),`Shell` 导航时会自动从容器解析页面实例;也可以把 `IHttpRemoteService` 注入到已注册的视图模型中,再由页面注入该视图模型。 --- # 8.1 常见问题 > 原文链接:https://http.furion.net/docs/faq/faq/ 这里汇总了一些发送 `HTTP` 远程请求时可能遇到的常见问题。 --- # 8.2 忽略 SSL 证书验证(https 错误) > 原文链接:https://http.furion.net/docs/faq/ignoring-ssl-certificate-validation-https-errors/ 若发送 `HTTP` 远程请求过程中出现 `The SSL connection could not be established, see inner exception.` 的证书错误问题,您可以通过添加以下配置来忽略 `SSL` 证书验证: ```cs showLineNumbers {3,6-7,12,14,17-18} // 默认客户端配置 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { // 忽略 SSL 证书验证 ServerCertificateCustomValidationCallback = HttpRemoteUtility.IgnoreSslErrors, SslProtocols = HttpRemoteUtility.AllSslProtocols }); // 若使用 SocketsHttpHandler,可以通过以下配置来忽略 SSL 证书验证 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler() { SslOptions = new SslClientAuthenticationOptions { // 忽略 SSL 证书验证 RemoteCertificateValidationCallback = HttpRemoteUtility.IgnoreSocketSslErrors, EnabledSslProtocols = HttpRemoteUtility.AllSslProtocols }, }); ``` 除了可以在全局配置中忽略 `SSL` 证书验证外,您还可以通过 `SetHttpClientProvider` 方法为单次请求单独设置忽略 `SSL` 证书验证。示例代码如下: ```cs showLineNumbers {2-7} HttpRequestBuilder.Get("https://furion.net/") .SetHttpClientProvider(() => (new HttpClient(new HttpClientHandler { // 忽略 SSL 证书验证 ServerCertificateCustomValidationCallback = HttpRemoteUtility.IgnoreSslErrors, SslProtocols = HttpRemoteUtility.AllSslProtocols }), client => client.Dispose())); ``` --- # 8.3 发送请求出现长时间挂起或阻塞 > 原文链接:https://http.furion.net/docs/faq/a-sent-request-hangs-or-blocks-for-a-long-time/ 若发送 `HTTP` 远程请求时,出现长时间挂起或阻塞,**可能是您在构造请求时使用了类似以下方式获取本机 `IP` 或 `MAC` 地址**: ```cs showLineNumbers var addressList = Dns.GetHostEntry(Dns.GetHostName()).AddressList; ``` `Dns.GetHostName()` 获取本机主机名,`Dns.GetHostEntry(主机名)` 则会通过 `DNS`、`NetBIOS` 等机制解析该名称。如果主机名未在 `DNS` 中注册或 `DNS` 服务器不可达,解析过程会一直等待直到系统超时(`Windows` 默认约 `10~15` 秒),导致每个请求都被阻塞。 此时,可使用框架内置的 `HttpRemoteUtility` 工具方法替代,它们直接读取本地网络接口信息,**完全无 `DNS/NetBIOS` 查询,执行时间 `< 1` 毫秒**: ```cs showLineNumbers {2,5} // 获取本机 IPv4 地址 var ip = HttpRemoteUtility.GetLocalIPv4(); // 获取本机 MAC 地址 var mac = HttpRemoteUtility.GetLocalMacAddress(); ``` 采用上述方法即可消除因本机主机名解析导致的长时间阻塞,恢复正常的 `HTTP` 请求响应速度。 --- # 8.4 强制 IPv4 或 IPv6 请求 > 原文链接:https://http.furion.net/docs/faq/forcing-ipv4-or-ipv6-requests/ 通过配置 `SocketsHttpHandler` 的 `ConnectCallback`,可强制 `HttpClient` 使用指定 `IP` 版本或本地出口地址发起请求,适用于网络策略控制或多网卡环境,,以优化性能或满足网络要求。示例如下: ```cs showLineNumbers {1,8,15,22,30} // 强制 IPv4 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { ConnectCallback = HttpRemoteUtility.IPv4ConnectCallback }); // 强制 IPv6 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { ConnectCallback = HttpRemoteUtility.IPv6ConnectCallback }); // 默认(自动选择) services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { ConnectCallback = HttpRemoteUtility.UnspecifiedConnectCallback }); // 多网卡场景:指定本地出口 IPv4 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { ConnectCallback = (context, token) => HttpRemoteUtility.ConnectWithLocalIPv4(IPAddress.Parse("192.168.0.103"), context, token) }); // 多网卡场景:指定本地出口 IPv6 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { ConnectCallback = (context, token) => HttpRemoteUtility.ConnectWithLocalIPv6(IPAddress.Parse("::1"), context, token) }); ``` --- # 8.5 重定向响应处理 > 原文链接:https://http.furion.net/docs/faq/handling-redirect-responses/ 在发送 `HTTP` 远程请求时,若目标服务器返回重定向响应(如 `301 Moved Permanently`、`302 Found` 等),**框架默认会自动跟随重定向**。为精确处理这些情况,可参考以下方案: - **通过 `HttpRemoteOptions` 全局启用或禁用自动重定向** 在发送 `HTTP` 远程请求时,若目标服务器返回重定向响应(如 `301 Moved Permanently`、`302 Found` 等),**框架默认会自动跟随重定向**。要启用或禁用这一行为,可以通过以下配置进行设置: ```cs showLineNumbers {2,5,8} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { // 设置指示请求是否应遵循重定向响应,默认 true options.AllowAutoRedirect = false; // 设置请求所遵循的最大重定向数,默认 50 次 options.MaximumAutomaticRedirections = 50; }); ``` - **通过 `HttpClient` 全局启用或禁用自动重定向** 若希望或不希望 `HttpClient` 自动处理重定向,可通过配置 `HttpClientHandler` 或 `SocketsHttpHandler` 来启用或禁用此功能。 ```cs showLineNumbers {3,5-6} // 配置默认客户端 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { AllowAutoRedirect = true, // 允许自动重定向,默认为 true (允许) MaxAutomaticRedirections = 20 // 设置最大重定向次数 }); ``` > **禁用自动重定向功能的配置** 若您希望完全禁用自动重定向功能,请确保同时配置以下代码段: ```cs showLineNumbers {2,4,8,9,11} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => { options.AllowAutoRedirect = false; }); // 配置默认客户端 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { AllowAutoRedirect = false }); ``` 请注意,上述全局配置不仅会影响 `HTTP` 远程请求服务的重定向行为,同样也会作用于 `HttpContext` 中的重定向转发。 - **通过手动处理重定向** 也可以检查响应状态码。若状态码在 `300-399` 范围内,表明为重定向状态码,此时可手动解析 `Location` 头,并使用该地址重新发送请求。 1. 禁用 `HttpClient` 的自动重定向功能 ```cs showLineNumbers {3,5} // 配置默认客户端 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { AllowAutoRedirect = false }); ``` 2. 手动管理重定向流程 ```cs showLineNumbers {4,7-16} var responseMessage = await httpRemoteService.GetAsync("https://furion.net/redirect"); // 检查响应消息是否包含重定向状态码 var statusCode = responseMessage.StatusCode; // 通常应使用 while 循环来进行判断,因为可能涉及多次重定向 if (statusCode is HttpStatusCode.Ambiguous or HttpStatusCode.Moved or HttpStatusCode.Redirect or HttpStatusCode.RedirectMethod or HttpStatusCode.RedirectKeepVerb || (int)statusCode == 308) { // 获取重定向地址 var redirectUrl = responseMessage.Headers.Location; if (redirectUrl != null) { // 用新的 URL 再次发送请求 response = await httpRemoteService.GetAsync(redirectUrl); } } ``` 选择哪种方法取决于您的具体需求。如果你需要更细粒度地控制每个重定向过程,比如记录日志或执行特定逻辑,那么手动处理可能是更好的选择。如果只是简单地想要让客户端自动处理所有重定向,则开启自动重定向更加方便。 --- # 8.6 启用标准请求标头 > 原文链接:https://http.furion.net/docs/faq/enabling-standard-request-headers/ 为了提升应用通过 `HTTP` 客户端发送网络请求的兼容性并避免被 WAF(Web 应用防火墙)拦截,框架提供了一键式配置方法,方便快速统一设置标准的请求头部: ```cs showLineNumbers {5} // 为默认客户端配置 services.AddHttpClient(string.Empty, client => { // 启用标准请求标头 client.UseStandardRequestHeaders(); }); ``` 除了使用全局 `HttpClient` 配置,您还可以为单次请求配置特定设置,如下所示: ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net/") .UseStandardRequestHeaders(); // 启用标准请求标头 ``` 启用标准请求标头后,请求将自动添加以下头部: - **`Accept`**:`application/json`、`text/plain;q=0.9`、`*/*;q=0.8`(明确的媒体类型优先级,避免被 `WAF` 拦截) - **`Connection`**:启用持久连接(`Keep-Alive`),减少 `TCP` 连接建立和关闭的开销 --- # 8.7 设置默认 User-Agent > 原文链接:https://http.furion.net/docs/faq/setting-the-default-user-agent/ 在发送 `HTTP` 请求时,若用户未指定 `User-Agent` 请求头,框架将默认使用 `Edge` 浏览器(版本 `142`)的 `User-Agent`。示例如下: ``` Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0 ``` 如需自定义默认 `User-Agent`,可通过以下方式实现: **全局配置**: ```cs showLineNumbers {2,4} // 配置默认客户端的 User-Agent services.AddHttpClient(string.Empty, client => { client.DefaultRequestHeaders.Add("User-Agent", UserAgents.Chrome.PC); // 推荐使用 UserAgents 静态类设置 // client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0"); // 或直接设置 User-Agent 字符串 }); ``` **单次请求设置**: ```cs showLineNumbers {2} HttpRequestBuilder.Post("https://furion.net/") .SetUserAgent(UserAgents.Chrome.PC); // 推荐使用 UserAgents 静态类设置 // .SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0"); // 或直接设置 User-Agent 字符串 // .WithHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0", replace: true); // 或使用 WithHeader 方法 ``` --- # 8.8 发送相对地址(内部)请求 > 原文链接:https://http.furion.net/docs/faq/sending-relative-address-internal-requests/ 在 `ASP.NET` 或 `Blazor` 等 `Web` 应用中,发送内部 `HTTP` 请求时,通常使用相对地址。然而,由于项目启动前无法获取主机地址和端口,无法直接通过 `services.AddHttpClient()` 配置 `BaseAddress`。此时,可以通过配置 `HttpRemoteOptions` 的 `FallbackBaseAddress` 来实现。 - **`ASP.NET` 应用** ```cs showLineNumbers {2,4-5} services.AddHttpRemote() .ConfigureOptions((options, serviceProvider) => { var serverAddressesFeature = serviceProvider.GetRequiredService().Features.Get()!; options.FallbackBaseAddress = new Uri(serverAddressesFeature.Addresses.FirstOrDefault()); }); ``` > **在 `BackgroundService` 派生类中使用相对地址** 由于 `BackgroundService` 是后台服务,默认不在 `Web` 线程中运行。如果在其派生类中发送相对地址(内部)的 `HTTP` 请求,可能会出现 `serverAddressesFeature.Addresses` 为空的情况。这是因为 `BackgroundService` 派生类的启动时机在 `Web` 主机启动之前,此时 `Web` 主机地址和端口还未分配。 如果出现这种情况,可以通过 `IConfiguration` 配置读取 `Urls` 节点来设置 `FallbackBaseAddress` 地址。 - **`Blazor WebAssembly` 应用** ```cs showLineNumbers {2,4-5} services.AddHttpRemote() .ConfigureOptions((options, serviceProvider) => { var navigation = serviceProvider.GetRequiredService(); options.FallbackBaseAddress = new Uri(navigation.BaseUri); }); ``` 通过上述配置,可以发送**相对地址(内部)请求**,示例如下: ```cs showLineNumbers var content = await httpRemoteService.GetAsStringAsync("/user/1"); // 相对地址(内部) ``` 在发送相对地址请求时,系统会自动获取 `Web` 主机启动时的主机地址和端口进行拼接。例如,最终发送的请求地址可能为:`https://localhost:5001/user/1`。这种方式简化了内部请求的配置,确保在项目启动时动态获取主机地址和端口,从而避免硬编码带来的维护问题。 --- # 8.9 获取响应 Cookie > 原文链接:https://http.furion.net/docs/faq/getting-the-response-cookie/ 在 `HTTP` 请求中,如果服务器设置了 `Cookie`,响应头中会包含一个或多个 `Set-Cookie` 键值对。客户端接收到响应后,可以通过读取这些 `Set-Cookie` 键值对来获取 `Cookie` 信息。框架提供了以下两种便捷的方式来获取 `Cookie`: **1. 使用 `HttpRemoteResult` 返回值类型** `HttpRemoteResult` 是一个泛型类型,专门用于封装 `HTTP` 远程请求模块中的响应内容。该类型不仅包含了常用的 `HTTP` 响应信息,还提供了请求耗时等功能。 ```cs showLineNumbers {3-4} // 使用请求谓词方式(result 类型为 HttpRemoteResult) var result = await httpRemoteService.GetAsync("https://furion.net/"); var setCookies = result.SetCookies; // 获取响应中的 Cookie 集合(IList 类型) var rawSetCookies = result.RawSetCookies; // 获取原始响应头中的 Set-Cookie 集合(List 类型) // 使用构建器方式同样适用(result 类型为 HttpRemoteResult) var result = await httpRemoteService.SendAsync(HttpRequestBuilder.Get("https://furion.net/")); ``` **2. 使用 `HttpResponseMessage` 的 `TryGetSetCookies` 扩展方法** 框架还为 `HttpResponseMessage` 和 `HttpResponseHeaders` 类型提供了 `TryGetSetCookies` 扩展方法,可以方便地读取并解析 `Set-Cookie` 响应头信息。 ```cs showLineNumbers {2,5} var httpResponseMessage = await httpRemoteService.GetAsync("https://furion.net/"); httpResponseMessage.TryGetSetCookies(out var setCookies, out var rawSetCookies); // 或者通过 Headers 属性获取 // httpResponseMessage.Headers.TryGetSetCookies(out var setCookies, out var rawSetCookies); ``` 通过以上两种方式,开发者可以轻松获取并处理 `HTTP` 响应中的 `Cookie` 信息。 --- # 8.10 配置 Windows 身份认证 > 原文链接:https://http.furion.net/docs/faq/configuring-windows-authentication/ `Windows` 身份认证是微软提供的一种安全机制,用于验证用户或实体的身份,确保他们对系统、网络资源和应用程序的访问符合安全策略。该机制通常用于 `Windows` 操作系统,允许用户无需手动输入用户名和密码即可登录系统或依赖该机制的应用程序。 在某些传统的基于 `Windows` 服务器部署的 `Web` 应用系统中,发送 `HTTP` 远程请求可能需要启用 `Windows` 身份认证。以下是两种常见的配置方式: ### 1. 使用当前 `Windows` 系统登录的用户(推荐) ```cs showLineNumbers {3,5,9} // 配置默认客户端 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler // 或使用 SocketsHttpHandler { UseDefaultCredentials = true }); // 配置特定客户端 services.AddHttpClient("furion") .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler // 或使用 SocketsHttpHandler { UseDefaultCredentials = true }); ``` ### 2. 手动输入 `Windows` 系统用户名和密码 ```cs showLineNumbers {3,5-6,10} // 配置默认客户端 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler // 或使用 SocketsHttpHandler { Credentials = new NetworkCredential("windows登录用户名", "windows登录密码"), PreAuthenticate = true }); // 配置特定客户端 services.AddHttpClient("furion") .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler // 或使用 SocketsHttpHandler { Credentials = new NetworkCredential("windows登录用户名", "windows登录密码"), PreAuthenticate = true }); ``` 通过以上配置,您可以根据需求选择使用当前系统用户或手动输入凭据来启用 `Windows` 身份认证。 --- # 8.11 配置 Kerberos 和 Active Directory 身份认证 > 原文链接:https://http.furion.net/docs/faq/configuring-kerberos-and-active-directory-authentication/ `Kerberos` 是一种网络认证协议,它利用对称密钥加密技术来验证用户和服务的身份。通过使用票证(`tickets`)机制,`Kerberos` 能够确保在网络上的通信是安全的,并且可以有效地防止窃听、重放攻击等安全威胁。自 `Windows 2000` 开始,`Kerberos` 成为了域环境下的默认认证协议。 `Active Directory (AD)` 是微软提供的一系列目录管理服务,用于 `Windows` 域网络的管理和安全设置。它允许 `IT` 管理员管理网络中的用户、设备和其他资源,并提供了诸如身份认证、授权等功能。`AD` 支持多种认证协议,包括 `NTLM`(`NT LAN Manager`),但更推荐使用 `Kerberos` 因为其更高的安全性。 --- **如果您的应用程序运行在域环境中,并且目标服务支持 `Windows` 身份认证(如 `Negotiate` 或 `NTLM`),可以直接使用 `Windows` 身份认证的配置方法**。以下是具体配置示例: ### 1. 使用当前域用户的凭据(推荐) 此方式适用于应用程序与目标服务在同一个域环境中运行的情况。系统会自动使用当前域用户的凭据进行身份认证。 ```cs showLineNumbers {3,5,9} // 配置默认客户端 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler // 或使用 SocketsHttpHandler { UseDefaultCredentials = true }); // 配置特定客户端 services.AddHttpClient("furion") .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler // 或使用 SocketsHttpHandler { UseDefaultCredentials = true }); ``` ### 2. 跨域环境的 `Kerberos` 身份认证 如果应用程序与目标服务不在同一个域环境中,可以通过安装 [Microsoft.Identity.Client](https://www.nuget.org/packages?q=Microsoft.Identity.Client) 库来实现跨域身份认证。该库提供了对 `Kerberos` 和其他身份认证协议的支持。 #### 安装 NuGet 包 ```bash showLineNumbers dotnet add package Microsoft.Identity.Client ``` #### 使用 `Microsoft.Identity.Client` 实现身份认证 以下是一个简单的示例,展示如何使用 `Microsoft.Identity.Client` 获取访问令牌并进行身份认证: ```cs showLineNumbers {3,12,16,18} using Microsoft.Identity.Client; // 配置身份认证参数 var clientId = "你的客户端ID"; var tenantId = "你的租户ID"; var authority = $"https://login.microsoftonline.com/{tenantId}"; var app = PublicClientApplicationBuilder.Create(clientId) .WithAuthority(authority) .Build(); // 获取访问令牌 var scopes = new[] { "api://目标服务的作用域" }; var result = await app.AcquireTokenInteractive(scopes).ExecuteAsync(); // 使用访问令牌调用目标服务 httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/") .AddBearerAuthentication(result.AccessToken)); ``` ### 3. 参考文档 - [Microsoft.Identity.Client NuGet 包](https://www.nuget.org/packages/Microsoft.Identity.Client) - [适用于 .NET 的 Microsoft 身份认证库 (MSAL) 文档](https://learn.microsoft.com/zh-cn/entra/msal/dotnet/) - [How to authenticate with Kerberos using .NET](https://dev.to/lucaspsilveira/how-to-authenticate-with-kerberos-using-net-2gh2) 通过以上配置,您可以根据应用程序的运行环境选择合适的身份认证方式,确保安全访问目标服务。 --- # 8.12 响应内容解压(支持 gzip、deflate、brotli 和 zstd)和内容乱码处理 > 原文链接:https://http.furion.net/docs/faq/decompression/ > **`WebAssembly` 说明** 以下内容不适用于 `Blazor WebAssembly` 应用。 在现代主流的 Web 框架中,多数已内置支持服务器响应内容的压缩机制,其中最常用的压缩方式包括 `gzip`、`deflate`、`brotli` 和 `zstd`。在发起 `HTTP` 请求时,若服务器返回的响应内容启用了压缩,默认情况下,框架默认会自动解压这些内容(针对 `gzip`、`deflate`、`brotli` 和 `zstd` 格式)。 若需启用自定义解压功能,可通过配置客户端行为实现。具体方法如下: ### 配置自动解压 ```cs showLineNumbers {3,5} // 配置默认客户端 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All, // 启用 gzip、deflate、brotli 和 zstd 自动解压 // AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate // 仅启用 gzip 和 deflate 自动解压 }); ``` 通过上述配置,若将 `AutomaticDecompression` 设置为 `DecompressionMethods.All`,则框架会自动处理 `gzip`、`deflate`、`brotli` 和 `zstd` 压缩格式的响应内容;若仅设置为 `DecompressionMethods.GZip | DecompressionMethods.Deflate`,则只会自动解压使用 `gzip` 和 `deflate` 压缩的内容。 需要注意的是:当响应头中的 `Content-Encoding` 包含上述压缩方式时,框架才会尝试自动解压。因此,在未包含 `brotli` 的配置中,相关响应内容将不会被自动解压。 ### 手动处理解压(如自动解压失效或处理非标准编码) 当需要手动处理解压时,可按以下步骤操作: 1. 关闭自动解压 ```cs showLineNumbers {5} // 配置默认客户端,关闭所有自动解压 services.AddHttpClient(string.Empty) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AutomaticDecompression = DecompressionMethods.None // 禁用自动解压 }); ``` 2. 检查响应头的 `Content-Encoding` ```cs showLineNumbers {2,4} var response = await httpRemoteService.GetAsync("https://furion.com"); if (response.Content.Headers.ContentEncoding.Contains("gzip")) { // 检测到 gzip 编码,可进行手动解压 } ``` 3. 执行手动解压(以 `gzip` 为例) ```cs showLineNumbers {1-3,5} using var responseStream = await response.Content.ReadAsStreamAsync(); using var gzipStream = new GZipStream(responseStream, CompressionMode.Decompress); using var reader = new StreamReader(gzipStream); var content = await reader.ReadToEndAsync(); Console.WriteLine(content); ``` --- # 8.13 JSON 序列化配置 > 原文链接:https://http.furion.net/docs/faq/json-serialization/ 框架默认采用 `System.Text.Json` 处理 `HTTP` 请求的 `JSON` 序列化,支持以下配置方式: - **全局默认配置** 通过 `HttpRemoteOptions` 为所有 `HttpClient` 设置统一的 `JSON` 序列化行为: ```cs showLineNumbers {2} services.AddHttpRemote(builder => {}) .ConfigureOptions(options => // 或使用重载:.ConfigureOptions((options, serviceProvider) => { options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); ``` - **客户端级配置(优先级更高)** 当同时存在全局配置和客户端级配置时,框架会优先采用客户端级配置: ```cs showLineNumbers {3,10} // 配置默认客户端 services.AddHttpClient(string.Empty) .ConfigureOptions(options => // 或使用重载:.ConfigureOptions((options, serviceProvider) => { options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); // 配置特定客户端 services.AddHttpClient("furion") .ConfigureOptions(options => // 或使用重载:.ConfigureOptions((options, serviceProvider) => { options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); ``` 使用示例: ```cs showLineNumbers {2} var model = await httpRemoteService.SendAsAsync(HttpRequestBuilder.Get("https://furion.net/test-json") .SetHttpClientName("furion")); // 不设置默认为 string.Empty ``` - **手动处理(完全控制)** 当需要特殊处理时可直接操作原始响应: ```cs showLineNumbers {2} var jsonString = await httpRemoteService.GetAsStringAsync("https://furion.net/test-json"); var model = JsonSerializer.Deserialize(jsonString, new JsonSerializerOptions()); ``` --- 如果您希望替换框架默认的 `System.Text.Json` 序列化提供程序,例如使用 `Newtonsoft.Json` 来提供 `JSON` 序列化配置选项,那么您可以通过实现 `IHttpContentProcessor` 接口来满足这一自定义需求。 > **框架建议** 但请注意,除非有充分的理由,否则通常建议使用 `System.Text.Json`,因为它与 `.NET Core` 紧密集成,且性能优异。 --- # 8.14 禁用分布式跟踪上下文(traceparent 标头) > 原文链接:https://http.furion.net/docs/faq/disabling-the-distributed-tracing-context-the-traceparent-header/ 在发起 `HTTP` 远程请求时,默认情况下,当前的 `Activity` 所包含的分布式跟踪上下文(如 `traceparent` 和 `tracestate`)会被自动注入到请求的 `HTTP` 标头中。这一机制有助于下游服务正确识别并关联到同一分布式跟踪链路。 如果出于某些业务或安全需求,需要禁用该自动注入行为,可以通过以下代码进行配置: ```csharp showLineNumbers {3,5} // 配置默认客户端 services.AddHttpClient(string.Empty, client => { }) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler() { ActivityHeadersPropagator = null // 禁用分布式上下文传播,或使用 DistributedContextPropagator.CreateNoOutputPropagator() }); ``` 通过将 `ActivityHeadersPropagator` 设置为 `null`,可阻止运行时自动将当前活动的分布式跟踪信息注入到 HTTP 请求头中,从而实现对跟踪上下文传播的控制。相关 Issue 了解:[#109558](https://github.com/dotnet/runtime/issues/109558),[#90407](https://bgithub.xyz/dotnet/runtime/issues/90407),[#IC7ZZS](https://gitee.com/dotnetchina/HttpAgent/issues/IC7ZZS)。 --- # 8.15 设置 Content-Type(MIME) > 原文链接:https://http.furion.net/docs/faq/setting-content-type-mime/ 在发送 `HTTP` 远程请求时,如果请求中包含数据体,通常需要正确设置 `Content-Type` 请求头,以指明发送内容的 `MIME` 类型,例如 `application/json` 或 `application/x-www-form-urlencoded`。 **手动输入 `MIME` 类型容易拼写错误**,从而引发潜在问题。为避免此类错误,推荐使用框架提供的 `MediaTypeNames` 静态类,该类封装了常用的标准 `MIME` 类型常量,能够有效提升代码的可读性和健壮性。 以下是使用 `MediaTypeNames` 设置 `Content-Type` 的示例代码: ```cs showLineNumbers {2,5,8} HttpRequestBuilder.Get("https://furion.net/") .SetContentType(MediaTypeNames.Text.Plain); // text/plain HttpRequestBuilder.Post("https://furion.net/") .SetContentType(MediaTypeNames.Application.Json); // application/json HttpRequestBuilder.Post("https://furion.net/") .SetContentType(MediaTypeNames.Application.FormUrlEncoded); // application/x-www-form-urlencoded ``` 通过上述方式设置 `Content-Type`,不仅避免了手动输入错误,还能提升开发效率与代码可维护性。 --- # 8.16 在非依赖注入环境中使用(Console/WinForms/WPF) > 原文链接:https://http.furion.net/docs/faq/di/ 在 `ASP.NET Core` 或 `Worker Service` 等应用程序中,通常内置了依赖注入支持。你只需在 `Startup.cs` 或 `Program.cs` 文件中,通过 `services` 或 `builder.Services` 注册所需服务即可。 但在某些特定场景下,例如控制台应用(`Console`)、`WinForms` 或 `WPF` 项目中,`.NET` 并未默认集成完整的依赖注入容器。针对这些场景,您可以使用 `HttpRemoteClient` 静态类的 `Service` 属性来发起远程 `HTTP` 请求: ```cs showLineNumbers var result = await HttpRemoteClient.Service.GetAsStringAsync("https://furion.net/"); ``` ### 自定义配置 HTTP 远程请求服务 如果需要自定义配置 `HTTP` 远程请求服务,可以通过调用 `HttpRemoteClient.Configure(services => {})` 方法进行设置: ```cs showLineNumbers {1,7} HttpRemoteClient.Configure(services => { // 示例:配置默认的 HttpClient services.AddHttpClient(string.Empty).AddProfilerDelegatingHandler(); // 如需自定义 HTTP 远程请求服务,可通过调用 AddHttpRemote() 进行配置;默认情况下无需注册 ⚠️ // services.AddHttpRemote(); }); ``` 完成自定义配置后,`HttpRemoteClient.Service` 静态属性将自动使用最新的服务构建信息实例。 ### 与应用程序依赖注入容器共享实例 虽然 `HttpRemoteClient` 主要面向非依赖注入场景,但也允许注入应用程序自身的根服务容器,使其与现有依赖注入体系共享同一个 `IHttpRemoteService` 实例。这在已经构建好 `IServiceProvider` 的任意应用中(包括 `Console`、`WinForms`、`WPF`、`Worker Service` 及 `ASP.NET Core` 等)均可使用。 #### 在通用主机、`Worker Service` 或控制台应用中 在已经构建出 `IHost` 或 `IServiceProvider` 的场景下,直接调用 `UseHttpRemoteClient` 扩展方法注入根容器即可: ```cs showLineNumbers {7,9} // 以 Worker Service 为例 var builder = Host.CreateApplicationBuilder(args); builder.Services.AddHttpRemote(); var host = builder.Build(); // 注入根容器 host.Services.UseHttpRemoteClient(); var result = await HttpRemoteClient.Service.GetAsStringAsync("https://furion.net/"); ``` #### 在 `ASP.NET Core` 应用中 提供了专门的 `UseHttpRemoteClient` 扩展方法,可在 `Program.cs` 中一行注入: ```cs showLineNumbers {4} var builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpRemote(); var app = builder.Build().UseHttpRemoteClient(); // 将根容器注入 HttpRemoteClient app.Run(); ``` 此后,`HttpRemoteClient.Service` 与 `app.Services.GetRequiredService()` 返回的实例完全相同,不再维护独立的内部容器。 > **关于 `HttpRemoteClient.Service` 实例的说明** 请注意: - **默认情况**:`HttpRemoteClient.Service` 内部维护了一个独立的 `IServiceProvider` 实例,与应用程序主机的依赖注入容器彼此隔离,因此获取到的实例也是不同的。 - **注入外部容器后**:若通过 `SetServiceProvider` 或 `UseHttpRemoteClient` 注入了应用程序的根容器,则 `HttpRemoteClient.Service` 将直接使用外部容器的 `IHttpRemoteService` 实例,与依赖注入环境下的实例完全一致,不再维护独立容器。 无论哪种方式,建议在应用程序关闭或不再需要 `HTTP` 远程请求服务时,手动调用 `HttpRemoteClient.Dispose()` 方法以释放资源(该操作仅释放内部自行构建的容器,不会影响外部注入的容器): ```cs showLineNumbers HttpRemoteClient.Dispose(); ``` **请注意:一旦调用了 `Dispose()` 方法,`HttpRemoteClient.Service` 将无法再次实例化。** --- 除了使用 `HttpRemoteClient` 静态类提供的 `Service` 属性外,您还可以选择手动构建服务容器并从中解析 `IHttpRemoteService` 实例,示例如下: ```csharp showLineNumbers {2,5,8,11} // 创建一个新的 ServiceCollection 实例 var services = new ServiceCollection(); // 向 ServiceCollection 中添加所需的 HTTP 远程请求服务 services.AddHttpRemote(); // 构建 ServiceProvider 实例 using var provider = services.BuildServiceProvider(); // 从 ServiceProvider 中获取 IHttpRemoteService 实例 var httpRemoteService = provider.GetRequiredService(); ``` 这种方式适用于需要完全控制依赖注入容器生命周期的场景。如果希望 `HttpRemoteClient.Service` 也与手动构建的容器共享服务实例,请使用 `provider.UseHttpRemoteClient()` 注入该容器,而不是分别创建。 --- # 8.17 并行请求(批量下载) > 原文链接:https://http.furion.net/docs/faq/parallel/ 在需要同时发送多个 `HTTP` 请求的场景下(如批量下载、并发调用多个 `API`、混合执行不同类型的请求等),可以使用框架内置的 `ParallelUtility` 工具类,提供更简洁的 `API`,并支持控制最大并发数。 ### 对集合并发执行相同操作 使用 `ParallelUtility.ForEachAsync` 对集合中的每个元素并发执行相同的异步操作: ```cs showLineNumbers {9-12,14} var urls = new[] { "https://furion.net/api/users", "https://furion.net/api/orders", "https://furion.net/api/products" }; // 并发请求,默认最大并发数为 4 var results = await ParallelUtility.ForEachAsync(urls, async (url, token) => { return await _httpRemoteService.GetAsStringAsync(url, cancellationToken: token); }); // results 按原始集合顺序返回 ``` 您还可以通过 `maxDegreeOfParallelism` 参数控制最大并发数: ```cs showLineNumbers {4} await ParallelUtility.ForEachAsync(urls, async (url, token) => { await _httpRemoteService.GetAsStringAsync(url, cancellationToken: token); }, maxDegreeOfParallelism: 2); // 最多同时执行 2 个请求 ``` ### 并发执行多个不同的操作 当需要同时执行多个**不同类型**的操作时,使用 `ParallelUtility.RunAsync`: ```cs showLineNumbers {3-5} // 并发执行多个不同的请求,所有操作同时执行 await ParallelUtility.RunAsync( token => _httpRemoteService.GetAsStringAsync("https://furion.net/api/users", cancellationToken: token), token => _httpRemoteService.PostAsStringAsync("https://furion.net/api/orders", "\"Furion\"", cancellationToken: token), token => _httpRemoteService.DownloadFileWithConsoleProgressAsync("https://furion.net/logo.png", @"C:\Workspaces\", FileExistsBehavior.Overwrite, cancellationToken: token) ); ``` 若需要获取返回值,可使用泛型重载: ```cs showLineNumbers {2-4,7} var results = await ParallelUtility.RunAsync( async token => await _httpRemoteService.GetAsStringAsync("https://furion.net/api/users", cancellationToken: token), async token => await _httpRemoteService.GetAsStringAsync("https://furion.net/api/orders", cancellationToken: token), async token => await _httpRemoteService.GetAsStringAsync("https://furion.net/api/products", cancellationToken: token) ); // results[0]、results[1]、results[2] 按传入顺序对应各操作的结果 ``` > **同步版本** `ParallelUtility` 同样提供了同步版本,适用于非异步上下文。 --- # 8.18 使用 LoadIntoBufferAsync 缓存响应内容 > 原文链接:https://http.furion.net/docs/faq/using-loadintobufferasync-to-cache-response-content/ 在发送 `HTTP` 远程请求时,默认情况下响应内容以流的形式返回,**只能被读取一次**。若您需要在多个地方重复读取响应内容(例如同时进行日志记录、内容校验和业务处理),直接读取流会导致后续读取为空或异常。 此时,可以调用 `HttpContent.LoadIntoBufferAsync()` 方法,将响应内容**缓冲到内存**中,从而实现后续**重复读取**。 ```cs showLineNumbers {4,7-8} var response = await httpRemoteService.GetAsync("https://furion.net/"); // 将响应内容加载到内存缓冲区 await response.Content.LoadIntoBufferAsync(); // 现在可以多次读取响应内容 var content1 = await response.Content.ReadAsStringAsync(); var content2 = await response.Content.ReadAsStringAsync(); ``` > **注意事项** - `LoadIntoBufferAsync` 会将整个响应体加载到内存,对于**大响应**(如大文件下载)可能会占用较多内存,请谨慎使用。 - 若响应内容已经为 `ByteArrayContent` 等内存类型,则无需调用此方法,其本身支持重复读取。 - 该方法在 `ETag` 缓存管道处理器中已内部使用,以确保响应内容在缓存后仍可被调用方正常读取。 --- # 8.19 与旧版 Java 程序提供的 API 对接 > 原文链接:https://http.furion.net/docs/faq/integrating-with-apis-provided-by-legacy-java-programs/ 在与旧版 `Java` 程序提供的 `API` 接口进行对接时,通常需要启用自动设置 `Host` 标头的功能,以确保请求能够正常发送。例如: ```cs showLineNumbers {2} HttpRequestBuilder.Get("https://furion.net") .AutoSetHostHeader(); // 启用自动设置 Host 标头 ``` --- # 8.20 AddHttpRemote 二义性错误 > 原文链接:https://http.furion.net/docs/faq/addhttpremote-ambiguity-error/ 若遇到 `AddHttpRemote` 方法的二义性错误,可通过为其添加一个空的委托参数来解决,示例如下: ```cs showLineNumbers services.AddHttpRemote(builder => {}); ``` --- # 8.21 反馈与建议 > 原文链接:https://http.furion.net/docs/faq/feedback-and-suggestions/ > **与我们交流** 欢迎向 HttpAgent 提交 [Issue](https://gitee.com/dotnetchina/HttpAgent/issues/new?issue)。 --- > **进一步了解** 如需深入了解 `HTTP` 和 `HttpClient` 的相关知识,可参考以下文档章节: - [ASP.NET Core - 发出 HTTP 请求](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/http-requests) - [.NET 中的 HTTP 支持](https://learn.microsoft.com/zh-cn/dotnet/fundamentals/networking/http/http-overview) --- # 1.6 更新日志 > 原文链接:https://http.furion.net/docs/changelog/ ## 📝 更新日志 - **新特性** - 新增 `HTTP` 远程请求 `Mock` 模拟测试功能支持 4.9.9.74 ⏱️2026.08.13 [320c865](https://gitee.com/dotnetchina/Furion/commit/320c86569d5d796e3d42093f4c57e121e1800f4f) - 新增 `HTTP` 远程请求自定义 `Logger` 支持 4.9.9.74 ⏱️2026.08.13 [320c865](https://gitee.com/dotnetchina/Furion/commit/320c86569d5d796e3d42093f4c57e121e1800f4f) - 新增 `HTTP` 远程请求支持设置 `MCP 2.0` 消息内容 4.9.9.71 ⏱️2026.08.11 [5513b32](https://gitee.com/dotnetchina/Furion/commit/5513b324df29c54ea508552937bde5f526494036) - 新增 `HTTP` 远程请求支持直接发送文件内容和二进制流内容 4.9.9.68 ⏱️2026.08.09 [63735d6](https://gitee.com/dotnetchina/Furion/commit/63735d64030c725902a3b7b89352fe14574047b8) - 新增 `HTTP` 远程请求支持从 `cURL` 命令字符串创建 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - 新增 `HTTP` 远程请求支持对特定类型的请求内容进行追加操作 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - 新增 `HTTP` 远程请求支持传入 `Key: Value` 键值对以设置请求标头 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - 新增 `HTTP` 远程请求支持启用标准请求标头功能 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - 新增 `HTTP` 远程请求 `HttpContext` 转发全局配置选项 `ConfigureForwardOptions` 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - 新增 `HTTP` 远程请求中内置微信开发平台的 `Access Token` 提供器 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 新增 `HTTP` 远程请求发送 `Server-Sent Events` 时支持获取原始消息行 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 新增 `HTTP` 远程请求 `HttpRemoteClient` 静态类支持使用外部服务容器(解决静态类无法应用外部服务配置,以及旧版本字符串扩展请求升级的问题) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 新增 `HTTP` 远程请求预配置 `HttpRequestBuilder` 的抽象基类 `HttpRequestBuilderConfigurator` 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 新增 `HTTP` 远程请求中 `HttpRequestBuilder` 和 `HttpFileUploadBuilder` 构建器支持追加多部分表单内容(`WithMultipart(u=>{})`) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 新增 `HTTP` 远程请求中 `HttpRequestBuilder` 支持设置 `SOAPAction` 方法(用于 `WebService`) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 新增 `HTTP` 远程请求发送 `Server-Sent Events` 时支持自动修正带请求内容的 `GET` 和 `HEAD` 请求(自动转为 `POST`) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 新增 `HTTP` 远程请求的 `UseHttpRemoteClient(serviceProvider)` 相关扩展方法,支持预设 `HttpRemoteClient` 静态类使用外部服务容器 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 新增 `HTTP` 远程请求下载文件时支持 `RFC 2047`、`RFC 5987` 和 `Latin-1`(`Mojibake`)编码的文件名解析 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 新增 `HTTP` 远程请求发送 `Server-Sent Events` 的相关辅助方法(设置响应头和流式输出) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 新增 `HTTP` 远程请求重定向时可配置委托 `SetOnRedirect` 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 新增 `HTTP` 远程请求带控制台进度打印的上传和下载文件方法 4.9.9.55 ⏱️2026.08.02 [8bd413e](https://gitee.com/dotnetchina/Furion/commit/8bd413eea24bd4b08a43affe546588ba694f2e17) - 新增 `HTTP` 远程请求 `Action` 操作符,支持将 `HttpRequestBuilder` 隐式转换为 `Action` 4.9.9.52 ⏱️2026.08.01 [2c3d42d](https://gitee.com/dotnetchina/Furion/commit/2c3d42d5783e041222340e2230502b85bfaa9974) - 新增 `HTTP` 远程请求 `HttpRequestBuilder.Setup` 和 `HttpBuilder.Setup` 静态属性 4.9.9.52 ⏱️2026.08.01 [2c3d42d](https://gitee.com/dotnetchina/Furion/commit/2c3d42d5783e041222340e2230502b85bfaa9974) - 新增 **`HTTP` 远程请求 `ETag` 缓存功能** 4.9.9.48 ⏱️2026.07.31 [53c6eb8](https://gitee.com/dotnetchina/Furion/commit/53c6eb8850aff618ee97bbfad8f2b0fd5b5bb34d) - 新增 **`HTTP` 远程请求配额策略功能** 4.9.9.46 ⏱️2026.07.29 [22c8edb](https://gitee.com/dotnetchina/Furion/commit/22c8edbe5e249fe10ad011d0dc8982b61278422c) - 新增 `HTTP` 远程请求支持 `zstd` 解压(`.NET11`) 4.9.9.46 ⏱️2026.07.29 [22c8edb](https://gitee.com/dotnetchina/Furion/commit/22c8edbe5e249fe10ad011d0dc8982b61278422c) - 新增 `HTTP` 远程请求 `ContentEquals`、`ContentMatches`、`ContentNotEmpty` 和 `HeaderNotExists` 断言方法 4.9.9.46 ⏱️2026.07.29 [95f0380](https://gitee.com/dotnetchina/Furion/commit/95f0380bf4be2430aa28ef80405140526828e649) - 新增 `HTTP` 远程请求 `SSE` 和长轮询支持返回 `IAsyncEnumerable` 4.9.9.43 ⏱️2026.07.26 [5fd05a1](https://gitee.com/dotnetchina/Furion/commit/5fd05a1bacf8bf67883b3bb8000164e41cacee32) - 新增 `HTTP` 远程请求 `Furion` 框架自动刷新 `Token` 功能 4.9.9.39 ⏱️2026.07.21 [6611c06](https://gitee.com/dotnetchina/Furion/commit/6611c0686b963add05ba7119cf0a4ffa41b21a9d) - 新增 `HTTP` 远程请求 `JWT` 解析工具类 `JwtTokenUtility` 4.9.9.39 ⏱️2026.07.21 [6611c06](https://gitee.com/dotnetchina/Furion/commit/6611c0686b963add05ba7119cf0a4ffa41b21a9d) - 新增 `HTTP` 远程请求支持为特定 `HttpClient` 配置 `IHttpRequestEventHandler` 4.9.9.39 ⏱️2026.07.21 [6611c06](https://gitee.com/dotnetchina/Furion/commit/6611c0686b963add05ba7119cf0a4ffa41b21a9d) - 新增 `HTTP` 远程请求支持配置查询参数、请求标头及 `Cookie` 的格式化字符串 4.9.9.38 ⏱️2026.07.21 [b783f3e](https://gitee.com/dotnetchina/Furion/commit/b783f3e00611d6bd84055fc0a1289bc20ccb7128) - 新增 `HTTP` 远程请求重试策略默认记录日志 4.9.9.38 ⏱️2026.07.21 [b783f3e](https://gitee.com/dotnetchina/Furion/commit/b783f3e00611d6bd84055fc0a1289bc20ccb7128) - 新增 `HTTP` 远程请求在启用异常抑制时,若发生异常则自动输出警告日志 4.9.9.38 ⏱️2026.07.21 [b783f3e](https://gitee.com/dotnetchina/Furion/commit/b783f3e00611d6bd84055fc0a1289bc20ccb7128) - 新增 `HTTP` 远程请求无依赖声明式代理支持 4.9.9.37 ⏱️2026.07.20 [f732128](https://gitee.com/dotnetchina/Furion/commit/f7321287388e3998b373d93cf38ce3c6f01a2e9f) - 新增 `HTTP` 远程声明式请求支持 `ValueTask` 返回值类型 4.9.9.36 ⏱️2026.07.19 [a221418](https://gitee.com/dotnetchina/Furion/commit/a221418fecae702978ce301f3b66dba2d2a17d41) - 新增 `HTTP` 远程请求 `URL` 地址支持 `{key?}` 和 `{**key}` 路径参数 4.9.9.36 ⏱️2026.07.19 [a221418](https://gitee.com/dotnetchina/Furion/commit/a221418fecae702978ce301f3b66dba2d2a17d41) - 新增 `HTTP` 远程请求 `HttpRemoteResult` 内容转换器 4.9.9.34 ⏱️2026.07.18 [3ae3f64](https://gitee.com/dotnetchina/Furion/commit/3ae3f64e8f42fd91e21707b6847550190a50f040) - 新增 `HTTP` 远程请求支持 `RFC 3986` 标准的请求地址拼接 4.9.9.34 ⏱️2026.07.18 [3ae3f64](https://gitee.com/dotnetchina/Furion/commit/3ae3f64e8f42fd91e21707b6847550190a50f040) - 新增 `HTTP` 远程请求 `Access Token` 自动管理功能 4.9.9.25 [d618a54](https://gitee.com/dotnetchina/Furion/commit/d618a54ed3ef9dc3c42d625e26f6d0360554e157) - 新增 `HTTP` 远程请求重试功能 4.9.9.25 [d618a54](https://gitee.com/dotnetchina/Furion/commit/d618a54ed3ef9dc3c42d625e26f6d0360554e157) - 新增 `HTTP` 远程请求支持自定义发送请求的管道处理器 4.9.9.25 [d618a54](https://gitee.com/dotnetchina/Furion/commit/d618a54ed3ef9dc3c42d625e26f6d0360554e157) - 新增 `HTTP` 远程请求支持多实例服务注册 4.9.9.16 [1284ea8](https://gitee.com/dotnetchina/Furion/commit/1284ea80ffa0e4c2c9ca44bb21650d24fd1ac632) - 新增 `HTTP` 远程请求内容处理器支持返回多个 `HttpContent` 组合 4.9.9.16 [1284ea8](https://gitee.com/dotnetchina/Furion/commit/1284ea80ffa0e4c2c9ca44bb21650d24fd1ac632) - 新增 `HTTP` 远程请求构建器 `RemoveContent` 方法 4.9.9.16 [1284ea8](https://gitee.com/dotnetchina/Furion/commit/1284ea80ffa0e4c2c9ca44bb21650d24fd1ac632) - 新增 `HTTP` 远程请求支持批量添加请求结束时需要释放的对象方法:`AddDisposables` 4.9.9.16 [1284ea8](https://gitee.com/dotnetchina/Furion/commit/1284ea80ffa0e4c2c9ca44bb21650d24fd1ac632) - 新增 `HTTP` 远程请求获取本地 `IP` 地址和 `MAC` 地址方法:`HttpRemoteUtility.GetLocalIPv4()`,`HttpRemoteUtility.GetLocalMacAddress()` 4.9.9.16 [1284ea8](https://gitee.com/dotnetchina/Furion/commit/1284ea80ffa0e4c2c9ca44bb21650d24fd1ac632) - 新增 `HTTP` 远程请求可通过 `.SetBaseAddress(url)` 或 `[BaseAddress(url]` 充当公共前缀功能 4.9.9.13 ⏱️2026.07.05 [0224fa0](https://gitee.com/dotnetchina/Furion/commit/0224fa0a5bdf21ee5f1488b0697b4f12daf95375) - 新增 `HTTP` 远程请求支持添加泛型响应内容转换器 4.9.9.10 ⏱️2026.07.03 [9a37cfa](https://gitee.com/dotnetchina/Furion/commit/9a37cfa1f89f1decc0b745d3b73302c15e8c35ac) - 新增 `HTTP` 远程请求 `JSON` 响应反序列化包装器 `ResultHandler` 功能 4.9.9.10 ⏱️2026.07.03 [9a37cfa](https://gitee.com/dotnetchina/Furion/commit/9a37cfa1f89f1decc0b745d3b73302c15e8c35ac) - 新增 `HTTP` 远程请求支持发送 `IBrowserFile` 文件 4.9.9.9 ⏱️2026.07.02 [fa73c28](https://gitee.com/dotnetchina/Furion/commit/fa73c28dbfff64d275ec85385983bcec64a7df71) - 新增 `HTTP` 声明式请求支持通过 `[Header("键: 值")]` 方式设置标头 4.9.9.9 ⏱️2026.07.02 [fa73c28](https://gitee.com/dotnetchina/Furion/commit/fa73c28dbfff64d275ec85385983bcec64a7df71) - 新增 `HTTP` 远程请求 `SetContent` 方法支持配置 `disposeResourcesOnRequestCompletion` 参数 4.9.9.9 ⏱️2026.07.02 [fa73c28](https://gitee.com/dotnetchina/Furion/commit/fa73c28dbfff64d275ec85385983bcec64a7df71) - 新增 `HTTP` 远程请求 `IFormFile` 内容处理器支持 4.9.9.9 ⏱️2026.07.02 [8a8e44b](https://gitee.com/dotnetchina/Furion/commit/8a8e44b3490a7195911759005b2c5c17d037b20a) - 新增 `HTTP` 远程请求 `IAsyncEnumerable` 响应内容类型支持 4.9.9.3 ⏱️2026.06.25 [4b80b9b](https://gitee.com/dotnetchina/Furion/commit/4b80b9b484a9b1ef2cc29a3387a9a1a184a61491) - 新增 `HTTP` 远程请求将流或字节数组保存到本地文件功能 4.9.8.97 ⏱️2026.06.16 [c8725dc](https://gitee.com/dotnetchina/Furion/commit/c8725dcc890d443aa6e8d0c888b634001bb2b77e) - 新增 `HTTP` 远程请求 `FileInfo` 请求内容处理器 4.9.8.92 ⏱️2026.06.08 [c9e3471](https://gitee.com/dotnetchina/Furion/commit/c9e347114c929ba1070a928ac69d8a284de22485) - 新增 `HTTP` 远程请求支持 双重序列化 `JSON` 的响应内容处理 4.9.8.66 ⏱️2026.05.15 [54466a9](https://gitee.com/dotnetchina/Furion/commit/54466a91119cfd15581e65fc6048f4a9357857e0) - 新增 `HTTP` 远程请求转发 `HttpContext` 选项 `IgnoreQueryParameters` 支持 4.9.8.64 ⏱️2026.05.13 [18d1922](https://gitee.com/dotnetchina/Furion/commit/18d1922b157d31dbec2a55302e8f173023559c70) - 新增 `HTTP` 远程请求 `JSON Lines` 数据格式支持 4.9.8.63 ⏱️2026.05.12 [9abc295](https://gitee.com/dotnetchina/Furion/commit/9abc295a58776f7754cd01888c26c34508733694) - 新增 `HTTP` 远程请求查询参数排序、表单字段提交排序支持 4.9.8.58 ⏱️2026.05.06 [d247eff](https://gitee.com/dotnetchina/Furion/commit/d247effaf9dd7d6526ba5589ecfd6b83e9f28c9d) - 新增 `HTTP` 远程请求支持通过 `SetUserAgent` 方法设置请求 `User-Agent` 标头 4.9.8.58 ⏱️2026.05.06 [d247eff](https://gitee.com/dotnetchina/Furion/commit/d247effaf9dd7d6526ba5589ecfd6b83e9f28c9d) - 新增 `HTTP` 远程请求 `UserAgents` 静态类 4.9.8.58 ⏱️2026.05.06 [d247eff](https://gitee.com/dotnetchina/Furion/commit/d247effaf9dd7d6526ba5589ecfd6b83e9f28c9d) - 新增 `HTTP` 声明式请求支持禁用数据验证 4.9.8.48 ⏱️2026.04.22 [3979720](https://gitee.com/dotnetchina/Furion/commit/39797204ea4a1a6644790f621f811287175d87f6) - 新增 `HTTP` 远程请求 `UriBuilder` 配置操作 4.9.8.45 ⏱️2026.04.19 [56be6c6](https://gitee.com/dotnetchina/Furion/commit/56be6c63d7ec079559cddf45531f8232ead19381) - 新增 `HTTP` 声明式请求支持面向对象继承 4.9.8.42 ⏱️2026.04.17 [b00f2b9](https://gitee.com/dotnetchina/Furion/commit/b00f2b9a1ebbe8e2005af6e2eb940a3eff48ebc4) - 新增 `HTTP` 远程请求支持设置永不超时 4.9.8.21 ⏱️2026.03.09 [92e0283](https://gitee.com/dotnetchina/Furion/commit/92e0283b8aba9f1cc9eeb9540392855095f2f0b5) - 新增 `HTTP` 远程请求支持发送不进行 `URL` 编码的表单数据 4.9.8.15 ⏱️2026.02.09 [f0104ef](https://gitee.com/dotnetchina/Furion/commit/f0104ef8154585b2f3d8faec39def9b4fb3797e2) - 新增 `HTTP` 远程请求声明式请求支持 `Action` 冻结参数 4.9.7.244 ⏱️2026.01.09 [d9fce11](https://gitee.com/dotnetchina/Furion/commit/d9fce115f4bda37d743fe97e7f3b97e7d3eee9f3) - 新增 `HTTP` 远程请求支持 `HttpRequestBuilder` 统一配置器 `IHttpRequestBuilderConfigurer` 4.9.7.244 ⏱️2026.01.09 [d9fce11](https://gitee.com/dotnetchina/Furion/commit/d9fce115f4bda37d743fe97e7f3b97e7d3eee9f3) - 新增 `HTTP` 远程请求支持提供从互联网 `URL` 地址下载文件流配置 `HttpClient` 和 `HttpRequestMessage` 实例 4.9.7.235 ⏱️2025.12.27 [a723ae5](https://gitee.com/dotnetchina/Furion/commit/a723ae5cbb93985969c342f7dffb22024e314e5b) - 新增 `HTTP` 远程请求设置请求标头和 `Cookie` 支持配置参数 4.9.7.231 ⏱️2025.12.19 [541fadd](https://gitee.com/dotnetchina/Furion/commit/541fadd4cebae94fc3686ffb36a337b193a307c5) - 新增 `HTTP` 远程请求支持指定网卡 `IP` 地址请求 4.9.7.230 ⏱️2025.12.19 [904705d](https://gitee.com/dotnetchina/Furion/commit/904705d20de53c41eed9d798ccbdb50be13a2408) - 新增 `HTTP` 远程请求 `HttpBuilder` 静态类,用于简化 `HttpRequestBuilder` 名称过长问题 4.9.7.222 ⏱️2025.12.08 [c0b6c77](https://gitee.com/dotnetchina/Furion/commit/c0b6c77bdc5d4ea83de9891df8dac002a23404ad) - 新增 `HTTP` 远程请求分析日志支持颜色高亮 4.9.7.217 ⏱️2025.12.03 [29b9348](https://gitee.com/dotnetchina/Furion/commit/29b93485d9d4f14215dcac687e5601321438aa4c) - 新增 `HTTP` 远程请求支持设置 `JSON` 响应反序列化包装器 4.9.7.214 ⏱️2025.11.26 [f046b4d](https://gitee.com/dotnetchina/Furion/commit/f046b4d423b90a6f6d6aa27cab0f0f671225dff6) [ebe71f9](https://gitee.com/dotnetchina/Furion/commit/ebe71f94f126741153a12e3857ba2536668bd8e6) - 新增 `HTTP` 远程请求支持在未配置日志服务时设置日志回退输出委托 4.9.7.213 ⏱️2025.11.26 [17ac155](https://gitee.com/dotnetchina/Furion/commit/17ac155b562c8a18eb2c758777aa4776b2f485eb) - 新增 `HTTP` 远程请求在设置 `JSON` 数据时支持传入 `JsonSerializerOptions` 对象 4.9.7.208 ⏱️2025.11.16 [c97b467](https://gitee.com/dotnetchina/HttpAgent/commit/c97b467f87a56512d8d196e51c969c05ffa180c0) - 新增 `HTTP` 远程请求支持自动修复无效的响应字符编码 4.9.7.202 ⏱️2025.11.13 [35530e8](https://gitee.com/dotnetchina/Furion/commit/35530e889181beed9672619989a9cbe2edd2c7ca) - 新增 `HTTP` 远程请求支持表单名称命名策略或自定义转换器 4.9.7.137 ⏱️2025.11.07 [6c175a8](https://gitee.com/dotnetchina/Furion/commit/6c175a8502806ff434898b19160874592efbdca3) - 新增 **`HTTP` 远程请求断言功能** 4.9.7.137 ⏱️2025.11.07 [c044b87](https://gitee.com/dotnetchina/Furion/commit/c044b87cd1a6243637612872df1dd4a3a8cf4100) - 新增 `HTTP` 远程请求默认启用响应内容 `gzip`、`deflate`、`brotli` 和 `zstd` 自动解压 4.9.7.137 ⏱️2025.11.07 [e9b10ac](https://gitee.com/dotnetchina/Furion/commit/e9b10ac12128e8b79cf320cf4110e87cc827b5fc) - 新增 `HTTP` 远程请求请求分析工具 `Profiler(enabled)` 别名方法:`Debugger([enabled])` 4.9.7.137 ⏱️2025.11.07 [c044b87](https://gitee.com/dotnetchina/Furion/commit/c044b87cd1a6243637612872df1dd4a3a8cf4100) - 新增 `HTTP` 远程请求支持添加动态 `URL` 参数(请求时求值)4.9.7.131 ⏱️2025.10.17 [a162c8d](https://gitee.com/dotnetchina/Furion/commit/a162c8dc2e9aca1e27ed397d8d9f9784e636b559) - 新增 `HTTP` 远程请求压力测试支持便捷禁用 `HTTP` 缓存 4.9.7.131 ⏱️2025.10.17 [a162c8d](https://gitee.com/dotnetchina/Furion/commit/a162c8dc2e9aca1e27ed397d8d9f9784e636b559) - 新增 `HTTP` 远程请求支持配置多线程下载文件 4.9.7.123 ⏱️2025.09.16 [10bddc9](https://gitee.com/dotnetchina/Furion/commit/10bddc926cfcc8590bfc7153b67a9f513d9f81b0) - 新增 `HTTP` 远程请求支持将 `XML` 字符串转换为类型对象 4.9.7.123 ⏱️2025.09.16 [41746d2](https://gitee.com/dotnetchina/Furion/commit/41746d215cffa5eb3ddee2396b3f62be4c658068) - 新增 `HTTP` 远程请求构建器实例支持 `When` 条件构建 4.9.7.100 ⏱️2025.07.22 [651b4d5](https://gitee.com/dotnetchina/Furion/commit/651b4d5a5a1467facc5015017026530b05d523f9) - 新增 `HTTP` 远程请求扩展功能构建器 `With(Action)` 方法 4.9.7.95 ⏱️2025.07.10 [4615670](https://gitee.com/dotnetchina/Furion/commit/461567045a1dfced8f4a12b2af028f15b653af21) - 新增 `HTTP` 远程请求声明式 `[MultipartObject]` 特性 4.9.7.94 ⏱️2025.07.09 [7e52e9c](https://gitee.com/dotnetchina/Furion/commit/7e52e9c5b1fccfb3923d6018b2af0d9ac0c8f438) - 新增 `HTTP` 远程请求支持 `Unix epoch` 日期格式 4.9.7.77 ⏱️2025.05.31 [ca9c94e](https://gitee.com/dotnetchina/Furion/commit/ca9c94e2d50cede3edcc3911ad6593c520ac6589) - 新增 `HTTP` 远程请求 `URL` 参数格式化程序 4.9.7.70 ⏱️2025.05.23 [e8b24b3](https://gitee.com/dotnetchina/Furion/commit/e8b24b3f5480e6cdca8a56e1cd01a9cd96603bac) - 新增 `HTTP` 远程请求支持配置 `SocketsHttpHandler` 忽略 `SSL` 证书验证 4.9.7.63 ⏱️2025.05.16 [042da35](https://gitee.com/dotnetchina/Furion/commit/042da3566110ac0c7abd28319d76154a45ff6ced) - 新增 `HTTP` 远程请求支持配置请求超时发生时的回调操作 4.9.7.62 ⏱️2025.05.15 [23a580d](https://gitee.com/dotnetchina/Furion/commit/23a580daff6914a5a186d798c4dbceb6caaad5a7) - 新增 `HTTP` 远程请求 `HttpRemoteClient` 静态类 4.9.7.58 ⏱️2025.05.02 [86e9dbe](https://gitee.com/dotnetchina/Furion/commit/86e9dbe197df8efb015e4e22cba4469e715aea27) - 新增 `HTTP` 远程请求 `HttpRemoteResult` 解构函数(析构表达式)功能支持 4.9.7.53 ⏱️2025.04.28 [e4dcc10](https://gitee.com/dotnetchina/Furion/commit/e4dcc1028eccd1ffad5471646cb079778ead3ce3) - 新增 `HTTP` 远程请求 `IHttpClientBuilder.ConfigureOptions(configure)` 扩展方法 4.9.7.51 ⏱️2025.04.26 [33479e2](https://gitee.com/dotnetchina/Furion/commit/33479e212bd09f1867e69e9c428d63b7ece7fa58) - 新增 `HTTP` 远程请求请求分析工具打印 `HttpClient Name` 项 4.9.7.51 ⏱️2025.04.26 [33479e2](https://gitee.com/dotnetchina/Furion/commit/33479e212bd09f1867e69e9c428d63b7ece7fa58) - 新增 `HTTP` 远程请求 `WithSuccessStatusCodeHandler` 方法支持设置请求成功状态码回调操作 4.9.7.47 ⏱️2025.04.20 [cf7956e](https://gitee.com/dotnetchina/Furion/commit/cf7956e227d978a05c6e0d294766aa45a681f1b9) - 新增 `HTTP` 远程请求状态码处理程序支持 `~` 符号设置区间,如 `200~299` 4.9.7.47 ⏱️2025.04.20 [cf7956e](https://gitee.com/dotnetchina/Furion/commit/cf7956e227d978a05c6e0d294766aa45a681f1b9) - 新增 `HTTP` 远程请求 `SetOmitContentType(omit)` 方法支持移除或保留请求内容的 `Content-Type` 4.9.7.44 ⏱️2025.04.17 [4d98d60](https://gitee.com/dotnetchina/Furion/commit/4d98d6053f7b05c73b6d60d5284c040538f18789) - 新增 `HTTP` 远程请求支持从 `JSON` 字符串创建 `HttpRequestBuilder` 实例 4.9.7.41 ⏱️2025.04.14 [580dd04](https://gitee.com/dotnetchina/Furion/commit/580dd04362d5c6fb5753402838b8029d0793c2a4) - 新增 `HTTP` 远程请求支持使用 `SuppressExceptions()` 和 `[SuppressExceptions]` 抑制请求异常 4.9.7.40 ⏱️2025.04.12 [1a9bc7b](https://gitee.com/dotnetchina/Furion/commit/1a9bc7b90472f81cee2fa80ba1459f07484c08f7) - 新增 `HTTP` 远程请求支持设置单次请求的 `HTTP` 版本 4.9.7.40 ⏱️2025.04.12 [1a9bc7b](https://gitee.com/dotnetchina/Furion/commit/1a9bc7b90472f81cee2fa80ba1459f07484c08f7) - 新增 `HTTP` 远程请求请求分析工具打印 `HTTP Version` 项 4.9.7.40 ⏱️2025.04.12 [1a9bc7b](https://gitee.com/dotnetchina/Furion/commit/1a9bc7b90472f81cee2fa80ba1459f07484c08f7) - 新增 `HTTP` 远程请求 `HttpRemoteResult` 类型 `Version` 属性(`HTTP` 版本) 4.9.7.40 ⏱️2025.04.12 [1a9bc7b](https://gitee.com/dotnetchina/Furion/commit/1a9bc7b90472f81cee2fa80ba1459f07484c08f7) - 新增 `HTTP` 远程请求支持设置请求来源地址 4.9.7.36 ⏱️2025.04.02 [5d4a241](https://gitee.com/dotnetchina/Furion/commit/5d4a241a8dcd63ed2b7fc7a3692b3418c69d3fc5) - 新增 `HTTP` 远程请求 `HttpRequestBuilder.AddAuthentication(string, string?)` 重载方法 4.9.7.33 ⏱️2025.03.25 [f8a648a](https://gitee.com/dotnetchina/Furion/commit/f8a648a7377617817ed629da63f1154246eb244f) - 新增 `HTTP` 远程请求多部分表单 `AddFile(IFormFile)` 和 `AddFiles(IEnumerable)` 扩展方法 4.9.7.31 ⏱️2025.03.24 [6eb54e0](https://gitee.com/dotnetchina/Furion/commit/6eb54e0f6851158149ca0c48a6604839f07bbf40) - 新增 `HTTP` 远程请求反序列化时支持 `Number` 和 `Boolean` 类型转 `String` 类型 4.9.7.29 ⏱️2025.03.23 [489aa55](https://gitee.com/dotnetchina/Furion/commit/489aa55fbe05ccd889c2b168f7d012918fdb5e1e) - 新增 `HTTP` 远程请求序列化时自动处理中文乱码问题 4.9.7.29 ⏱️2025.03.23 [489aa55](https://gitee.com/dotnetchina/Furion/commit/489aa55fbe05ccd889c2b168f7d012918fdb5e1e) - 新增 `HTTP` 远程请求进行 `JSON` 反序列化时支持非 `ISO 8601-1:2019` 标准的时间字符串 4.9.7.25 ⏱️2025.03.14 [10de94b](https://gitee.com/dotnetchina/Furion/commit/10de94babfcc60cb2837714ce9de5c2284e21ced) [3f3d619](https://gitee.com/dotnetchina/Furion/commit/3f3d61965e432c6597039a630e516f59cad08de4) - 新增 `HTTP` 远程请求支持为所有 `HttpClient` 客户端添加配置 `IHttpRemoteBuilder.ConfigureHttpClientDefaults(configure)` 4.9.7.22 ⏱️2025.03.04 [cef4ca0](https://gitee.com/dotnetchina/Furion/commit/cef4ca03a727792663eb2d7b4fa8ad9f196cf3ef) - 新增 `HTTP` 远程请求支持 `WithPathSegment[s]` 设置路径片段 4.9.7.21 ⏱️2025.03.03 [7b3335e](https://gitee.com/dotnetchina/Furion/commit/7b3335e8af30509aa1f0465a881693bd3b6f114b) - 新增 `HTTP` 远程请求支持为所有 `HttpClient` 客户端启用请求分析工具 `IHttpRemoteBuilder.AddProfilerDelegatingHandler()` 4.9.7.18 ⏱️2025.03.01 [b6ba52b](https://gitee.com/dotnetchina/Furion/commit/b6ba52bea7f40098a101811c5eb403456139de3c) - 新增 `HTTP` 远程请求支持 `WebService(SOAP)` 支持 4.9.7.15 ⏱️2025.02.27 [479073a](https://gitee.com/dotnetchina/Furion/commit/479073abf3712bcb9e5566e762289281ea7e6ec1) - 新增 `HTTP` 远程请求 `AddProfilerDelegatingHandler(this IHttpClientBuilder builder, bool disableInProduction)` 重载方法 4.9.7.13 ⏱️2025.02.26 [5ef4b13](https://gitee.com/dotnetchina/Furion/commit/5ef4b13c522a824822266dbcf6ad91d8f65e701a) - 新增 `HTTP` 远程请求 `Server-Sent Events` 支持任意 `HttpMethod` 4.9.7.13 ⏱️2025.02.26 [caa2aca](https://gitee.com/dotnetchina/Furion/commit/caa2acaec7da88d7f9f879ac0fcfe73bd1dc71db) - 新增 `HTTP` 远程请求获取响应标头 `Set-Cookie` 扩展方法 4.9.7.11 ⏱️2025.02.24 [62737cf](https://gitee.com/dotnetchina/Furion/commit/62737cfccfbc130eaf9bcf8e1ffce15bf690e506) - 新增 `HTTP` 远程请求支持设置请求分析工具触发委托 4.9.7.10 ⏱️2025.02.22 [82b4d81](https://gitee.com/dotnetchina/Furion/commit/82b4d81ae60f1918f06cc28b780902f7096c4fa4) - 新增 `HTTP` 远程请求 `ConfigureOptions` 支持解析服务的重载方法 4.9.7.9 ⏱️2025.02.20 [dabbc47](https://gitee.com/dotnetchina/Furion/commit/dabbc47d78a1bfab82d367ef359ddf10d94c298d) - 新增 `HTTP` 远程请求 `HttpRemoteOptions` 选项 `FallbackBaseAddress` 属性,支持回退请求基地址设置 4.9.7.9 ⏱️2025.02.20 [dabbc47](https://gitee.com/dotnetchina/Furion/commit/dabbc47d78a1bfab82d367ef359ddf10d94c298d) - 新增 `HTTP` 远程请求 `HttpRemoteResult` 类型 `Server` 属性 4.9.7.9 ⏱️2025.02.20 [5b1c181](https://gitee.com/dotnetchina/Furion/commit/5b1c18130cb1324ec6ad4b723cd47d101f33a402) - 新增 `HTTP` 远程请求 `HttpRequestMessage` 克隆扩展方法 4.9.7.8 ⏱️2025.02.18 [abd61c8](https://gitee.com/dotnetchina/Furion/commit/abd61c888a7032e64b63943a35765a8d6eb8c46c) - 新增 `HTTP` 远程请求 `[Forward]` 转发特性支持 4.9.7 ⏱️2025.01.23 [023166b](https://gitee.com/dotnetchina/Furion/commit/023166b0439e5c43c7f3f58bd88fef3be8f98473) - 新增 `HTTP` 远程请求配置参数支持 4.9.7 ⏱️2025.01.23 [023166b](https://gitee.com/dotnetchina/Furion/commit/023166b0439e5c43c7f3f58bd88fef3be8f98473) - 新增 `HTTP` 远程请求转发支持忽略请求或响应标头 4.9.7 ⏱️2025.01.23 [023166b](https://gitee.com/dotnetchina/Furion/commit/023166b0439e5c43c7f3f58bd88fef3be8f98473) - 新增 `HTTP` 远程请求重定向支持相对路径 4.9.6.21 ⏱️2024.12.28 [17df0c4](https://gitee.com/dotnetchina/Furion/commit/17df0c473a7c91d1989e2319109a24d7404e9d65) - 新增 `HTTP` 远程请求内置自动重定向处理流程 4.9.6.20 ⏱️2024.12.27 [4998e13](https://gitee.com/dotnetchina/Furion/commit/4998e139dec691a154bfbd52463c5fd5e33f6141) - 新增 `HTTP` 远程请求 `HttpRemoteOptions` 选项 `AllowAutoRedirect` 和 `MaximumAutomaticRedirections` 配置 4.9.6.20 ⏱️2024.12.27 [4998e13](https://gitee.com/dotnetchina/Furion/commit/4998e139dec691a154bfbd52463c5fd5e33f6141) - 新增 `HTTP` 远程请求 `WithCookie(cookieHeaderValue)` 重载方法 4.9.6.18 ⏱️2024.12.25 [80394dc](https://gitee.com/dotnetchina/Furion/commit/80394dceb9d56bfbffd5612d14c350450ff8c93f) - 新增 `HTTP` 远程请求默认无配置支持 `HTTP/1.0` 和 `HTTP/1.1` 的服务器接口 4.9.6.16 ⏱️2024.12.17 [61afe9a](https://gitee.com/dotnetchina/Furion/commit/61afe9a28cad036ac51b3a457f865cad36711837) - 新增 `HTTP` 远程请求支持设置请求基地址功能 4.9.6.15 ⏱️2024.12.10 [187a178](https://gitee.com/dotnetchina/Furion/commit/187a1787cfbc202e69fcd1132a924aad19b3380b) - 新增 `HTTP` 远程请求在添加表单项内容时支持预置操作 4.9.6.12 ⏱️2024.12.06 [e610e32](https://gitee.com/dotnetchina/Furion/commit/e610e3233c201eda2397e5f9bc8b3cc7e6ee6375) - 新增 `HTTP` 远程请求在非依赖注入环境中支持打印请求分析工具内容 4.9.6.12 ⏱️2024.12.06 [e610e32](https://gitee.com/dotnetchina/Furion/commit/e610e3233c201eda2397e5f9bc8b3cc7e6ee6375) - 新增 `HTTP` 远程请求支持声明式设置 `HttpRequestMessage` 请求属性特性 4.9.6.11 ⏱️2024.12.04 [8306cf0](https://gitee.com/dotnetchina/Furion/commit/8306cf018bac468d091431efd48e0f9d934190ca) - 新增 `HTTP` 远程请求支持配置禁用请求分析工具委托 4.9.6.7 ⏱️2024.12.02 [250ea66](https://gitee.com/dotnetchina/Furion/commit/250ea66c6c9fff98480c79a26e4a5ef629b99153) - 新增 `HTTP` 远程请求支持启用性能优化支持 4.9.6.6 ⏱️2024.12.01 [b7ad81b](https://gitee.com/dotnetchina/Furion/commit/b7ad81bd4b575f1cf9f141f581eb3e7027f741af) - 新增 `HTTP` 远程请求支持设置自动 `Host` 标头 4.9.6.6 ⏱️2024.12.01 [b7ad81b](https://gitee.com/dotnetchina/Furion/commit/b7ad81bd4b575f1cf9f141f581eb3e7027f741af) - 新增 `HTTP` 远程请求 `DigestCredentials` 摘要身份认证支持 4.9.6.5 ⏱️2024.12.01 [3298c02](https://gitee.com/dotnetchina/Furion/commit/3298c027a6df5c400c2885662ce00dc01a185e62) - 新增 `HTTP` 远程请求 `FileTypeMapper` 文件 `MIME` 类型映射类 4.9.6.4 ⏱️2024.11.29 [6782110](https://gitee.com/dotnetchina/Furion/commit/6782110d073a6193c431023b8c40c7ad4fb1129e) - 新增 `HTTP` 远程请求支持带应用速率限制的流 4.9.6.3 ⏱️2024.11.28 [f281c32](https://gitee.com/dotnetchina/Furion/commit/f281c32a877dedfd006271768b9054d133a65c29) - 新增 `HTTP` 远程请求支持特定需验证 `Content-Type` 的服务器程序 4.9.6.3 ⏱️2024.11.28 [f281c32](https://gitee.com/dotnetchina/Furion/commit/f281c32a877dedfd006271768b9054d133a65c29) - 新增 `HTTP` 远程请求支持配置请求分析工具日志级别 4.9.6.3 ⏱️2024.11.28 [f281c32](https://gitee.com/dotnetchina/Furion/commit/f281c32a877dedfd006271768b9054d133a65c29) - 新增 `HTTP` 远程请求支持全局配置 `HttpRemoteOptions` 配置 4.9.6.2 ⏱️2024.11.28 [b60c996](https://gitee.com/dotnetchina/Furion/commit/b60c99699d8de8000be19077d43a08858f28f874) - 新增 `HTTP` 远程请求支持配置查询参数是否忽略空值 `ignoreNullValues` 4.9.6.2 ⏱️2024.11.28 [b60c996](https://gitee.com/dotnetchina/Furion/commit/b60c99699d8de8000be19077d43a08858f28f874) - 新增 `HTTP` 远程请求 `MultipartFile` 添加文件类型 4.9.6.1 ⏱️2024.11.27 [590cd5e](https://gitee.com/dotnetchina/Furion/commit/590cd5e93b15e9573a299710c81b8e4821e749d7) - 新增 `HTTP` 远程请求 `WithStatusCodeHandler` 支持包含比较符号类型状态码 4.9.6.1 ⏱️2024.11.27 [590cd5e](https://gitee.com/dotnetchina/Furion/commit/590cd5e93b15e9573a299710c81b8e4821e749d7) - 新增 `HTTP` 远程请求 `AddHttpDeclarativeExtractorsFromAssemblies` 批量注册 `HTTP` 声明式提取器 4.9.6.1 ⏱️2024.11.27 [590cd5e](https://gitee.com/dotnetchina/Furion/commit/590cd5e93b15e9573a299710c81b8e4821e749d7) - **突破性变化** - 调整 `HTTP` 远程请求声明式请求上下文名称 `HttpDeclarativeExtractorContext` -> `HttpDeclarativeParsingContext` 4.9.9.70 ⏱️2026.08.11 [cfbc6917](https://gitee.com/dotnetchina/Furion/commit/cfbc69174eea0621cedc262e6de8b485b00a579f) - 调整 `HTTP` 远程请求 `HttpContext` 转发默认 `CompletionOption` 选项为 `ResponseHeadersRead` 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - 调整 `HTTP` 远程请求设置 `Bearer` 授权的方法名:`AddJwtBearerAuthentication` → `AddBearerAuthentication` 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 调整 `HTTP` 远程请求设置 `URL` 构建器的方法名:`SetUriBuilder` → `SetOnUriBuilding` 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 调整 `HTTP` 远程请求的 `OnPostReceiveResponse` 事件方法为异步方法(`OnPostReceiveResponseAsync`) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 调整 `HTTP` 远程请求 `WebSocket` 客户端的事件为异步事件 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 调整 `HTTP` 远程请求分析工具打印二进制内容的大小限制:`1KB` → `0.5KB`,以避免控制台输出刷屏 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 调整 `HTTP` 远程请求 `WithRequest` 方法名为 `With` 4.9.9.52 ⏱️2026.08.01 [2c3d42d](https://gitee.com/dotnetchina/Furion/commit/2c3d42d5783e041222340e2230502b85bfaa9974) - 调整 `HTTP` 远程请求 `URL` 参数格式化程序 `IUrlParameterFormatter` 接口方法 4.9.9.40 ⏱️2026.07.23 [c380d55](https://gitee.com/dotnetchina/Furion/commit/c380d552df469597c33f4e2a6b9b085079fef3c0) - 调整 `HTTP` 远程请求 `URL` 参数排序委托签名 4.9.9.40 ⏱️2026.07.23 [c380d55](https://gitee.com/dotnetchina/Furion/commit/c380d552df469597c33f4e2a6b9b085079fef3c0) - 调整 `HTTP` 远程请求内容转换器方法参数设计 4.9.9.34 ⏱️2026.07.18 [3ae3f64](https://gitee.com/dotnetchina/Furion/commit/3ae3f64e8f42fd91e21707b6847550190a50f040) - 新增 **`HTTP` 远程请求 `QUERY` 请求支持** 4.9.9.8 ⏱️2026.07.01 [1195ffa](https://gitee.com/dotnetchina/Furion/commit/1195ffa77796a1a334dffda01f217036ba08e1a0) - 调整 **`HTTP` 远程请求 `[Query]` 重命名为 `[QueryParam]`** 4.9.9.8 ⏱️2026.07.01 [b9df71b](https://gitee.com/dotnetchina/Furion/commit/b9df71b692bfbefb3ff2897ef08a103150e2bd23) ## 查看变化——[Query] 重命名为 [QueryParam] 昨天,IETF(互联网工程任务组)正式发布了 `RFC 10008`,为 `HTTP` 协议族增添了一个新成员——`QUERY` 方法。这是一种与 `GET` 同样安全、幂等,但支持携带请求内容的 `HTTP` 动词。 https://www.rfc-editor.org/info/rfc10008 框架已第一时间提供适配支持。 **值得注意的是,原有的 `[Query]` 特性已重命名为 `[QueryParam]`,现在的 `[Query]` 则用作 HTTP 方法(请求谓词)。** - - 调整 **`HTTP` 远程请求内容处理器 `IHttpContentProcessor` 接口方法签名** 4.9.8.90 ⏱️2026.06.05 [8c5df99](https://gitee.com/dotnetchina/Furion/commit/8c5df990b842b001ab111f27109d7d7608df0aa9) - - 调整 `HTTP` 远程请求对象的内容转换器工厂(**含接口签名变更**),提升扩展灵活性与代码可维护性 4.9.7.129 ⏱️2025.10.09 [cf83a79](https://gitee.com/dotnetchina/Furion/commit/cf83a790821c0209031c0fc3a491ac9061c32efd) - - 调整 `HTTP` 远程请求扩展功能接口方法签名 4.9.7.95 ⏱️2025.07.10 [4615670](https://gitee.com/dotnetchina/Furion/commit/461567045a1dfced8f4a12b2af028f15b653af21) - - 调整 `HTTP` 远程请求设置 `HTTP` 版本的声明式特性 `[Version]` 名称,调整为 `[HttpVersion]` 4.9.7.41 ⏱️2025.04.14 [b054693](https://gitee.com/dotnetchina/Furion/commit/b05469379da93f81e61cc1fe7216f2300be7d742) - **问题修复** - 修复 `HTTP` 远程请求 `ETag` 缓存未遵守 `Cache-Control: no-store/private` 的问题 4.9.9.82 ⏱️2026.08.18 [1de84bb](https://gitee.com/dotnetchina/Furion/commit/1de84bb5ec90162b83a36fc9e33c9bb395b89365) - 修复 `HTTP` 远程请求 `ETag` 缓存读取响应后导致内容无法重复读取的问题 4.9.9.82 ⏱️2026.08.18 [1de84bb](https://gitee.com/dotnetchina/Furion/commit/1de84bb5ec90162b83a36fc9e33c9bb395b89365) - 修复 `HTTP` 远程请求多线程分块下载数据完整性校验缺失的问题 4.9.9.82 ⏱️2026.08.18 [1de84bb](https://gitee.com/dotnetchina/Furion/commit/1de84bb5ec90162b83a36fc9e33c9bb395b89365) - 修复 `HTTP` 远程请求长轮询通道资源泄漏及背压缺失的问题 4.9.9.82 ⏱️2026.08.18 [1de84bb](https://gitee.com/dotnetchina/Furion/commit/1de84bb5ec90162b83a36fc9e33c9bb395b89365) - 修复 `HTTP` 远程请求超时处理器主动取消时误触发 `OnTimeout` 回调的问题 4.9.9.82 ⏱️2026.08.18 [1de84bb](https://gitee.com/dotnetchina/Furion/commit/1de84bb5ec90162b83a36fc9e33c9bb395b89365) - 修复 `HTTP` 远程请求设置请求标头时不能自动归类的问题,如无法识别哪些是请求内容标头 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - 修复 `HTTP` 远程请求内容类型不能从请求标头中自动推断的问题 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - 修复 `HTTP` 远程请求不支持设置密码为空的 `Basic` 身份认证的问题 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - 修复 `HTTP` 远程请求 `URL` 编码表单处理器不支持字符串内容的问题 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - 修复 `HTTP` 远程请求 `HttpContext` 转发时未排除 `Content-Length` 标头,导致转发异常 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - 修复 `HTTP` 远程请求 `HttpContext` 转发到 `IActionResult` 结果时不能自动解压的问题 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - 修复 `HTTP` 远程请求文件下载器因网络波动导致下载失败(额外新增日志跟踪输出) 4.9.9.66 ⏱️2026.08.08 [152fa8e](https://gitee.com/dotnetchina/Furion/commit/152fa8e40207a53f84980830a7a55685c839201a) - 修复 `HTTP` 远程请求中,配置下载/上传线程数后偶现卡死的问题 4.9.9.65 ⏱️2026.08.07 [632ad1e](https://gitee.com/dotnetchina/Furion/commit/632ad1e13a294d390136f7853a65ce8502900759) - 修复 `HTTP` 远程请求 `HttpContext` 转发网站时出现页面白屏(无输出)的问题 4.9.9.65 ⏱️2026.08.07 [632ad1e](https://gitee.com/dotnetchina/Furion/commit/632ad1e13a294d390136f7853a65ce8502900759) - 修复 `HTTP` 远程请求长轮询及 `SSE` 场景下的内存溢出问题 4.9.9.65 ⏱️2026.08.07 [632ad1e](https://gitee.com/dotnetchina/Furion/commit/632ad1e13a294d390136f7853a65ce8502900759) - 修复 `HTTP` 远程请求压力测试结果计算偏差的问题 4.9.9.65 ⏱️2026.08.07 [632ad1e](https://gitee.com/dotnetchina/Furion/commit/632ad1e13a294d390136f7853a65ce8502900759) - 修复 `HTTP` 远程请求内置 `Furion` 和 `WeChat` 的 `Access Token` 管理器自动刷新配置失效问题 4.9.9.65 ⏱️2026.08.07 [632ad1e](https://gitee.com/dotnetchina/Furion/commit/632ad1e13a294d390136f7853a65ce8502900759) - 修复 `HTTP` 远程请求内置的 `Furion` 框架 `Access Token` 提供器过期后不刷新的问题 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 修复 `HTTP` 远程请求断言上下文在读取响应内容后,导致外部无法再次读取的问题 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 修复 `HTTP` 远程请求多播委托处理错误的问题(同步和异步) 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 修复 `HTTP` 远程请求追加参数时,若配置了 `replace: true` 却无法替换原始 `URL` 地址的问题 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 修复 `HTTP` 远程请求重定向时携带不必要的原始参数,导致请求失败的问题 4.9.9.64 ⏱️2026.08.06 [7e959b2](https://gitee.com/dotnetchina/Furion/commit/7e959b22b46001a827f1e4c2f3aa57508a4a3f13) - 修复 `HTTP` 远程请求多文件同时下载时控制台进度条出现错乱 4.9.9.59 ⏱️2026.08.03 [ecc25f9](https://gitee.com/dotnetchina/Furion/commit/ecc25f946d27c226234798277cca1cd56a5d8ca0) [029e51a](https://gitee.com/dotnetchina/Furion/commit/029e51aa27128bf6a22b66770a4fb1039cb1aa9c) - 修复 `HTTP` 远程请求不支持 `File-Based Apps` 应用类型问题 4.9.9.53 ⏱️2026.08.02 [aae2318](https://gitee.com/dotnetchina/Furion/commit/aae23182e161e84419fa854b0c68e8b722056167) - 修复 `HTTP` 远程请求转发表单时内容类型的 `Boundary` 解析错误问题 4.9.9.50 ⏱️2026.07.31 [8a93868](https://gitee.com/dotnetchina/Furion/commit/8a93868afedaf56f879fcfdd256e90540ebb556d) - 修复 `HTTP` 远程请求 `Digest` 摘要认证错误处理格式和算法问题 4.9.9.47 ⏱️2026.07.30 [312767f](https://gitee.com/dotnetchina/Furion/commit/312767f1a51f4c681fd6f8d6957d0ddaf9bd4885) - 修复 `HTTP` 远程请求分析日志打印未知大小且超过 `5MB` 的响应内容时出现流已释放的异常 4.9.9.46 ⏱️2026.07.29 [22c8edb](https://gitee.com/dotnetchina/Furion/commit/22c8edbe5e249fe10ad011d0dc8982b61278422c) - 修复 `HTTP` 远程请求 `WebSocket` 客户端接收大消息时数据出现截断问题 4.9.9.46 ⏱️2026.07.29 [fc1634d](https://gitee.com/dotnetchina/Furion/commit/fc1634d52182db315f7a2c38c9f1f33f25be100e) - 修复 `HTTP` 远程请求分析日志打印 `SSE` 或流式响应内容出现死锁问题 4.9.9.43 ⏱️2026.07.26 [5fd05a1](https://gitee.com/dotnetchina/Furion/commit/5fd05a1bacf8bf67883b3bb8000164e41cacee32) [09cb4f1](https://gitee.com/dotnetchina/Furion/commit/09cb4f130ccacd7c027cf06e752716013e19697c) - 修复 `HTTP` 远程请求 `SSE` 和长轮询错误处理数据问题 4.9.9.43 ⏱️2026.07.26 [5fd05a1](https://gitee.com/dotnetchina/Furion/commit/5fd05a1bacf8bf67883b3bb8000164e41cacee32) - 修复 `HTTP` 远程请求 `WebSocket` 客户端销毁后没有释放事件资源 4.9.9.34 [3ae3f64](https://gitee.com/dotnetchina/Furion/commit/3ae3f64e8f42fd91e21707b6847550190a50f040) - 修复 `HTTP` 远程请求转发存在 `SSRF` 攻击安全问题 4.9.9.30 [020aee7](https://gitee.com/dotnetchina/Furion/commit/020aee78942c2f1d0630a27e483cf00cf83fbc90) [b30721d](https://gitee.com/dotnetchina/Furion/commit/b30721d9c30bd41376abeaa0dbca18ba59a0fc6b) - 修复 `HTTP` 远程文件上传/下载时,控制台进度打印可能抛出异常的问题 4.9.9.20 [acfd9dd](https://gitee.com/dotnetchina/Furion/commit/acfd9ddc1c3c97f0a25ac526e880e3242303537f) - 修复 `HTTP` 远程请求在极端情况下出现连接池耗尽问题 4.9.9.18 [9b7b430](https://gitee.com/dotnetchina/Furion/commit/9b7b430155b975dbd0e707575e7619c6251eeb98) - 修复 `HTTP` 远程请求在 `Blazor WebAssembly` 应用中使用出现解压异常问题 4.9.9.13 ⏱️2026.07.05 [0224fa0](https://gitee.com/dotnetchina/Furion/commit/0224fa0a5bdf21ee5f1488b0697b4f12daf95375) - 修复 `HTTP` 远程请求分析日志在一些特殊场景下出现空行打印问题 4.9.9.13 ⏱️2026.07.05 [0224fa0](https://gitee.com/dotnetchina/Furion/commit/0224fa0a5bdf21ee5f1488b0697b4f12daf95375) - 修复 `HTTP` 远程请求声明式接口不支持封闭泛型接口定义问题 4.9.9.12 ⏱️2026.07.05 [aa47822](https://gitee.com/dotnetchina/Furion/commit/aa478228719b1ea1c1e3d23559ec089175ed8068) - 修复 `HTTP` 远程请求存在未释放 `HttpResponseMessage` 问题 4.9.9.10 ⏱️2026.07.03 [9a37cfa](https://gitee.com/dotnetchina/Furion/commit/9a37cfa1f89f1decc0b745d3b73302c15e8c35ac) - 修复 `HTTP` 远程请求 `SetJsonContent` 不能应用全局 `HttpClient` 配置问题 4.9.8.90 ⏱️2026.06.05 [8c5df99](https://gitee.com/dotnetchina/Furion/commit/8c5df990b842b001ab111f27109d7d7608df0aa9) - 修复 `HTTP` 远程请求转发 `HttpContext` 可能内存溢出问题 4.9.8.90 ⏱️2026.06.05 [8c5df99](https://gitee.com/dotnetchina/Furion/commit/8c5df990b842b001ab111f27109d7d7608df0aa9) - 修复 `HTTP` 远程请求文件下载器存在线程池问题 4.9.8.90 ⏱️2026.06.05 [8c5df99](https://gitee.com/dotnetchina/Furion/commit/8c5df990b842b001ab111f27109d7d7608df0aa9) - 修复 `HTTP` 远程请求分析工具无法打印被压缩后的内容(如 `gzip`) 4.9.8.64 ⏱️2026.05.13 [18d1922](https://gitee.com/dotnetchina/Furion/commit/18d1922b157d31dbec2a55302e8f173023559c70) - 修复 `HTTP` 远程请求压力测试的内存问题及 `QPS` 计算误差过大问题 4.9.8.57 ⏱️2026.05.01 [ab3b093](https://gitee.com/dotnetchina/Furion/commit/ab3b093ae7569ae8a03853e989ed870e7c7d60cf) - 修复 `HTTP` 远程请求启用请求分析日志在 `Blazor` 应用同步请求中出现死锁问题 4.9.8.46 ⏱️2026.04.19 [bfa8579](https://gitee.com/dotnetchina/Furion/commit/bfa8579b6f90d158aca7ccaa68bd5d7a79a3c5b7) - 修复 `HTTP` 远程请求获取代理接口特性列表时未递归查找子特性 4.9.8.44 ⏱️2026.04.18 [7b0098d](https://gitee.com/dotnetchina/Furion/commit/7b0098d7ef59100a617678966ce46c679eaa2a30) - 修复 `HTTP` 远程请求添加泛型类型的声明式接口出现异常问题 4.9.8.42 ⏱️2026.04.17 [b00f2b9](https://gitee.com/dotnetchina/Furion/commit/b00f2b9a1ebbe8e2005af6e2eb940a3eff48ebc4) - 修复 `HTTP` 远程请求下载文件时若服务器未设置 `Content-Length` 导致下载失败问题 4.9.8.36 ⏱️2026.04.09 [d904e8d](https://gitee.com/dotnetchina/Furion/commit/d904e8de21210ec9b218594685b1da01be9be1f6) - 修复 `HTTP` 远程请求转发 `HttpContext` 时不能转发 `Accept-Language` 问题 4.9.8.31 ⏱️2026.03.31 [#IHTVU9](https://gitee.com/dotnetchina/Furion/issues/IHTVU9) [1f67681](https://gitee.com/dotnetchina/Furion/commit/1f676815435ce65969b5a34ce051542321f07204) - 修复 `HTTP` 远程请求分析工具打印超过 `2GB` 文件出现异常问题 4.9.8.2 ⏱️2026.01.24 [600d02a](https://gitee.com/dotnetchina/Furion/commit/600d02a0b60ce2b0cb594aa3bf2ba1e81be49ed3) - 修复 `HTTP` 远程请求在处理重定向时没有移除路径片段问题 4.9.8.1 ⏱️2026.01.22 [288facb](https://gitee.com/dotnetchina/Furion/commit/288facb79c71a71151ae9ec0bac650872fb1589e) - 修复 `HTTP` 远程请求设置基地址不支持路径参数和配置参数问题 4.9.7.232 ⏱️2025.12.22 [5252bbd](https://gitee.com/dotnetchina/Furion/commit/5252bbd3f5932ccaf336cbcd936880252aef1b67) - 修复 `HTTP` 远程请求分析工具存在重复打印问题 4.9.7.219 ⏱️2025.12.03 [82091b4](https://gitee.com/dotnetchina/Furion/commit/82091b43cd1f9b09eb7f0068630c34800124ccf4) - 修复 `HTTP` 远程请求分析日志打印表单数据不全问题 4.9.7.217 ⏱️2025.12.03 [5bce378](https://gitee.com/dotnetchina/Furion/commit/5bce37839a0eb933e41c3d1bc8679488599badfe) - 修复 `HTTP` 远程请求分析日志不打印 `HttpClient` 默认配置请求头问题 4.9.7.217 ⏱️2025.12.03 [fd0eedc](https://gitee.com/dotnetchina/Furion/commit/fd0eedc372ba42f341c7d0d50a1d4f578ba5056d) - 修复 `HTTP` 远程请求克隆 `HttpRequestMessage` 丢失 `Options` 属性问题 4.9.7.215 ⏱️2025.11.26 [bf38601](https://gitee.com/dotnetchina/Furion/commit/bf38601a2ab8a2da11b26498c2c62269f50842fd) - 修复 `HTTP` 远程请求当上游服务器响应未携带 `Content-Type` 标头时,引发的空引用异常问题 4.9.7.210 ⏱️2025.11.18 [48eae77](https://gitee.com/dotnetchina/Furion/commit/48eae773bbc01bb7da2e277b88c05007eb5e991a) - 修复 `HTTP` 远程请求转发 `HttpContext` 内容时,部分状态码的响应正文丢失的问题 4.9.7.210 ⏱️2025.11.18 [48eae77](https://gitee.com/dotnetchina/Furion/commit/48eae773bbc01bb7da2e277b88c05007eb5e991a) - 修复 `HTTP` 远程请求静态类 `HttpRemoteClient` 多线程死锁问题 4.9.7.137 ⏱️2025.11.07 [c044b87](https://gitee.com/dotnetchina/Furion/commit/c044b87cd1a6243637612872df1dd4a3a8cf4100) - 修复 `HTTP` 远程请求解析响应 `Content-Disposition` 标头文件名出现中文乱码问题 4.9.7.124 ⏱️2025.09.16 [183cb5e](https://gitee.com/dotnetchina/Furion/commit/183cb5e84ac894f9cb5580498cc1709c26a910d4) - 修复 `HTTP` 远程请求进行文件上传下载时控制台进度条不能自适应问题 4.9.7.116 ⏱️2025.09.02 [47250ef](https://gitee.com/dotnetchina/Furion/commit/47250ef3103d72d861df1f073331ec7edf45d6cd) - 修复 `HTTP` 远程声明式请求存在并发线程安全问题 4.9.7.115 ⏱️2025.08.31 [0a5e57f](https://gitee.com/dotnetchina/Furion/commit/0a5e57fae035c2c0a614a67451743f3444ff3178) [#ICVKHB](https://gitee.com/dotnetchina/HttpAgent/issues/ICVKHB) - 修复 `HTTP` 远程请求文件下载解析响应标头时文件名存在前后双引号问题 4.9.7.113 ⏱️2025.08.29 [5e92eab](https://gitee.com/dotnetchina/Furion/commit/5e92eabe4a95cd1af8de90e01e1e9f3eeed5aa5e) - 修复 `HTTP` 远程请求转发 `HttpContext` 丢失 `Content-Type` 问题 4.9.7.109 ⏱️2025.08.14 [9aaf17c](https://gitee.com/dotnetchina/Furion/commit/9aaf17c568f5b470858056849310d4808c103ab7) - 修复 `HTTP` 远程请求转发 `HttpContext` 时出现禁用缓存无效问题 4.9.7.104 ⏱️2025.07.24 [3a386fa](https://gitee.com/dotnetchina/Furion/commit/3a386fa5345c56f6b29903e73a371a2bb3888fdf) - 修复 `HTTP` 远程请求中无法通过表单方式发送 `MultipartFile` 类型属性的问题 4.9.7.93 ⏱️2025.07.05 [30c853d](https://gitee.com/dotnetchina/Furion/commit/30c853d2597feeb81cca6df288021b634d686631) - 修复 `HTTP` 远程请求上传文件时,未配置文件名导致服务端无法正常接收文件的问题(若未指定文件名,默认将文件名设置为 `Unnamed_xxxxxxxxx`) 4.9.7.93 ⏱️2025.07.05 [30c853d](https://gitee.com/dotnetchina/Furion/commit/30c853d2597feeb81cca6df288021b634d686631) - 修复 `HTTP` 远程请求分析工具在打印二进制内容时,若包含退格符可能导致输出不完整的问题 4.9.7.93 ⏱️2025.07.05 [30c853d](https://gitee.com/dotnetchina/Furion/commit/30c853d2597feeb81cca6df288021b634d686631) - 修复 `HTTP` 远程请求中配置超时时间的问题,并明确了超时后抛出的异常类型 4.9.7.90 ⏱️2025.06.25 [679319d](https://gitee.com/dotnetchina/Furion/commit/679319ddbde84af6977114a5823f43fd3c006b96) - 修复 `HTTP` 远程请求转换 `HttpContext` 时不能篡改 `HttpContent(Body)` 问题 4.9.7.89 ⏱️2025.06.20 [ca7bfb5](https://gitee.com/dotnetchina/Furion/commit/ca7bfb5e1a3da863c84e03fbb8ce8e13aee7d101) - 修复 `HTTP` 远程请求分析工具不支持 `Blazor WebAssembly` 应用问题 4.9.7.69 ⏱️2025.05.22 [c257ed0](https://gitee.com/dotnetchina/Furion/commit/c257ed031236666d1e4bcb458c16f02c501d2175) - 修复 `HTTP` 远程请求请求分析工具手动打印出现格式错乱问题 4.9.7.52 ⏱️2025.04.27 [14261e4](https://gitee.com/dotnetchina/Furion/commit/14261e4fb2ff77c88b805ae8ac771bebfef51885) - 修复 **因 `v4.9.7.49` 版本导致 `HTTP` 远程请求反序列化出现内存溢出(`OOM`)问题** 4.9.7.50 ⏱️2025.04.25 [4cf7375](https://gitee.com/dotnetchina/Furion/commit/4cf7375508ac134f95e7e500c9aac7cffa19d115) [406ff44](https://gitee.com/dotnetchina/Furion/commit/406ff4495fb93a0a33827c63802c7d1f85bd4a1d) - 修复 `HTTP` 远程请求当请求的路径末尾包含 `/` 时被自动移除问题 4.9.7.45 ⏱️2025.04.17 [5b18955](https://gitee.com/dotnetchina/Furion/commit/5b18955367d208222a668a5ae1c76702e77e6e3e) - 修复 `HTTP` 远程请求无法通过 `RemoveHeaders` 移除 `User-Agent` 问题 4.9.7.44 ⏱️2025.04.17 [4d98d60](https://gitee.com/dotnetchina/Furion/commit/4d98d6053f7b05c73b6d60d5284c040538f18789) - 修复 `HTTP` 远程请求在强制启用 `IPv4` 时,若请求地址为 `IP` 地址时出现的异常问题 4.9.7.28 ⏱️2025.03.23 [1d57a07](https://gitee.com/dotnetchina/Furion/commit/1d57a0732531a69ea1ed6774add60b24e86f8b7d) - 修复 `HTTP` 远程请求在解析 `URL` 参数若参数值出现多个 `=` 时导致解析失败问题 4.9.7.24 ⏱️2025.03.13 [5c9270f](https://gitee.com/dotnetchina/Furion/commit/5c9270f39721d5b25c08d081f27377f7bdb4bee5) - 修复 `HTTP` 远程请求在未设置查询参数且设置了移除查询参数列表时无效 4.9.7.21 ⏱️2025.03.03 [7b3335e](https://gitee.com/dotnetchina/Furion/commit/7b3335e8af30509aa1f0465a881693bd3b6f114b) - 修复 `HTTP` 远程请求文件上传下载、长轮询和 `Server-Sent Events` 错误处理 `CancellationToken` 问题 4.9.7.16 ⏱️2025.02.28 [21c1f06](https://gitee.com/dotnetchina/Furion/commit/21c1f06cfed1f892eb5ee7bf91989103a3e922d5) - 修复 `HTTP` 远程请求客户端配置的基地址时出现空引用异常 4.9.7.16 ⏱️2025.02.28 [21c1f06](https://gitee.com/dotnetchina/Furion/commit/21c1f06cfed1f892eb5ee7bf91989103a3e922d5) - 修复 `HTTP` 远程请求分析工具未打印实际未成功但确保请求为成功的请求的问题 4.9.7.10 ⏱️2025.02.22 [82b4d81](https://gitee.com/dotnetchina/Furion/commit/82b4d81ae60f1918f06cc28b780902f7096c4fa4) - 修复 `HTTP` 远程请求重定向操作错误的处理请求方法和请求内容问题 4.9.7.2 ⏱️2025.01.26 [c326cf3](https://gitee.com/dotnetchina/Furion/commit/c326cf3c536f29dd29198477990708590b2aeeed) - 修复 `HTTP` 远程请求转发 `HttpContext` 文件出现文件已损坏问题 4.9.7.1 ⏱️2025.01.23 [e90a08c](https://gitee.com/dotnetchina/Furion/commit/e90a08cb76419d6a9130db89774130a8c13e27b4) - 修复 `HTTP` 远程请求遇重定向时可能出现重复拼接查询参数问题 4.9.7 ⏱️2025.01.23 [0e64da5](https://gitee.com/dotnetchina/Furion/commit/0e64da5fe468a0c925bc9bb21985dd119fe9834c) - **其他更改** - 改进 `HTTP` 远程请求分析工具,支持显示 `CURL` 命令字符串 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - 移除 `HTTP` 远程请求文件上传和下载管理器的日志输出 4.9.9.67 ⏱️2026.08.09 [764367a](https://gitee.com/dotnetchina/Furion/commit/764367a3e29e25a8633566c1398392bbbf58881c) - 改进 `HTTP` 远程请求分析工具,对标 `Fiddler/Wireshark` 等外部抓包工具 4.9.9.63 ⏱️2026.08.05 [af1de91](https://gitee.com/dotnetchina/Furion/commit/af1de91d28b7a8a7aa2d38b4c4ecbcc26e464f94) - 改进 `HTTP` 远程请求分析工具和发送表单数据的默认 `boundary` 格式 4.9.9.61 ⏱️2026.08.04 [45d88e1](https://gitee.com/dotnetchina/Furion/commit/45d88e123a3a9cebcd97225324f169b49a3bfabd) - 改进 `HTTP` 远程请求 `ETag` 缓存和重定向处理的性能 4.9.9.60 ⏱️2026.08.04 [1f99ba2](https://gitee.com/dotnetchina/Furion/commit/1f99ba28110e7c4d5575c34a2922fbe70b73fd2a) - 改进 `HTTP` 远程请求压力测试因存在性能问题导致计算 `QPS` 误差较大 4.9.9.56 ⏱️2026.08.02 [635af70](https://gitee.com/dotnetchina/Furion/commit/635af70bd71cf31bc4f6ba87923dfaf2ec2df50c) - 改进 `HTTP` 远程请求 `JSON` 序列化功能 4.9.9.51 ⏱️2026.08.01 [85f6dea](https://gitee.com/dotnetchina/Furion/commit/85f6dea02f34dac9f257ac831cbc05d09daca7b4) [#IK5MUW](https://gitee.com/dotnetchina/Furion/issues/IK5MUW) - 改进 `HTTP` 远程请求发送原始字符串内容 4.9.9.46 ⏱️2026.07.29 [22c8edb](https://gitee.com/dotnetchina/Furion/commit/22c8edbe5e249fe10ad011d0dc8982b61278422c) - 改进 `HTTP` 远程请求路径参数模板语法 4.9.9.41 ⏱️2026.07.24 [40b7d08](https://gitee.com/dotnetchina/Furion/commit/40b7d08bd5b97de971e85fdab31e221a4f38ccb0) - 改进 `HTTP` 远程请求声明式数据验证功能 4.9.9.35 ⏱️2026.07.19 [3e91e0c](https://gitee.com/dotnetchina/Furion/commit/3e91e0c22ac9d5966fcea563f3fb4f4bb4c8ba3d) - 改进 `HTTP` 远程请求底层发送请求核心逻辑 4.9.9.34 ⏱️2026.07.18 [3ae3f64](https://gitee.com/dotnetchina/Furion/commit/3ae3f64e8f42fd91e21707b6847550190a50f040) - 改进 `HTTP` 远程请求默认 `Content-Type` 推断逻辑 4.9.9.15 ⏱️2026.07.06 [5495a09](https://gitee.com/dotnetchina/Furion/commit/5495a096f2f9dd98090ea0561a36afd8be8ebd38) - 改进 `HTTP` 远程请求分析日志,减少内存占用 4.9.8.98 ⏱️2026.06.17 [dc8f573](https://gitee.com/dotnetchina/Furion/commit/dc8f5738d79be3ca69b3fd5fada3768655d99742) - 改进 `HTTP` 远程请求分析日志,合并日志输出 4.9.8.77 ⏱️2026.05.20 [fb8428c](https://gitee.com/dotnetchina/Furion/commit/fb8428cf8e03e4cd341bd69a4fae0767ea3c2cdb) - 改进 `HTTP` 远程请求文件下载传输进度的通知频率 4.9.8.37 ⏱️2026.04.11 [49223d6](https://gitee.com/dotnetchina/Furion/commit/49223d6d0569b7b5136df9db9f4e7b95c38d9b94) - 改进 `HTTP` 远程请求超时时间,支持设置为 `null` 4.9.8.22 ⏱️2026.03.09 [537400c](https://gitee.com/dotnetchina/Furion/commit/537400c15879548cad16ffda301fc1962a93e88b) - 改进 `HTTP` 远程请求构建器的 `.SetOnPreSendRequest` 方法,支持多次调用 4.9.7.244 ⏱️2026.01.09 [e42e6b0](https://gitee.com/dotnetchina/Furion/commit/e42e6b0a58c72c526120c125189d3aab163f58e9) - 改进 简化 `HTTP` 远程请求静态类 HttpRemoteClient 自定义配置 4.9.7.221 ⏱️2025.12.06 [ca3d6f6](https://gitee.com/dotnetchina/Furion/commit/ca3d6f6a5546fc9d14a135a8fb0986ed8092c024) - 改进 `HTTP` 远程请求发送文本内容不支持设置 `Content-Type` 问题 4.9.7.218 ⏱️2025.12.03 [9d6cdd1](https://gitee.com/dotnetchina/Furion/commit/9d6cdd124b44886f68b6cccab119176640c18492) - 改进 `HTTP` 远程请求日志系统,方便生产环境准确定位错误 4.9.7.212 ⏱️2025.11.26 [c40570b](https://gitee.com/dotnetchina/Furion/commit/c40570b72b604d947dca14f6c2787385466fcd39) - 改进 `HTTP` 远程请求 `WebSocket` 客户端构造函数选项参数 4.9.7.130 ⏱️2025.10.15 [ca85e8e](https://gitee.com/dotnetchina/Furion/commit/ca85e8e846f6017eaac72b1b7c51dfab8181eefd) - 改进 `HTTP` 远程请求文件下载功能,新增 `FileTransferResult` 返回值 4.9.7.128 ⏱️2025.09.30 [9311ee3](https://gitee.com/dotnetchina/Furion/commit/9311ee357e134cc7ba89369b45cb3ef13929d823) [04010e2](https://gitee.com/dotnetchina/Furion/commit/04010e282cc8b58becd4e7c47ae6d516b304d455) - 改进 `HTTP` 远程请求文件上传和下载控制台进度条时间格式 4.9.7.117 ⏱️2025.09.02 [665a453](https://gitee.com/dotnetchina/Furion/commit/665a453227aecac21492f1a32435f8448550d2c2) - 改进 `HTTP` 远程请求文件上传和下载打印到控制台进度条效果 4.9.7.114 ⏱️2025.08.29 [3204e72](https://gitee.com/dotnetchina/Furion/commit/3204e721c10ea60a523af39ca0578dd040d372ff) - 改进 `HTTP` 远程请求设置多部分表单方法(重载) 4.9.7.99 ⏱️2025.07.19 [60b9260](https://gitee.com/dotnetchina/Furion/commit/60b92609029f0afd6707582429d617e7caae9dc8) - 改进 `HTTP` 远程请求分析工具自动处理 `Unicode` 转义 4.9.7.48 ⏱️2025.04.23 [f0a01d6](https://gitee.com/dotnetchina/Furion/commit/f0a01d6b4524670bf3e38f93338ebee19a60ddd3) - 改进 `HTTP` 远程请求分析工具,支持打印请求和响应内容的大小 4.9.7.47 ⏱️2025.04.20 [cf7956e](https://gitee.com/dotnetchina/Furion/commit/cf7956e227d978a05c6e0d294766aa45a681f1b9) - 调整 `HTTP` 远程请求默认的 `User-Agent` 为 `Edge` 浏览器(版本 `133`)的 `User-Agent` 一致 4.9.7.18 ⏱️2025.03.01 [b6ba52b](https://gitee.com/dotnetchina/Furion/commit/b6ba52bea7f40098a101811c5eb403456139de3c) - 调整 **`HTTP` 远程请求长轮询属性(事件)类型,由 `Func?` -> `Func`** 4.9.7.17 ⏱️2025.02.28 [050e64f](https://gitee.com/dotnetchina/Furion/commit/050e64f0a27c782f360ff3a78ef27f841c6260e6) - 调整 **`HTTP` 远程请求 `ServerSentEvents` 的 `onMessage` 属性类型,由 `Func?` -> `Func`** 4.9.7.14 ⏱️2025.02.26 [5ef4b13](https://gitee.com/dotnetchina/Furion/commit/5ef4b13c522a824822266dbcf6ad91d8f65e701a) - 调整 **`HTTP` 远程请求自动设置 `Host` 请求标头为 `false`,即默认不启用** 4.9.6.20 ⏱️2024.12.27 [4998e13](https://gitee.com/dotnetchina/Furion/commit/4998e139dec691a154bfbd52463c5fd5e33f6141) - 改进 `HTTP` 远程请求默认启用自动设置请求 `Host` 标头 4.9.6.16 ⏱️2024.12.17 [61afe9a](https://gitee.com/dotnetchina/Furion/commit/61afe9a28cad036ac51b3a457f865cad36711837) - 改进 `HTTP` 远程请求提交表单数据时默认设置 `Boundary` 4.9.6.16 ⏱️2024.12.17 [61afe9a](https://gitee.com/dotnetchina/Furion/commit/61afe9a28cad036ac51b3a457f865cad36711837) - 改进 `HTTP` 远程请求 `RateLimitedStream` 带应用速率限制的流,基于令牌桶算法 4.9.6.10 ⏱️2024.12.03 [f0ee8af](https://gitee.com/dotnetchina/Furion/commit/f0ee8af32aed3e94d778b04be626bcdb069ec46f) - 改进 `HTTP` 远程请求分析工具性能,打印内容时默认只输出 `5KB` 内容 4.9.6.9 ⏱️2024.12.02 [88afe64](https://gitee.com/dotnetchina/Furion/commit/88afe64ec45921f1a53fe98edba9b12b7b5f2a9c) - 改进 `HTTP` 远程请求分析工具,提供请求内容和响应内容打印 4.9.6.7 ⏱️2024.12.02 [250ea66](https://gitee.com/dotnetchina/Furion/commit/250ea66c6c9fff98480c79a26e4a5ef629b99153) - 改进 `HTTP` 远程请求分析工具,提供更多细节打印 4.9.6.4 ⏱️2024.11.29 [6782110](https://gitee.com/dotnetchina/Furion/commit/6782110d073a6193c431023b8c40c7ad4fb1129e) --- # HttpAgent 正式发布:内置工业级 HTTP 流量分析引擎 > 原文链接:https://http.furion.net/blog/hello-httpagent/ 我们很高兴宣布,**HttpAgent** 正式发布。这是一个面向 `.NET 8+` 的高性能、灵活、易用的 `HTTP` 开源库,为文件传输、轮询、测试工具、实时通信、请求管理、媒体类型处理、`MessagePack`、声明式请求等场景提供全面支持。 ## 为什么需要 HttpAgent 对接第三方 `API` 时,开发者最头疼的问题是什么?**看不见请求到底发出去什么、响应到底返回了什么**。传统做法是打开 Fiddler 或 Wireshark 抓包,配置代理、装证书、过滤流量——一套流程下来,调试五分钟,配置半小时。 HttpAgent 内置了工业级 `HTTP` 流量分析引擎(`Profiler`),直接在控制台呈现完整请求/响应报文: ```cs showLineNumbers {1,3} await httpRemoteService.GetAsync("https://furion.net/", builder => builder.Profiler()); // 一行代码开启请求分析 ``` 控制台将输出请求头、响应头、状态码、耗时等全部细节,无需任何外部工具。 > **生产环境注意** `Profiler` 默认展示最多 `5KB` 的内容,并且建议在生产环境中禁用,以避免性能开销。 ## 核心能力一览 - **全谓词支持**:`GET`、`POST`、`PUT`、`DELETE`、`PATCH`、`HEAD`、`TRACE`、`OPTIONS`、`QUERY`; - **文件传输**:上传/下载、实时进度、多线程分片下载; - **实时通信**:`SSE`、`WebSocket`、标准/长轮询; - **韧性治理**:重试(指数退避)、超时控制、异常抑制,可扩展熔断降级; - **Token 自动管理**:自动刷新、自动注入、`401` 自动重试; - **配额与缓存**:日/周/月/永久配额窗口、`ETag` 响应缓存; - **声明式请求**:接口 + 特性注解,支持继承与面向对象设计; - **cURL 导入**:从 cURL 命令字符串直接发起请求; - **压力测试**:内置压测工具,自动生成测试报告。 ## 一分钟上手 ```bash dotnet add package HttpAgent ``` ```cs showLineNumbers {1,6} builder.Services.AddHttpRemote(); // 注册服务 public class YourService(IHttpRemoteService httpRemoteService) { public async Task GetContent() => await httpRemoteService.GetAsStringAsync("https://furion.net/"); } ``` 更完整的渐进式学习路径,请从 [HttpAgent 简介](/docs/getting-started/intro/) 开始阅读。 ## 致谢 项目在 [Gitee](https://gitee.com/dotnetchina/HttpAgent) 公开托管,欢迎 Star、Issue 与 PR。后续我们将持续更新实战教程与最佳实践,敬请关注博客动态。 --- # 别再折磨 HttpClient 了!全新 HttpAgent 横空出世,重新定义 .NET 远程请求! > 原文链接:https://http.furion.net/blog/stop-suffering-with-httpclient/ > 让 HTTP 请求所见即所得。 每个 .NET 开发者,都有一段和 HttpClient 相爱相杀的记忆: - 每发一个请求,都要重写一遍:`new HttpClient()`、拼接 URL、拼 QueryString、塞 Headers、判 Content-Type; - 一个不留神,把 `HttpClient` 随手 `new` 在了 `using` 里,生产环境 Socket 耗尽,压测一跑就崩; - 超时、重试、退避、熔断?对不起,要么自己手搓,要么再引一个 Polly 全家桶; - 同事甩来一段 curl 让你"复现一下",你只能逐行翻译成 C#,翻译错了还查半天; - 想知道请求到底发出去什么、响应是什么,只能开 Fiddler 抓包,抓半天找不到自己那条; - 文件上传下载、进度条、SSE、长轮询、WebSocket、SOAP、OData、Token 自动刷新……每一样都要另起炉灶。 写到最后你会发现:**真正难的从来不是"发出一个请求",而是把请求这件事做对、做全、做得可维护。** 今天的主角,就是来终结这些折磨的 —— **HttpAgent**。 ## 一句话认识 HttpAgent HttpAgent 是一个高性能、灵活、易用的 .NET HTTP 开源库,核心理念只有一句:**让 HTTP 请求所见即所得。** 它有几组让人无法拒绝的硬指标: - **零第三方依赖**:一个 NuGet 包开箱即用,不需要 Polly、不需要 Flurl,什么都不需要; - **全平台覆盖**:Console / Web / WASM / WinForms / WPF / MAUI,.NET 8+ 通吃; - **工业级质量**:98% 测试覆盖率,每一次发版都经过严格的回归验证; - **功能全家桶**:重试、超时、熔断、配额、ETag 缓存、Access Token 自动管理……全部内置,装上即用。 ## 五种姿势,任你选择 HttpAgent 最"上头"的设计,是把"发请求"这件事拆成了**五种写法**。你喜欢哪种就用哪种,一个项目里混着用也毫无违和。 ### 姿势一:谓词方式,一行直达 ```csharp // 一行代码获取网站内容 var content = await httpRemoteService.GetAsStringAsync("https://api.example.com/"); // 泛型重载,直接拿强类型结果 var user = await httpRemoteService.GetAsAsync("https://api.example.com/user/1"); ``` ### 姿势二:构建器方式,链式拼装 ```csharp var result = await httpRemoteService.SendAsync( HttpRequestBuilder.Post("https://api.example.com/login") .SetJsonContent(new { account = "admin", password = "123456" }) .SetTimeout(TimeSpan.FromSeconds(30)) .Profiler()); // 顺手开启请求分析工具 ``` ### 姿势三:声明式请求,接口即 API ```csharp public interface IUserApi : IHttpDeclarative { [Get("https://api.example.com/user/{id}")] Task GetUserAsync(int id); [Post("https://api.example.com/user"), Profiler] Task CreateUserAsync([Body] User user); } ``` 注册之后直接注入使用,**调用远程接口就像调用本地方法**。路径模板、查询参数、请求体、表单、多部分上传,全部用特性声明,代码干净得不像话。 ```csharp services.AddHttpRemote(builder => { builder.AddHttpDeclarative(); }); ``` ### 姿势四:从 cURL 发送,粘贴即用 你一定经历过这种场景:第三方平台的接口文档里只有一段 curl,客服甩给你、测试甩给你,甚至老板都甩给你:"就按这个来"。 以前,你得把这段 curl 逐字翻译成 C#。现在: ```csharp var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromCurl(""" curl -k -X POST 'https://api.example.com/order?query1=10&query2=hello' \ -H 'Content-Type: application/json' \ -d '{ "id": 1, "name": "sample" }' """)); ``` HttpAgent 内置了 cURL 解析引擎,支持 `-X`、`-H`、`-d`、`-F`、`-u`、`--data-urlencode`、`--max-time`、`--http2` 等常用选项,文件上传、URL 编码表单、Basic 认证统统不在话下,还支持自定义扩展标志。 **cURL 原文粘贴进去,剩下的交给引擎。** 这不仅对人友好,对 AI 也极其友好 —— AI 生成的 curl 命令,不再需要人工翻译成代码。 ### 姿势五:从 JSON 发送,配置即请求 如果觉得链式调用还是啰嗦,那就把请求写成 JSON: ```csharp var result = await httpRemoteService.SendAsStringAsync( HttpRequestBuilder.FromJson(""" { "url": "https://api.example.com/order", "method": "POST", "queries": { "page": 1, "size": 10 }, "headers": { "Content-Type": "application/json" }, "auth": { "type": "bearer", "token": "xxx" }, "timeout": 5000, "data": { "id": 1, "name": "sample" } } """)); ``` 地址、基地址、查询参数、Header、Cookie、超时、认证(Bearer / Basic / Digest)、请求体、多部分上传、是否开启分析工具……全部 JSON 化。**请求配置可以存进数据库、配置文件,甚至由 AI 动态生成。** ## 一个包,装下整个 HTTP 工程化 很多"轻量"的 HTTP 库,轻到最后什么都得自己补。HttpAgent 的思路正相反:**一个包,把 HTTP 工程化需要的一切都内置了,而且没有任何第三方依赖。** - **弹性与容错**:重试、指数退避、超时、熔断降级,一行 `SetRetry` 就能拉起一整套韧性策略; - **Access Token 自动管理**:自动获取、过期自动刷新、`401` 自动重试,Token 注入 Header / Query / Cookie 任你配置,对接微信、企业微信等第三方平台时,再也不用手写 Token 定时器; - **配额与 ETag 缓存**:日 / 周 / 月 / 终身调用额度窗口,`304` 缓存复用,省流量又防超限; - **实时通信**:SSE、长轮询、WebSocket、`IAsyncEnumerable` 流式响应,一应俱全; - **文件传输**:上传下载、多线程分块下载、进度回调、控制台进度条,RFC 2047 / RFC 5987 等文件名编码自动解析; - **网关与代理**:`HttpContext` 一行转发,自带代理网关能力; - **测试与调试**:请求 / 响应断言、Mock 模拟测试、压力测试,内置即用; - **数据格式全覆盖**:JSON、JSON Lines、MessagePack、XML、HTML、SOAP(WebService)、OData 全支持; - **自动解压**:gzip / deflate / brotli / zstd 响应自动解压,无需关心压缩细节。 随便举一个例子,一行开启"带指数退避的智能重试": ```csharp HttpRequestBuilder.Get("https://api.example.com/order/1") .SetRetry(options => options .SetMaxRetries(3) .SetUseExponentialBackoff(true) .AddRetryStatusCodes(408, 429, 500, 502, 503, 504)); ``` ## 内置工业级流量分析引擎:黑盒变白盒 调试第三方接口最痛苦的是什么?**看不见。** 请求到底发了什么 Header?Body 编码对不对?为什么响应是空? HttpAgent 内置工业级 HTTP 流量分析引擎(Profiler),在任意请求后追加一个 `.Profiler()`,终端立即输出完整、带颜色高亮的请求与响应报文: ```csharp await httpRemoteService.GetAsync("https://api.example.com/order/1", builder => builder.Profiler()); ``` 请求方法、完整 URL、所有请求头、请求体、响应状态码、响应头、响应体、耗时……一眼看穿,所见即所得。**以前抓包半小时,现在一行代码。** ## 对 AI 最友好的 .NET HTTP 库 2026 年,AI 编程已经变成日常。而 HttpAgent,可能是目前对 AI 最友好的 .NET HTTP 库: 1. **llms.txt 原生支持**:官方文档站内置 `llms.txt` 与 `llms-full.txt` 两份 AI 友好文件(中英文皆有),Claude Code、Codex 等 AI 助手可以一次性读取全部文档,不用逐页抓取网页。把 `llms-full.txt` 放进项目的 `CLAUDE.md` / `AGENTS.md`,AI 写出的 HttpAgent 代码几乎不会错。 2. **声明式接口是 AI 的天然游乐场**:给 AI 一个接口定义,它就能稳定生成带特性注解的完整 API 客户端,可读性极强,代码审查成本极低。 3. **cURL / JSON 即代码**:AI 输出的 curl 命令、JSON 配置,粘贴进来直接就是可运行的请求,翻译成本为零。 ## 三步上手 ```bash # 1. 安装(任意 .NET 8+ 应用) dotnet add package HttpAgent # Web 应用额外提供 HttpContext 转发能力 dotnet add package HttpAgent.AspNetCore ``` ```csharp // 2. 注册服务 builder.Services.AddHttpRemote(); ``` ```csharp // 3. 开干 var user = await httpRemoteService.GetAsAsync("https://api.example.com/user/1"); ``` 从安装到发出第一个请求,不超过三分钟。 ## 写在最后 如果你也曾在深夜为一段 curl 抓耳挠腮,为 HttpClient 的 Socket 耗尽问题反复排查,为一个"小小的请求功能"堆了三千行封装代码 —— 不妨给 HttpAgent 一个机会,也给自己一个机会。 - **开源仓库(Gitee)**:https://gitee.com/dotnetchina/HttpAgent - **NuGet**:https://www.nuget.org/packages/HttpAgent 让 HTTP 请求,所见即所得。 --- # 别再翻文档了!HttpAgent 上线「工坊 + AI 助手」双引擎,写 HTTP 代码进入所见即所得时代 > 原文链接:https://http.furion.net/blog/workshop-ai-assistant/ > 让 HTTP 请求所见即所得。 每个 .NET 开发者的浏览器里,大概都常驻着这几个标签页: - HTTP 客户端库的文档,来回翻 API 名; - Fiddler 抓包窗口,试图看清请求到底发出去什么; - 搜索引擎,「xxx 库怎么上传文件」「xxx 库怎么设置超时」……一查就是一下午。 先别急着怪自己。**问题从来不在你,而在于:库给了你,但「写代码」和「排错」这两件最费时间的事,一直没人帮你解决。** HttpClient 没解决,Refit 没解决,RestSharp 也没解决——它们都把火力集中在了「怎么发请求」上,而把「怎么把请求写好、写对、写明白」留给了你自己。 今天,HttpAgent 带来了两个「新物种」:**【工坊】** 和 **【HttpAgent 助手】**。 ## 痛点一:示例代码是死的,参数永远对不上 文档里的示例永远是 `GetAsStringAsync("https://example.com")`,而你要写的是带 8 个查询参数、3 个自定义标头、超时 5 秒、失败重试 2 次、还要开 Profiler 的真实请求。 于是你开始抄模板 → 改参数 → 少个逗号 → 编译报错 → 再改。五分钟能写完的请求,折腾半小时。 ## 解法一:【工坊】——可视化代码生成器,所见即所得 打开 [工坊](/workshop/),你面对的不是冷冰冰的文档,而是一张在线表单: - 选方法(GET / POST / PUT / DELETE……9 种动词); - 填地址、查询参数、请求标头、正文与内容类型; - 勾选返回类型、超时重试、冻结参数、Profiler 开关、摘要认证…… 配置完成的瞬间,**五种代码同步生成**,随你复制: 1. **builder 构建器**——流畅式写法; 2. **请求谓词**——一行式语法糖; 3. **cURL 命令**——反向导出,发给同事直接复现; 4. **声明式接口**——接口 + 特性,团队协作首选; 5. **JSON 配置**——FromJson 一把梭。 新手不用再记任何 API 名,老手省掉大量样板时间。**这是 Refit、RestSharp 乃至原生 HttpClient 都从未提供过的东西。** ## 痛点二:文档再全,也扛不住「我就要这一句」 再好的文档也是「人找答案」:你要先知道答案在「2.13 Server-Sent Events」还是「5.8 设置超时时间」,然后翻进去,再在一千字的正文里定位那三行代码。 深夜改 bug 时,这体验堪称酷刑。 ## 解法二:【HttpAgent 助手】——基于官方文档的 AI 问答 HttpAgent 站点右下角,现在住着一个**答案 100% 来自官方文档**的 AI 助手: - 问「sse 怎么使用」→ 直接给出对应章节的用法与代码示例,末尾附引用文档链接; - 问「如何联系作者,我想赞助作者」→ 给出邮箱、微信与支持页地址; - 问「HttpAgent 和 Refit 有什么区别」→ 甩出完整的对比结论; - 问「你是谁」→ 它比谁都清楚。 它不联网瞎编——检索源就是全站文档全文(llms-full.txt,含所有代码示例),还能开启「全文增强」模式把整份文档喂给模型。**自带 DeepSeek 或 OpenAI API Key 即可使用,Key 只保存在你本地浏览器、直连所选服务商,不经过任何服务器。** ## 为什么别人做不到? 不是技术有多难,而是定位不同: - Refit 把力气花在「接口即 API」上,生态里没有官方可视化生成器,也没有文档 AI; - RestSharp 是经典老牌客户端,稳,但「写代码」和「排错」两件事依旧要自己来; - 原生 HttpClient 更不用说——它连「库」都算不上,只是基座。 而 HttpAgent 的思路是:**库负责「把请求做对」,工坊负责「把代码写好」,助手负责「把问题答明白」**。三件套凑齐,从「会用」到「用好」的路,我们替你铺平了。 ## 现在就去试试 - 🛠️ [工坊](/workshop/)——配置即代码,所见即所得; - 🤖 右下角【HttpAgent 助手】——官方文档的活字典; - 📚 [文档](/docs/)——560+ 篇中英双语文档; - ⭐ 觉得有用,去 [Gitee](https://gitee.com/dotnetchina/HttpAgent) 点个 Star,让更多人告别翻文档的日子。 > 让 HTTP 请求所见即所得——这一次,从「写代码」开始。 --- # 支持项目与赞助(联系作者,含邮箱与微信) > 原文链接:https://http.furion.net/support/ 如果你想联系作者或赞助 HttpAgent 项目,可通过以下方式: - 邮箱:monksoul@outlook.com(发邮件说明赞助意向,我们会尽快回复) - 微信号:ibaiqian(添加时请备注「HttpAgent 赞助」) - 访问 https://http.furion.net/support/ 查看赞助方式与感谢名单 - 在 Gitee 给项目点 Star 也是支持:https://gitee.com/dotnetchina/HttpAgent