Skip to main content

19.Ⅰ 基础使用

📝 模块更新日志
  • 新特性
    •   HTTP 远程请求 UriBuilder 配置操作 4.9.8.45 ⏱️2026.04.19 56be6c6
    •   HTTP 声明式请求支持面向对象继承 4.9.8.42 ⏱️2026.04.17 b00f2b9
    •   HTTP 远程请求支持设置永不超时 4.9.8.21 ⏱️2026.03.09 92e0283
    •   HTTP 远程请求支持发送不进行 URL 编码的表单数据 4.9.8.15 ⏱️2026.02.09 f0104ef
    •   HTTP 远程请求声明式请求支持 Action<HttpRequestMessage> 冻结参数 4.9.7.244 ⏱️2026.01.09 d9fce11
    •   HTTP 远程请求支持 HttpRequestBuilder 统一配置器 IHttpRequestBuilderConfigurer 4.9.7.244 ⏱️2026.01.09 d9fce11
    •   HTTP 远程请求支持提供从互联网 URL 地址下载文件流配置 HttpClientHttpRequestMessage 实例 4.9.7.235 ⏱️2025.12.27 a723ae5
    •   HTTP 远程请求设置请求标头和 Cookie 支持配置参数 4.9.7.231 ⏱️2025.12.19 541fadd
    •   HTTP 远程请求支持指定网卡 IP 地址请求 4.9.7.230 ⏱️2025.12.19 904705d
    •   HTTP 远程请求 HttpBuilder 静态类,用于简化 HttpRequestBuilder 名称过长问题 4.9.7.222 ⏱️2025.12.08 c0b6c77
    •   HTTP 远程请求分析日志支持颜色高亮 4.9.7.217 ⏱️2025.12.03 29b9348
    •   HTTP 远程请求支持设置 JSON 响应反序列化包装器 4.9.7.214 ⏱️2025.11.26 f046b4d ebe71f9
    •   HTTP 远程请求支持在未配置日志服务时设置日志回退输出委托 4.9.7.213 ⏱️2025.11.26 17ac155
    •   HTTP 远程请求在设置 JSON 数据时支持传入 JsonSerializerOptions 对象 4.9.7.208 ⏱️2025.11.16 c97b467
    •   HTTP 远程请求支持自动修复无效的响应字符编码 4.9.7.202 ⏱️2025.11.13 35530e8
    •   HTTP 远程请求支持表单名称命名策略或自定义转换器 4.9.7.137 ⏱️2025.11.07 6c175a8
    •   HTTP 远程请求断言功能 4.9.7.137 ⏱️2025.11.07 c044b87
    •   HTTP 远程请求默认启用响应内容 gzipdeflatebrotli 自动解压 4.9.7.137 ⏱️2025.11.07 e9b10ac
    •   HTTP 远程请求请求分析工具 Profiler(enabled) 别名方法:Debugger([enabled]) 4.9.7.137 ⏱️2025.11.07 c044b87
    •   HTTP 远程请求支持添加动态 URL 参数(请求时求值)4.9.7.131 ⏱️2025.10.17 a162c8d
    •   HTTP 远程请求压力测试支持便捷禁用 HTTP 缓存 4.9.7.131 ⏱️2025.10.17 a162c8d
    •   HTTP 远程请求支持配置多线程下载文件 4.9.7.123 ⏱️2025.09.16 10bddc9
    •   HTTP 远程请求支持将 XML 字符串转换为类型对象 4.9.7.123 ⏱️2025.09.16 41746d2
    •   HTTP 远程请求构建器实例支持 When 条件构建 4.9.7.100 ⏱️2025.07.22 651b4d5
    •   HTTP 远程请求扩展功能构建器 WithRequest(Action<HttpRequestBuilder>) 方法 4.9.7.95 ⏱️2025.07.10 4615670
    •   HTTP 远程请求声明式 [MultipartObject] 特性 4.9.7.94 ⏱️2025.07.09 7e52e9c
    •   HTTP 远程请求支持 Unix epoch 日期格式 4.9.7.77 ⏱️2025.05.31 ca9c94e
    •   HTTP 远程请求 URL 参数格式化程序 4.9.7.70 ⏱️2025.05.23 e8b24b3
    •   HTTP 远程请求支持配置 SocketsHttpHandler 忽略 SSL 证书验证 4.9.7.63 ⏱️2025.05.16 042da35
    •   HTTP 远程请求支持配置请求超时发生时的回调操作 4.9.7.62 ⏱️2025.05.15 23a580d
    •   HTTP 远程请求 HttpRemoteClient 静态类 4.9.7.58 ⏱️2025.05.02 86e9dbe
    •   HTTP 远程请求 HttpRemoteResult<TResult> 解构函数(析构表达式)功能支持 4.9.7.53 ⏱️2025.04.28 e4dcc10
    •   HTTP 远程请求 IHttpClientBuilder.ConfigureOptions(configure) 扩展方法 4.9.7.51 ⏱️2025.04.26 33479e2
    •   HTTP 远程请求请求分析工具打印 HttpClient Name4.9.7.51 ⏱️2025.04.26 33479e2
    •   HTTP 远程请求 WithSuccessStatusCodeHandler 方法支持设置请求成功状态码回调操作 4.9.7.47 ⏱️2025.04.20 cf7956e
    •   HTTP 远程请求状态码处理程序支持 ~ 符号设置区间,如 200~299 4.9.7.47 ⏱️2025.04.20 cf7956e
    •   HTTP 远程请求 SetOmitContentType(omit) 方法支持移除或保留请求内容的 Content-Type 4.9.7.44 ⏱️2025.04.17 4d98d60
    •   HTTP 远程请求支持从 JSON 字符串创建 HttpRequestBuilder 实例 4.9.7.41 ⏱️2025.04.14 580dd04
    •   HTTP 远程请求支持使用 SuppressExceptions()[SuppressExceptions] 抑制请求异常 4.9.7.40 ⏱️2025.04.12 1a9bc7b
    •   HTTP 远程请求支持设置单次请求的 HTTP 版本 4.9.7.40 ⏱️2025.04.12 1a9bc7b
    •   HTTP 远程请求请求分析工具打印 HTTP Version4.9.7.40 ⏱️2025.04.12 1a9bc7b
    •   HTTP 远程请求 HttpRemoteResult<TResult> 类型 Version 属性(HTTP 版本) 4.9.7.40 ⏱️2025.04.12 1a9bc7b
    •   HTTP 远程请求支持设置请求来源地址 4.9.7.36 ⏱️2025.04.02 5d4a241
    •   HTTP 远程请求 HttpRequestBuilder.AddAuthentication(string, string?) 重载方法 4.9.7.33 ⏱️2025.03.25 f8a648a
    •   HTTP 远程请求多部分表单 AddFile(IFormFile)AddFiles(IEnumerable<IFormFile>) 扩展方法 4.9.7.31 ⏱️2025.03.24 6eb54e0
    •   HTTP 远程请求反序列化时支持 NumberBoolean 类型转 String 类型 4.9.7.29 ⏱️2025.03.23 489aa55
    •   HTTP 远程请求序列化时自动处理中文乱码问题 4.9.7.29 ⏱️2025.03.23 489aa55
    •   HTTP 远程请求进行 JSON 反序列化时支持非 ISO 8601-1:2019 标准的时间字符串 4.9.7.25 ⏱️2025.03.14 10de94b 3f3d619
    •   HTTP 远程请求支持为所有 HttpClient 客户端添加配置 IHttpRemoteBuilder.ConfigureHttpClientDefaults(configure) 4.9.7.22 ⏱️2025.03.04 cef4ca0
    •   HTTP 远程请求支持 WithPathSegment[s] 设置路径片段 4.9.7.21 ⏱️2025.03.03 7b3335e
    •   HTTP 远程请求支持为所有 HttpClient 客户端启用请求分析工具 IHttpRemoteBuilder.AddProfilerDelegatingHandler() 4.9.7.18 ⏱️2025.03.01 b6ba52b
    •   HTTP 远程请求支持 WebService(SOAP) 支持 4.9.7.15 ⏱️2025.02.27 479073a
    •   HTTP 远程请求 AddProfilerDelegatingHandler(this IHttpClientBuilder builder, bool disableInProduction) 重载方法 4.9.7.13 ⏱️2025.02.26 5ef4b13
    •   HTTP 远程请求 Server-Sent Events 支持任意 HttpMethod 4.9.7.13 ⏱️2025.02.26 caa2aca
    •   HTTP 远程请求获取响应标头 Set-Cookie 扩展方法 4.9.7.11 ⏱️2025.02.24 62737cf
    •   HTTP 远程请求支持设置请求分析工具触发委托 4.9.7.10 ⏱️2025.02.22 82b4d81
    •   HTTP 远程请求 ConfigureOptions 支持解析服务的重载方法 4.9.7.9 ⏱️2025.02.20 dabbc47
    •   HTTP 远程请求 HttpRemoteOptions 选项 FallbackBaseAddress 属性,支持回退请求基地址设置 4.9.7.9 ⏱️2025.02.20 dabbc47
    •   HTTP 远程请求 HttpRemoteResult 类型 Server 属性 4.9.7.9 ⏱️2025.02.20 5b1c181
    •   HTTP 远程请求 HttpRequestMessage 克隆扩展方法 4.9.7.8 ⏱️2025.02.18 abd61c8
    •   HTTP 远程请求 [Forward] 转发特性支持 4.9.7 ⏱️2025.01.23 023166b
    •   HTTP 远程请求配置参数支持 4.9.7 ⏱️2025.01.23 023166b
    •   HTTP 远程请求转发支持忽略请求或响应标头 4.9.7 ⏱️2025.01.23 023166b
    •   HTTP 远程请求重定向支持相对路径 4.9.6.21 ⏱️2024.12.28 17df0c4
    •   HTTP 远程请求内置自动重定向处理流程 4.9.6.20 ⏱️2024.12.27 4998e13
    •   HTTP 远程请求 HttpRemoteOptions 选项 AllowAutoRedirectMaximumAutomaticRedirections 配置 4.9.6.20 ⏱️2024.12.27 4998e13
    •   HTTP 远程请求 WithCookie(cookieHeaderValue) 重载方法 4.9.6.18 ⏱️2024.12.25 80394dc
    •   HTTP 远程请求默认无配置支持 HTTP/1.0HTTP/1.1 的服务器接口 4.9.6.16 ⏱️2024.12.17 61afe9a
    •   HTTP 远程请求支持设置请求基地址功能 4.9.6.15 ⏱️2024.12.10 187a178
    •   HTTP 远程请求在添加表单项内容时支持预置操作 4.9.6.12 ⏱️2024.12.06 e610e32
    •   HTTP 远程请求在非依赖注入环境中支持打印请求分析工具内容 4.9.6.12 ⏱️2024.12.06 e610e32
    •   HTTP 远程请求支持声明式设置 HttpRequestMessage 请求属性特性 4.9.6.11 ⏱️2024.12.04 8306cf0
    •   HTTP 远程请求支持配置禁用请求分析工具委托 4.9.6.7 ⏱️2024.12.02 250ea66
    •   HTTP 远程请求支持启用性能优化支持 4.9.6.6 ⏱️2024.12.01 b7ad81b
    •   HTTP 远程请求支持设置自动 Host 标头 4.9.6.6 ⏱️2024.12.01 b7ad81b
    •   HTTP 远程请求 DigestCredentials 摘要身份认证支持 4.9.6.5 ⏱️2024.12.01 3298c02
    •   HTTP 远程请求 FileTypeMapper 文件 MIME 类型映射类 4.9.6.4 ⏱️2024.11.29 6782110
    •   HTTP 远程请求支持带应用速率限制的流 4.9.6.3 ⏱️2024.11.28 f281c32
    •   HTTP 远程请求支持特定需验证 Content-Type 的服务器程序 4.9.6.3 ⏱️2024.11.28 f281c32
    •   HTTP 远程请求支持配置请求分析工具日志级别 4.9.6.3 ⏱️2024.11.28 f281c32
    •   HTTP 远程请求支持全局配置 HttpRemoteOptions 配置 4.9.6.2 ⏱️2024.11.28 b60c996
    •   HTTP 远程请求支持配置查询参数是否忽略空值 ignoreNullValues 4.9.6.2 ⏱️2024.11.28 b60c996
    •   HTTP 远程请求 MultipartFile 添加文件类型 4.9.6.1 ⏱️2024.11.27 590cd5e
    •   HTTP 远程请求 WithStatusCodeHandler 支持包含比较符号类型状态码 4.9.6.1 ⏱️2024.11.27 590cd5e
    •   HTTP 远程请求 AddHttpDeclarativeExtractorsFromAssemblies 批量注册 HTTP 声明式提取器 4.9.6.1 ⏱️2024.11.27 590cd5e
  • 突破性变化
    •   HTTP 远程请求对象的内容转换器工厂(含接口签名变更),提升扩展灵活性与代码可维护性 4.9.7.129 ⏱️2025.10.09 cf83a79
    •   HTTP 远程请求扩展功能接口方法签名 4.9.7.95 ⏱️2025.07.10 4615670
    •   HTTP 远程请求设置 HTTP 版本的声明式特性 [Version] 名称,调整为 [HttpVersion] 4.9.7.41 ⏱️2025.04.14 b054693
  • 问题修复
    •   HTTP 远程请求启用请求分析日志在 Blazor 应用同步请求中出现死锁问题 4.9.8.46 ⏱️2026.04.19 bfa8579
    •   HTTP 远程请求获取代理接口特性列表时未递归查找子特性 4.9.8.44 ⏱️2026.04.18 7b0098d
    •   HTTP 远程请求添加泛型类型的声明式接口出现异常问题 4.9.8.42 ⏱️2026.04.17 b00f2b9
    •   HTTP 远程请求下载文件时若服务器未设置 Content-Length 导致下载失败问题 4.9.8.36 ⏱️2026.04.09 d904e8d
    •   HTTP 远程请求转发 HttpContext 时不能转发 Accept-Language 问题 4.9.8.31 ⏱️2026.03.31 #IHTVU9 1f67681
    •   HTTP 远程请求分析工具打印超过 2GB 文件出现异常问题 4.9.8.2 ⏱️2026.01.24 600d02a
    •   HTTP 远程请求在处理重定向时没有移除路径片段问题 4.9.8.1 ⏱️2026.01.22 288facb
    •   HTTP 远程请求设置基地址不支持路径参数和配置参数问题 4.9.7.232 ⏱️2025.12.22 5252bbd
    •   HTTP 远程请求分析工具存在重复打印问题 4.9.7.219 ⏱️2025.12.03 82091b4
    •   HTTP 远程请求分析日志打印表单数据不全问题 4.9.7.217 ⏱️2025.12.03 5bce378
    •   HTTP 远程请求分析日志不打印 HttpClient 默认配置请求头问题 4.9.7.217 ⏱️2025.12.03 fd0eedc
    •   HTTP 远程请求克隆 HttpRequestMessage 丢失 Options 属性问题 4.9.7.215 ⏱️2025.11.26 bf38601
    •   HTTP 远程请求当上游服务器响应未携带 Content-Type 标头时,引发的空引用异常问题 4.9.7.210 ⏱️2025.11.18 48eae77
    •   HTTP 远程请求转发 HttpContext 内容时,部分状态码的响应正文丢失的问题 4.9.7.210 ⏱️2025.11.18 48eae77
    •   HTTP 远程请求静态类 HttpRemoteClient 多线程死锁问题 4.9.7.137 ⏱️2025.11.07 c044b87
    •   HTTP 远程请求解析响应 Content-Disposition 标头文件名出现中文乱码问题 4.9.7.124 ⏱️2025.09.16 183cb5e
    •   HTTP 远程请求进行文件上传下载时控制台进度条不能自适应问题 4.9.7.116 ⏱️2025.09.02 47250ef
    •   HTTP 远程声明式请求存在并发线程安全问题 4.9.7.115 ⏱️2025.08.31 0a5e57f #ICVKHB
    •   HTTP 远程请求文件下载解析响应标头时文件名存在前后双引号问题 4.9.7.113 ⏱️2025.08.29 5e92eab
    •   HTTP 远程请求转发 HttpContext 丢失 Content-Type 问题 4.9.7.109 ⏱️2025.08.14 9aaf17c
    •   HTTP 远程请求转发 HttpContext 时出现禁用缓存无效问题 4.9.7.104 ⏱️2025.07.24 3a386fa
    •   HTTP 远程请求中无法通过表单方式发送 MultipartFile 类型属性的问题 4.9.7.93 ⏱️2025.07.05 30c853d
    •   HTTP 远程请求上传文件时,未配置文件名导致服务端无法正常接收文件的问题(若未指定文件名,默认将文件名设置为 Unnamed_xxxxxxxxx4.9.7.93 ⏱️2025.07.05 30c853d
    •   HTTP 远程请求分析工具在打印二进制内容时,若包含退格符可能导致输出不完整的问题 4.9.7.93 ⏱️2025.07.05 30c853d
    •   HTTP 远程请求中配置超时时间的问题,并明确了超时后抛出的异常类型 4.9.7.90 ⏱️2025.06.25 679319d
    •   HTTP 远程请求转换 HttpContext 时不能篡改 HttpContent(Body) 问题 4.9.7.89 ⏱️2025.06.20 ca7bfb5
    •   HTTP 远程请求分析工具不支持 Blazor WebAssembly 应用问题 4.9.7.69 ⏱️2025.05.22 c257ed0
    •   HTTP 远程请求请求分析工具手动打印出现格式错乱问题 4.9.7.52 ⏱️2025.04.27 14261e4
    •   v4.9.7.49 版本导致 HTTP 远程请求反序列化出现内存溢出(OOM)问题 4.9.7.50 ⏱️2025.04.25 4cf7375 406ff44
    •   HTTP 远程请求当请求的路径末尾包含 / 时被自动移除问题 4.9.7.45 ⏱️2025.04.17 5b18955
    •   HTTP 远程请求无法通过 RemoveHeaders 移除 User-Agent 问题 4.9.7.44 ⏱️2025.04.17 4d98d60
    •   HTTP 远程请求在强制启用 IPv4 时,若请求地址为 IP 地址时出现的异常问题 4.9.7.28 ⏱️2025.03.23 1d57a07
    •   HTTP 远程请求在解析 URL 参数若参数值出现多个 = 时导致解析失败问题 4.9.7.24 ⏱️2025.03.13 5c9270f
    •   HTTP 远程请求在未设置查询参数且设置了移除查询参数列表时无效 4.9.7.21 ⏱️2025.03.03 7b3335e
    •   HTTP 远程请求文件上传下载、长轮询和 Server-Sent Events 错误处理 CancellationToken 问题 4.9.7.16 ⏱️2025.02.28 21c1f06
    •   HTTP 远程请求客户端配置的基地址时出现空引用异常 4.9.7.16 ⏱️2025.02.28 21c1f06
    •   HTTP 远程请求分析工具未打印实际未成功但确保请求为成功的请求的问题 4.9.7.10 ⏱️2025.02.22 82b4d81
    •   HTTP 远程请求重定向操作错误的处理请求方法和请求体问题 4.9.7.2 ⏱️2025.01.26 c326cf3
    •   HTTP 远程请求转发 HttpContext 文件出现文件已损坏问题 4.9.7.1 ⏱️2025.01.23 e90a08c
    •   HTTP 远程请求遇重定向时可能出现重复拼接查询参数问题 4.9.7 ⏱️2025.01.23 0e64da5
  • 其他更改
    •   HTTP 远程请求文件下载传输进度的通知频率 4.9.8.37 ⏱️2026.04.11 49223d6
    •   HTTP 远程请求超时时间,支持设置为 null 4.9.8.22 ⏱️2026.03.09 537400c
    •   HTTP 远程请求构建器的 .SetOnPreSendRequest 方法,支持多次调用 4.9.7.244 ⏱️2026.01.09 e42e6b0
    •   简化 HTTP 远程请求静态类 HttpRemoteClient 自定义配置 4.9.7.221 ⏱️2025.12.06 ca3d6f6
    •   HTTP 远程请求发送文本内容不支持设置 Content-Type 问题 4.9.7.218 ⏱️2025.12.03 9d6cdd1
    •   HTTP 远程请求日志系统,方便生产环境准确定位错误 4.9.7.212 ⏱️2025.11.26 c40570b
    •   HTTP 远程请求 WebSocket 客户端构造函数选项参数 4.9.7.130 ⏱️2025.10.15 ca85e8e
    •   改进 HTTP 远程请求文件下载功能,新增 FileTransferResult 返回值 4.9.7.128 ⏱️2025.09.30 9311ee3 04010e2
    •   改进 HTTP 远程请求文件上传和下载控制台进度条时间格式 4.9.7.117 ⏱️2025.09.02 665a453
    •   HTTP 远程请求文件上传和下载打印到控制台进度条效果 4.9.7.114 ⏱️2025.08.29 3204e72
    •   HTTP 远程请求设置多部分表单方法(重载) 4.9.7.99 ⏱️2025.07.19 60b9260
    •   HTTP 远程请求分析工具自动处理 Unicode 转义 4.9.7.48 ⏱️2025.04.23 f0a01d6
    •   HTTP 远程请求分析工具,支持打印请求和响应内容的大小 4.9.7.47 ⏱️2025.04.20 cf7956e
    •   HTTP 远程请求默认的 User-AgentEdge 浏览器(版本 133)的 User-Agent 一致 4.9.7.18 ⏱️2025.03.01 b6ba52b
    •   HTTP 远程请求长轮询属性(事件)类型,由 Func<HttpResponseMessage, Task>? -> Func<HttpResponseMessage, CancellationToken, Task> 4.9.7.17 ⏱️2025.02.28 050e64f
    •   HTTP 远程请求 ServerSentEventsonMessage 属性类型,由 Func<ServerSentEventsData, Task>? -> Func<ServerSentEventsData, CancellationToken, Task> 4.9.7.14 ⏱️2025.02.26 5ef4b13
    •   HTTP 远程请求自动设置 Host 请求标头为 false,即默认不启用 4.9.6.20 ⏱️2024.12.27 4998e13
    •   HTTP 远程请求默认启用自动设置请求 Host 标头 4.9.6.16 ⏱️2024.12.17 61afe9a
    •   HTTP 远程请求提交表单数据时默认设置 Boundary 4.9.6.16 ⏱️2024.12.17 61afe9a
    •   HTTP 远程请求 RateLimitedStream 带应用速率限制的流,基于令牌桶算法 4.9.6.10 ⏱️2024.12.03 f0ee8af
    •   HTTP 远程请求分析工具性能,打印内容时默认只输出 10KB 内容 4.9.6.9 ⏱️2024.12.02 88afe64
    •   HTTP 远程请求分析工具,提供请求内容和响应内容打印 4.9.6.7 ⏱️2024.12.02 250ea66
    •   HTTP 远程请求分析工具,提供更多细节打印 4.9.6.4 ⏱️2024.11.29 6782110
4.9.6+ 版本说明

Furion 4.9.6+ 版本采用 HttpAgent 请求代理替换原有的 RemoteRequest查看旧文档

重要说明

以下内容仅适用于 Furion 4.9.6+ 版本,且不支持 .NET8 以下版本。

19.1 HTTP 远程请求概述

HTTP 远程请求是指客户端(如 Web 浏览器、移动应用等)通过 HTTP 协议向远程服务器发送请求,以获取所需资源的过程。它是现代互联网应用中最基础且核心的通信方式之一。

查看高清架构图

19.1.1 应用场景

HTTP 远程请求在互联网应用系统中应用广泛,涵盖以下主要场景:

  • 资源获取:从服务器获取互联网资源,如网页、图片、视频等。
  • 数据抓取:用于网络爬虫抓取网页数据或进行数据分析。
  • 文件传输:支持文件的上传与下载操作。
  • API 对接:与第三方 API 接口进行数据交互。
  • 系统集成:实现异构系统之间的互联互通。
  • 配置管理:用于配置中心的动态配置拉取与更新。
  • 微服务通信:支持微服务架构中的服务间调用。
  • 负载均衡:通过请求分发实现资源优化与高可用性。
  • 压力测试:用于模拟高并发请求,进行系统性能测试。
  • 请求代理:实现请求的代理与转发,支持跨域或安全访问。
  • 其他场景:适用于多种需要远程通信的场景。

HTTP 远程请求为互联网应用提供了高效、灵活的通信能力,是构建分布式系统和实现数据交互的重要技术基础。

19.2 快速入门

安装包说明

Furion 框架已内置该功能,无需额外安装 NuGet 包。若使用非 Furion 框架,可通过以下命令安装 HttpAgentHttpAgent.AspNetCore 包:

  • 适用于任何 .NET/C# 应用:
dotnet add package HttpAgent
  • 适用于 Web 应用(包含 HttpAgent 且提供 HttpContext 转发功能):
dotnet add package HttpAgent.AspNetCore

在发起 HTTP 远程请求前,需在 Startup.csProgram.cs 文件中注册并配置 HttpRemote 服务。

// 在 Startup.cs 中注册:
services.AddHttpRemote();

// 在 Program.cs 中,注册方式如下:
// builder.Services.AddHttpRemote();
解决 AddHttpRemote 二义性错误

若遇到 AddHttpRemote 方法的二义性错误,可通过为其添加一个空的委托参数来解决,示例如下:

services.AddHttpRemote(builder => {});

随后,在您的服务、控制器或任何支持依赖注入的类中,注入 IHttpRemoteService 服务。

public class YourService
{
private readonly IHttpRemoteService _httpRemoteService;

public YourService(IHttpRemoteService httpRemoteService)
{
_httpRemoteService = httpRemoteService;
}
}

若您使用的是 .NET 8 及以上版本时,可通过主构造函数注入简化代码:

public class YourService(IHttpRemoteService httpRemoteService)
{
// 使用 httpRemoteService 变量
}

或者,您也可以在特定方法中按需注入:

public class YourService
{
public Task<string> GetResource([FromServices] IHttpRemoteService httpRemoteService)
{
// 您的代码逻辑
}
}
在没有依赖注入的环境中的使用说明

.NET Core 应用开发中,推荐使用依赖注入(DI)和控制反转(IoC)的方式来构建应用程序。因此,建议在条件允许的情况下,优先采用依赖注入方式管理服务。但在某些特定场景下,例如控制台应用(Console)、WinFormsWPF 项目中,.NET 并未默认集成完整的依赖注入容器。此时,您可以使用以下两种方式手动获取所需服务:

  • Furion 框架

如果您正在使用 Furion 框架,可以通过全局静态类 App 获取已注册的服务:

var httpRemoteService = App.GetRequiredService<IHttpRemoteService>();
  • 其他项目(Console/WinForms/WPF

对于一般的 Console/WinForms/WPF 项目,可以使用 HttpRemoteClient 静态类提供的 Service 属性发起远程 HTTP 请求:

var result = await HttpRemoteClient.Service.GetAsStringAsync("https://furion.net/");

使用建议:上述两种方式应作为依赖注入机制的补充手段使用,而非替代方案。在支持依赖注入的环境中,请尽量遵循标准的 DI 实践来构建和管理应用程序服务。

19.2.1 获取网站内容

获取网站内容是一个常见的需求,例如获取 Furion 框架网站(https://furion.net)的首页内容。以下展示了多种使用 httpRemoteService 来实现这一需求的方法。

// 直接获取字符串内容
var content = await httpRemoteService.GetAsStringAsync("https://furion.net");

除了上述方法外,还支持以下多种方式:

1. 使用构建器方式 ✅

  • 直接获取字符串类型内容:
var content = await httpRemoteService.SendAsStringAsync(HttpRequestBuilder.Get("https://furion.net"));
// var content = await httpRemoteService.SendAsStringAsync(HttpBuilder.Get("https://furion.net")); // 可使用 HttpBuilder 替代 HttpRequestBuilder
  • 通过泛型指定字符串类型:
var content = await httpRemoteService.SendAsAsync<string>(HttpRequestBuilder.Get("https://furion.net"));
  • 获取 HttpRemoteResult<T> 类型,并从中提取结果:
var result = await httpRemoteService.SendAsync<string>(HttpRequestBuilder.Get("https://furion.net"));
var content = result.Result;
  • 获取 HttpResponseMessage 类型,并读取其内容:
var httpResponseMessage = await httpRemoteService.SendAsync(HttpRequestBuilder.Get("https://furion.net"));
var content = await httpResponseMessage.Content.ReadAsStringAsync();

2. 使用请求谓词方式

  • 通过泛型指定字符串类型并直接获取:
var content = await httpRemoteService.GetAsAsync<string>("https://furion.net");
  • 获取 HttpRemoteResult<T> 类型,并从中提取结果:
var result = await httpRemoteService.GetAsync<string>("https://furion.net");
var content = result.Result;
  • 获取 HttpResponseMessage 类型,并读取其内容:
var httpResponseMessage = await httpRemoteService.GetAsync("https://furion.net");
var content = await httpResponseMessage.Content.ReadAsStringAsync();

这些方式提供了灵活的选择,可以根据具体需求选择最适合的方法来获取网站内容。

19.2.2 携带请求数据

在获取第三方 API 数据时,通常需要携带请求数据,这些数据可以是 URL 地址参数或请求内容。最常见的做法是通过 URL 地址传递参数,以及发送 JSON 格式的数据。

var content = await httpRemoteService.PostAsAsync<YourRemoteModel>("https://localhost:7044/HttpRemote/AddModel",
builder => builder
.WithQueryParameters(new { query1 = 1, query2 = "furion" }) // 设置 URL 查询参数
.SetJsonContent(new { id = 1, name = "furion" })); // 设置请求的 JSON 内容

除了上述方式,还支持以下几种方法:

// 使用构建器模式
var content = await httpRemoteService.SendAsAsync<YourRemoteModel>(HttpRequestBuilder.Post("https://localhost:7044/HttpRemote/AddModel")
.WithQueryParameter("query1", 1) // 设置查询参数(支持单个设置)
.WithQueryParameter("query2", "furion") // 设置查询参数(支持单个设置)
.SetJsonContent("{\"id\":1,\"name\":\"furion\"}")); // 设置请求内容(支持直接传入 JSON 字符串)

// 更多详细用法可参考第 19.2.1 节

此外,您还可以使用 SetContent 方法来设置请求内容,该方法支持设置任意类型的请求内容。事实上,SetJsonContent 方法内部也是通过调用 SetContent 来实现的。

// 自定义 Content-Type
var content = await httpRemoteService.PostAsAsync<YourRemoteModel>("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<YourRemoteModel>("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<YourRemoteModel>("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)); // 设置请求内容

19.2.3 Form 表单提交(URL 编码)

在互联网应用中,保存用户自定义的数据最常见的方式是使用 Form 表单提交。Form 表单不仅能携带文本数据,还能携带二进制数据,如文件。

var content = await httpRemoteService.PostAsAsync<YourRemoteFormResult>("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 的表单数据,并提供了丰富的扩展选项,包括:

  • BoundarySetBoundary(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(IFormFile):添加 IFormFile 文件内容。
  • AddFiles(IFormFileCollection):添加 IFormFileCollection 文件内容。
  • AddStream(stream, name):添加二进制流内容。
  • AddByteArray(byteArray, name):添加二进制字节数组内容。
  • Add(httpContent):添加 HttpContent 内容。

这些只是常用的设置方法,SetMultipartContent 提供了更多的灵活性。

除了上述方式,还支持以下几种方法:

// 使用构建器模式
var content = await httpRemoteService.SendAsAsync<YourRemoteFormResult>(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 字符串

// 更多详细用法可参考第 19.2.1 节

以下是一些 Form 表单提交的常见例子:

var content = await httpRemoteService.PostAsAsync<YourRemoteFormResult>("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 异常。异常信息提示如下:

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<HttpMultipartFormDataBuilder>` 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 提交类型的表单数据。

var content = await httpRemoteService.PostAsAsync<YourRemoteModel>("https://localhost:7044/HttpRemote/AddURLForm",
builder => builder
.SetFormUrlEncodedContent(new { id = 1, name = "furion" })); // 设置 application/x-www-form-urlencoded 请求内容

// 支持 URL 编码字符串格式
var content = await httpRemoteService.PostAsAsync<YourRemoteModel>("https://localhost:7044/HttpRemote/AddURLForm",
builder => builder
.SetFormUrlEncodedContent("id=1&name=furion", useStringContent: true);
URL 编码表单内容说明
  • 默认情况下,URL 编码表单通过 FormUrlEncodedContent 类型进行构建,但此类型不支持自定义请求内容编码,它默认使用 Encoding.Latin1 而不是 UTF-8 这可能在提交到某些接口时引发异常。 为解决此问题,可以通过设置参数 useStringContenttrue 来采用 StringContent 方式构建表单数据,从而允许自定义编码为 UTF-8
    var content = await httpRemoteService.PostAsAsync<YourRemoteModel>("https://localhost:7044/HttpRemote/AddURLForm",
    builder => builder
    .SetFormUrlEncodedContent(new { id = 1, name = "furion" }, useStringContent: true));
  • 某些服务器要求显式声明字符集(charset),此时可通过 contentEncoding 参数指定编码方式,例如使用 UTF-8: 此设置在发送远程请求时会生成如下 Content-Type 请求头:application/x-www-form-urlencoded; charset=UTF-8
    var content = await httpRemoteService.PostAsAsync<YourRemoteModel>("https://localhost:7044/HttpRemote/AddURLForm",
    builder => builder
    .SetFormUrlEncodedContent(new { id = 1, name = "furion" }, Encoding.UTF8));

19.2.4 下载网络资源

HTTP 远程请求最常见的应用场景之一是下载网络资源并将其保存到本地磁盘,这包括下载网页内容、图片、压缩包以及安装软件等。以下示例展示了如何下载 ASP.NET Core 运行时:

// 从指定 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 参数来指定文件存在时的行为:

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:保留现有文件,并跳过下载操作。

在下载文件时,您还可以获取实时的下载进度。以下示例展示了如何打印下载进度:

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(progress.ToSummaryString()); // 输出简要进度字符串
await Task.CompletedTask;
}
, fileExistsBehavior: FileExistsBehavior.Overwrite);

下载进度的控制台输出示例(使用 progress.ToSummaryString()):

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.

若需在控制台中实时显示文件下载进度,推荐使用 UpdateConsoleProgress() 方法。示例如下:

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 =>
{
progress.UpdateConsoleProgress(); // 在控制台中更新文件传输进度条
await Task.CompletedTask;
}
, fileExistsBehavior: FileExistsBehavior.Overwrite);

执行后,控制台将显示如下进度信息:

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(),则控制台输出将包含更详细的进度信息:

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():在控制台中更新(打印)文件传输进度条。

除了上述方式,还支持以下几种方法:

// 使用构建器模式
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));

// 更多详细用法可参考第 19.2.1 节

19.2.5 上传文件资源(OSS

在互联网应用中,用户上传文件是一项常见需求,涵盖设置头像、发布图文动态、上传相册至网盘、分享 Vlog 到视频社区等场景。以下展示了多种文件上传的实现方式。

1. 使用 Form 表单方式上传

通过 Form 表单方式上传文件的方式与第 19.2.3 章节中描述的 Form 表单提交方法一致。

await httpRemoteService.PostAsync("https://localhost:7044/HttpRemote/AddFile", builder => builder
.SetMultipartContent(multipart => multipart
.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file")));

若需上传多个文件,只需在 multipart 中继续添加(需保持表单名一致,如 files):

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")));

此外,还支持使用构建器模式,以及获取上传文件的返回值。更多详情可参考第 19.2.1 节。

// 使用构建器模式
await httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://localhost:7044/HttpRemote/AddFile")
.SetMultipartContent(multipart => multipart
.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file")));

// 更多详细用法可参考第 19.2.1 节

2. 使用非 Form 表单方式上传(OSS

在与某些 OSS(对象存储服务)或云网盘进行对接时,通常会遇到不支持传统 Form 表单方式上传文件的情况。此时,需要直接以文件字节数组或 Stream 流的形式上传文件。以下是具体实现示例:

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) 方法实现,示例如下:

await httpRemoteService.PutAsync("https://localhost:7044/HttpRemote/AddFile", builder => builder
.SetContent(fileStream) // 或使用 .SetContent(fileBytes);
.SetOmitContentType(true);
// .AutoSetHostHeader()); // 某些服务器可能会强制验证 Host 请求头(可选)
将 IFormFile 实例转换为 Stream 并上传

在 Web 应用中,我们通常使用 IFormFile 类型来接收用户上传的文件。如果需要将该文件进一步上传至 OSS(对象存储服务)或云网盘,可通过以下步骤将其转换为 Stream 并完成上传:

// 创建内存流并写入 IFormFile 数据
var memoryStream = new MemoryStream();
formFile.CopyTo(memoryStream);
memoryStream.Position = 0; // 重置流位置为起始点,确保读取完整数据

await httpRemoteService.PutAsync("https://localhost:7044/HttpRemote/AddFile", builder => builder
.SetContent(memoryStream));

3. 使用 UploadFile 扩展方法(表单方式)

在视频分享等应用中,用户上传文件时通常需要查看实时进度。为此,可使用 UploadFile 扩展方法,该方法支持实时进度获取,并允许对文件类型和大小进行限制。

以下示例展示了如何打印上传进度:

await httpRemoteService.UploadFileAsync("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file"
, async progress =>
{
Console.WriteLine(progress.ToSummaryString()); // 输出简要进度信息
await Task.CompletedTask;
});

控制台输出示例:

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.

若需在控制台中实时显示文件上传进度,推荐使用 UpdateConsoleProgress() 方法。示例如下:

await httpRemoteService.UploadFileAsync("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file"
, async progress =>
{
progress.UpdateConsoleProgress(); // 在控制台中更新文件传输进度条
await Task.CompletedTask;
});

执行后,控制台将显示如下进度信息:

File: httptest.jpg, Path: C:\Workspaces\httptest.jpg.
[##################################################] 61.35% (0.01MB/0.01MB) Speed: 0.86MB/s, Time: 0.01s, ETA: 0.00s.

若需限制文件类型和大小,可如下操作:

await httpRemoteService.SendAsync(HttpRequestBuilder.UploadFile("https://localhost:7044/HttpRemote/AddFile", @"C:\Workspaces\httptest.jpg", "file"
, async progress =>
{
Console.WriteLine(progress.ToSummaryString()); // 输出简要进度信息
await Task.CompletedTask;
})
.SetAllowedFileExtensions(".jpg;.png") // 仅允许 jpg 和 png 类型
.SetMaxFileSizeInBytes(5 * 1024 * 1024)); // 限制文件大小为 5MB

通过上述方式,可以灵活满足各类文件上传需求。

关于多文件上传

UploadFile 扩展方式仅支持单个文件上传,无法同时处理多个文件的上传需求。

禁用请求分析工具

在打印请求内容时,Stream 对象可能会被重复读取或变得不可读。这是因为流会被提前读取到内存中,其位置指针会移动到尾部。这会导致无法准确获取上传进度。

因此,在使用框架提供的专门上传功能时,建议禁用请求分析工具,以确保能够获取准确的上传进度信息。

19.2.6 HTTP 声明式请求(代理方式)

HTTP 声明式请求机制通过实现 IHttpDeclarative 接口,在程序运行时动态地构建实现类。该机制会智能地拦截符合特定规则的方法调用,并自动生成相应的 HTTP 远程请求代码。这种方法不仅极大地减轻了开发人员编写 HTTP 请求代码的负担,而且使得代码结构更加条理分明,更易于进行组织、维护和复用。

以下示例简单展示了如何定义和使用 HTTP 声明式请求:

1. 定义接口 IHttpService 并实现 IHttpDeclarative

public interface IHttpService : IHttpDeclarative
{
// 获取网站内容
[Get("https://furion.net")]
Task<string> GetWebSiteContent();

// 携带请求数据
[Post("https://localhost:7044/HttpRemote/AddModel")]
[Query("query1", 1)] // 设置查询参数
Task<YourRemoteModel> PostData([Query(AliasAs = "query2")] string param, [Body(MediaTypeNames.Application.Json)] object data); // 设置查询参数并指定别名和请求内容

// Form 表单提交
[Post("https://localhost:7044/HttpRemote/AddForm?id=1")]
Task<YourRemoteFormResult> PostForm(Action<HttpMultipartFormDataBuilder> multipart);

// Form 表单提交
[Post("https://localhost:7044/HttpRemote/AddForm?id=1")]
Task<YourRemoteFormResult> PostForm2([Multipart(AsFormItem = false)] object obj, [Multipart("file", AsFileFrom = FileSourceType.Path)] string filePath);

// URL 编码表单提交
[Post("https://localhost:7044/HttpRemote/AddURLForm")]
Task<YourRemoteModel> PostURLForm([Body(MediaTypeNames.Application.FormUrlEncoded)] object data);
}

2. 注册 IHttpService 服务

Startup.csProgram.cs 文件中,注册并配置 HttpRemote 服务以支持 HTTP 声明式请求:

services.AddHttpRemote(builder =>
{
// 注册单个 HTTP 声明式请求接口
builder.AddHttpDeclarative<IHttpService>();

// 扫描程序集批量注册 HTTP 声明式请求接口(推荐此方式注册)
// builder.AddHttpDeclarativesFromAssemblies([Assembly.GetEntryAssembly()]); // 如果使用的是 Furion 框架,可直接传入 App.Assemblies
});

3. 注入 IHttpService 服务

在需要使用 IHttpService 的类中,通过依赖注入获取其实例:

public class YourService
{
private readonly IHttpService _httpService;

public YourService(IHttpService httpService)
{
_httpService = httpService;
}
}

若您使用的是 .NET 8 及以上版本时,可通过主构造函数注入简化代码:

public class YourService(IHttpService httpService)
{
// 使用 httpService 变量
}

或者,您也可以在特定方法中按需注入:

public class YourService
{
public Task<string> GetResource([FromServices] IHttpService httpService)
{
// 您的代码逻辑
}
}

4. 调用 IHttpService 方法

使用注入的 IHttpService 实例调用其方法,以发送 HTTP 请求并获取响应:

// 获取网站内容
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 请求代码的工作量,并使代码更加简洁、易于组织和维护。在大型项目或多人合作项目中,这种方式尤其推荐。

19.2.7 请求分析工具 ✨

在现代化的浏览器中,通常内置了开发者工具,这些工具能够捕获并直观展示用户访问网站时的所有请求与响应数据。类似地,我们也为 HTTP 远程请求模块提供了一套分析工具。

以下是如何启用请求分析工具的示例:

// 构建器方式
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 远程请求时,控制台将输出如下详细信息:

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/
HTTP Method: GET
Status Code: 200 OK
HTTP Version: 1.1
HTTP Content:
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 中启用:

// 为默认客户端启用
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] 特性启用请求分析工具:

[Profiler] // 为接口内所有方法启用请求分析工具
public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net")]
Task<string> ProfilerMethod();

[Profiler(false)] // 关闭该方法请求分析工具
[Get("https://furion.net")]
Task<string> NonProfilerMethod();
}

public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net")]
Task<string> NonProfilerMethod();

[Profiler] // 启用该方法请求分析工具
[Get("https://furion.net")]
Task<string> ProfilerMethod();
}

通过启用请求分析工具,开发者能够更直观、便捷地观察和调试 HTTP 请求,从而提升开发效率与调试准确性。

生产环境禁用

为了确保生产环境的最佳性能和安全性,建议在生产环境中禁用请求分析工具。

此外,打印请求内容时可能会导致 Stream 对象被重复读取或变得不可读,因为流会被提前读取到内存中,其 Position 随之移动到尾部。

补充说明: 请求分析工具默认仅展示请求或响应体中最多 8KB 的内容数据。

19.2.8 添加授权凭证

在互联网社会中,网络安全愈发关键,特别是在与第三方接口对接时,通常需先通过鉴权授权才能访问。目前,互联网应用接口常用的授权方式包括 JWT (JSON Web Token) 身份验证、Basic 身份验证、Digest 摘要身份认证和 OAuth 身份认证。

以下示例展示了如何为 HTTP 远程请求添加授权:

// 添加 JWT (JSON Web Token) 身份验证
await httpRemoteService.SendAsync(HttpRequestBuilder.Get("http://furion.net")
.AddJwtBearerAuthentication("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 类,实现全局授权凭证的注册:

public class AuthorizationDelegatingHandler : DelegatingHandler
{
protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
{
// 参考 SendAsync 代码

return base.Send(request, cancellationToken);
}

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// 添加 JWT (JSON Web Token) 身份验证
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.csProgram.cs 文件中注册 AuthorizationDelegatingHandler

// 注册 AuthorizationDelegatingHandler 为服务
services.TryAddTransient<AuthorizationDelegatingHandler>();

// 为默认客户端启用
services.AddHttpClient(string.Empty)
.AddHttpMessageHandler<AuthorizationDelegatingHandler>();

// 为特定客户端启用
//services.AddHttpClient("weixin")
// .AddHttpMessageHandler<AuthorizationDelegatingHandler>()

这样,每当发送 HTTP 请求时,都会进入 AuthorizationDelegatingHandler 类的 Send/SendAsync 方法,从而自动为请求添加授权凭证。

19.2.9 设置 Cookie(模拟/自动登录)

Cookie 是服务器在 HTTP 响应中发送的一段数据。 客户端 (选择性地) 存储 Cookie,并在后续请求中返回它。 这允许客户端和服务器共享状态。在发送 HTTP 远程请求时,设置 Cookie 有以下两种方式。

  • 通过 HttpClient 全局设置 Cookie

这种方式允许在同源域名下共享 Cookie,并且如果服务器返回了新的 Cookie,这些 Cookie 将在后续请求中自动携带,非常适用于实现网站自动登录等功能。

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 处理:

// 默认客户端配置
services.AddHttpClient(string.Empty)
.ConfigurePrimaryHttpMessageHandler(() =>
new SocketsHttpHandler
{
UseCookies = false // 禁用自动处理 Cookies
});
  • 单个请求设置 Cookie

这种方式仅对当前请求有效,如果服务器返回了新的 Cookie,它们不会被后续请求携带。

await httpRemoteService.SendAsync(HttpRequestBuilder.Get("http://furion.net")
.WithCookie("cookieName", "cookieValue") // 设置单个
.WithCookies(new { name = "furion", author = "monksoul" })); // 设置多个

注意: 在实际应用中,您可能需要根据具体需求选择适当的 Cookie 设置方式,并确保 Cookie 的安全性,例如避免跨站脚本攻击(XSS)和跨站请求伪造攻击(CSRF)。同时,对于敏感信息,建议使用更安全的认证机制,如 OAuthJWT 等。

19.2.10 异常处理(异常抑制)

在发起 HTTP 远程请求时,可能会遇到以下异常情况:

  • 目标主机不可达
  • 请求被取消
  • 请求超时
  • 其他网络异常

默认情况下,这些异常会中断程序执行。为提升系统健壮性,框架提供以下异常处理方案:

1. 基础异常捕获(try/catch 模式)

try
{
var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/");
}
catch(Exception ex)
{
// 异常处理逻辑(如日志记录、降级处理)
}

适用场景:需要精确控制异常处理逻辑时(如记录特定异常日志、执行补偿操作)。

2. 异常抑制(静默模式)

虽然开发者通常使用 try/catch 进行异常处理,但在某些场景下,我们更希望异常发生时静默返回 null 而不中断流程。为此,框架提供了灵活的异常抑制功能。

  • 抑制所有请求异常
var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/")
.SuppressExceptions()); // 抑制所有异常

当请求发生异常时,代码不会中断,而是返回 null,即 httpResponseMessage 的值为 null

在某些场景下,我们希望在抑制异常的同时,仍能捕获异常信息(例如记录到日志中),而不中断程序的正常执行。此时,可以通过 SetOnRequestFailed 回调来实现:

HttpRequestBuilder.Post("https://furion.net/")
.SuppressExceptions()
.SetOnRequestFailed((exception, responseMessage) => // 注意:responseMessage 可能为空
{
Console.WriteLine(exception.Message);
});

该方法允许你在异常被抑制后,安全地处理错误信息,适用于日志记录、监控或其他错误响应逻辑。

  • 仅抑制特定类型的异常

框架还支持仅抑制特定类型的异常。例如,可以仅抑制超时异常和请求取消异常:

var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/")
.SuppressExceptions([typeof(TimeoutException), typeof(TaskCanceledException)])); // 抑制超时和取消异常
  • 禁用异常抑制配置

若需恢复默认行为(即异常发生时中断程序),可显式禁用异常抑制:

var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/")
.SuppressExceptions(false); // 恢复缺省配置

此配置等效于未调用 SuppressExceptions(),当发生任何异常时,程序将中断执行。

注意事项

当启用异常抑制功能时,请注意以下事项:

  1. 覆盖规则
    多次调用 SuppressExceptions() 或相关配置时,仅最后一次调用生效
  2. 状态码检查与异常抑制的优先级
    即使已配置 EnsureSuccessStatusCode(),被抑制的异常仍会返回 null,不会触发状态码检查逻辑。
  3. 异常抑制的优先级
    异常抑制功能的优先级高于状态码检查。如果同时启用状态码检查和异常抑制,异常抑制会优先生效。
  4. 请求拦截器依旧可用
    若通过 SetOnRequestFailed(ex, res) 或其他请求处理机制捕获异常,即使异常被抑制,拦截器或回调方法仍会被调用。
  5. 异常类型选择建议
    应根据具体业务场景谨慎选择需要抑制的异常类型,避免因过度抑制异常而掩盖潜在问题。

19.2.11 压力与模拟测试(性能测试)

在开发面向互联网或需承受多人并发访问的应用系统时,性能压测和接口自动化模拟测试成为部署前的关键环节。通过这两项测试获取的报告指标,我们能在系统上线前对代码进行优化,确保其满足最低上线要求。

Furion 框架官网为例,进行压力测试:

var stressTestHarnessResult = await httpRemoteService.StressTestHarnessAsync("https://furion.net/");
Console.WriteLine(stressTestHarnessResult.ToString()); // 打印压力测试结果

测试结果概览:

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 响应时间(毫秒)
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 类型)。
    • Percentile10ResponseTimeP10 响应时间(毫秒)(double 类型)。
    • Percentile25ResponseTimeP25 响应时间(毫秒)(double 类型)。
    • Percentile50ResponseTimeP50 响应时间(毫秒)(double 类型)。
    • Percentile75ResponseTimeP75 响应时间(毫秒)(double 类型)。
    • Percentile90ResponseTimeP90 响应时间(毫秒)(double 类型)。
    • Percentile99ResponseTimeP99 响应时间(毫秒)(double 类型)。
    • Percentile9999ResponseTimeP99.99 响应时间(毫秒)(double 类型)。
  • 方法
    • ToString():输出带缩进的详细报告字符串。

默认情况下,压力测试执行 1 轮,每次包含 100 个并发请求,最大并发度为 100。为获取更精确的测试结果,可按需调整这些参数:

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 请求标头,以防止滥用对目标系统造成损害。 同时,由于测试结果受硬件设备、操作系统及代码实现等多种因素影响,仅供参考。

此外,为获取更准确的数据,请求分析工具默认被禁用

19.2.12 长轮询 Long Polling

长轮询(Long Polling)是一种实现服务器向客户端推送数据的技术。它通过保持 HTTP 连接打开直到有新数据发送给客户端,或者直到超时为止,从而模拟了服务器推送的效果。长轮询是传统轮询(即客户端定期向服务器发送请求以检查是否有新的数据)的一种改进,可以减少不必要的请求,提高效率。

长轮询的工作原理:

  1. 客户端向服务器发起一个请求。
  2. 如果服务器上没有新数据,服务器不会立即响应这个请求,而是将请求挂起。
  3. 一旦服务器上有新数据可供发送,或达到了预设的超时时间,服务器就会响应请求,并发送数据给客户端。
  4. 客户端处理完数据后,再次向服务器发起一个新的请求,重复上述过程。

长轮询的应用场景:

  • 实时通知:例如,在线聊天应用中,当用户收到新消息时,服务器可以通过长轮询及时推送消息给客户端。
  • 在线协作工具:如多人同时编辑文档的应用,长轮询可以用来实时同步用户的编辑操作。
  • 游戏更新:在网络游戏中,长轮询可用于实时更新游戏状态,比如玩家位置、得分等信息。
  • 股票市场更新:金融应用程序中使用长轮询来实时显示股票价格变动。
  • 配置中心:在微服务架构中,配置中心使用长轮询技术来确保各个服务能够即时接收到最新的配置变更。当配置发生更改时,配置中心可以迅速将更新推送到所有相关的服务实例,确保配置的一致性和时效性。

以下示例展示了如何使用长轮询请求:

await httpRemoteService.LongPollingAsync("https://localhost:7044/HttpRemote/LongPolling"
, async (responseMessage, token) =>
{
Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(cancellationToken));
await Task.CompletedTask;
}, cancellationToken: cancellationToken);

// 使用构建器模式
await httpRemoteService.SendAsync(HttpRequestBuilder
.LongPolling("https://localhost:7044/HttpRemote/LongPolling"
, async (responseMessage, token) =>
{
Console.WriteLine(await responseMessage.Content.ReadAsStringAsync(cancellationToken));
await Task.CompletedTask;
}), cancellationToken: cancellationToken);

虽然长轮询在一定程度上解决了实时通信的需求,但它也有一些缺点,比如在高并发情况下可能会对服务器造成较大压力,以及长时间的连接可能会影响服务器的性能。随着 Web 技术的发展,Server-Sent EventsWebSocket 等更先进的技术逐渐成为实现实时双向通信的首选方案。然而,在某些受限环境中,长轮询仍然是一个可行的选择。

19.2.13 Server-Sent Events 单向通信

随着人工智能聊天机器人 ChatGPT 的快速流行,其用户界面中模拟打字机效果的对话设计给人留下了深刻印象。这种生动逼真的交互体验,实际上是通过一种称为“服务器发送事件”(Server-Sent Events, SSE)的技术实现的。

Server-Sent Events 是一种允许服务器主动向客户端(通常是浏览器)发送实时更新数据的通信技术。与传统的客户端请求-服务器响应模式不同,SSE 实现了服务器到客户端的单向、异步通信,从而无需客户端不断轮询服务器以获取最新数据。 这种技术极大地减轻了服务器的负担,并提高了数据传输的效率和实时性。

Server-Sent Events 的应用场景:

  1. 实时通知:可以用来实现实时的消息提醒或通知系统,如社交网络上的新消息提示或邮件到达通知。
  2. 数据流更新:对于需要持续更新的数据,如股票价格、天气信息或体育比赛结果,SSE 能够提供即时的数据更新。
  3. 进度报告:在执行耗时较长的任务时,比如文件上传或复杂计算过程中,SSE 可以用来向客户端报告任务的进度。
  4. 日志和监控:在开发和运维领域,SSE 可用于实时显示日志文件的变化或监控系统的健康状态。

以下示例展示了如何使用 Server-Sent Events 向服务器获取数据:

await httpRemoteService.ServerSentEventsAsync("https://localhost:7044/HttpRemote/Events"
// 接收到数据时的操作
, async (data, token) =>
{
Console.WriteLine(data.Data.ToString());
await Task.CompletedTask;
}, cancellationToken: cancellationToken);

// 使用构建器模式
await httpRemoteService.SendAsync(HttpRequestBuilder
.ServerSentEvents("https://localhost:7044/HttpRemote/Events"
// 接收到数据时的操作
, async (data, token) =>
{
Console.WriteLine(data.Data.ToString());
await Task.CompletedTask;
}), cancellationToken: cancellationToken);

data 参数的类型为 ServerSentEventsData,包含以下属性:

  • 属性
    • Event:事件类型(string 类型)。
    • Data:消息(string 类型)。
    • Id:事件 IDstring 类型)。
    • Retry:重新连接的时间(以毫秒为单位的 int 类型)。
    • CustomFields:自定义的字段数据(IReadOnlyCollection<KeyValuePair<string, string>> 类型)。

您还可以监听连接成功和发送异常时的事件:

await httpRemoteService.ServerSentEventsAsync("https://localhost:7044/HttpRemote/Events"
// 接收到数据时的操作
, async (data, token) =>
{
Console.WriteLine(data.Data.ToString());
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.ToString());
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

HttpRequestBuilder
.ServerSentEvents(HttpMethod.Post, new Uri("https://localhost:7044/HttpRemote/Events"));

19.2.14 WebSocket 双工通信

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

WebSocket 的应用场景:

  • 实时聊天应用WebSocket 可以实现实时的消息传递,使得用户间的交流几乎无延迟。
  • 在线游戏:对于需要快速响应的游戏,WebSocket 能够提供低延迟的数据传输。
  • 股票市场更新:实时更新股票价格和其他金融信息。
  • 协同编辑工具:允许多个用户同时编辑同一个文档,并实时看到其他人的更改。
  • 实时地图应用:例如导航应用中实时交通状况的更新。

以下示例展示了如何使用 WebSocketClient 连接服务器:

using var webSocketClient = new WebSocketClient("wss://localhost:7044/ws"); // 支持 ws:// 和 wss://

// 连接成功事件
webSocketClient.Connected += (sender, s) => Console.WriteLine("连接成功");
// 连接关闭事件
webSocketClient.Closed += (sender, args) => Console.WriteLine("连接关闭");
// 接收文本消息
webSocketClient.TextReceived += (sender, s) => Console.WriteLine(s.Message);
// 接收二进制消息
webSocketClient.BinaryReceived += (sender, s) => Console.WriteLine(s.Message);

// 连接服务器
await webSocketClient.ConnectAsync(cancellationToken);

for (var i = 0; i < 10; i++)
{
// 向服务器发送消息
await webSocketClient.SendAsync($"Message at {DateTime.UtcNow}\n\n", cancellationToken: cancellationToken);

await Task.Delay(1000, cancellationToken);
}

// 关闭连接
await webSocketClient.CloseAsync(cancellationToken);

WebSocketServer-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 可能更加轻量和易于实现。

19.2.15 HttpContext 转发和代理

HttpContext 转发是指在 ASP.NET Core 应用程序中,将一个 HTTP 请求的上下文信息(包括请求标头、请求内容、查询字符串、响应标头、响应内容等)从一个请求转发到另一个内部请求或服务的过程。这种技术允许开发者在不改变客户端请求的情况下,将请求重定向到另一个处理点,从而实现请求的代理或路由功能。

HttpContext 转发的应用场景:

  • API Gateway 模式:作为所有外部请求的入口点,将请求路由到正确的后端服务。
  • 负载均衡和故障转移:将请求转发到其他可用的服务实例,确保系统的稳定性和可靠性。
  • 请求日志记录和审计:将请求信息记录到日志系统或审计服务,便于监控和调试。
  • 安全过滤和验证:在转发过程中检查请求的认证信息和权限,确保请求的合法性。
  • A/B 测试和蓝绿部署:将部分流量路由到新版本的服务,逐步验证新功能。
  • 跨域请求处理:处理跨域请求,确保请求能够成功执行。

在使用 HttpContext 进行转发操作之前,请确保已完成以下两个步骤:

独立库说明

Furion 框架已内置该功能,无需额外安装 NuGet 包。若您使用 HttpAgent 独立库,请安装 HttpAgent.AspNetCore 以替代 HttpAgent

  1. 注册并启用 IHttpContextAccessor 服务。

Startup.csProgram.cs 文件中注册并启用 IHttpContextAccessor 服务。

services.AddHttpContextAccessor(); // 若使用 Furion 框架无需注入(已默认注入)
  1. 启用请求正文缓存中间件,以支持请求内容的重复读取。
app.UseEnableBuffering();
  1. (可选) 若在转发过程中出现 The SSL connection could not be established, see inner exception. 的证书错误问题,您可以通过添加以下配置来忽略 SSL 证书验证:
// 默认客户端配置
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 转发:

[ApiController]
[Route("[controller]/[action]")]
public class GetStartController(IHttpRemoteService httpRemoteService, IHttpContextAccessor httpContextAccessor) : ControllerBase
{
// 转发代理到网站
[HttpGet]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // 禁用浏览器缓存
public Task<IActionResult?> ForwardToWebSite()
{
return httpContextAccessor.HttpContext.ForwardAsResultAsync("https://github.com");
}

// 转发代理到图片
[HttpGet]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] // 禁用浏览器缓存
public Task<IActionResult?> 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<IActionResult?> 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<YourRemoteFormResult?> ForwardToForm(int id, [FromForm] YourRemoteFormModel model)
{
return httpContextAccessor.HttpContext.ForwardAsAsync<YourRemoteFormResult>(
"https://localhost:7044/HttpRemote/AddForm");
}
}
X-Forward-To 请求标头

除了可以手动配置转发目标地址外,系统还支持通过解析 X-Forward-To 请求标头来自动设置目标地址。

GET 请求转发失败的可能原因

在某些特殊应用场景中,例如通过 GET 请求转发至特定文件或图片时,可能会出现转发失败的情况。这可能是由于 TLS/SSL 证书问题导致的。此时,请确保用于转发的目标应用通过 HTTPS 协议部署网站。

通过 HttpContext 转发,可以在 ASP.NET Core 应用程序中结合 Middleware 中间件技术实现灵活的请求路由和处理机制,适用于多种应用场景,如 API Gateway、负载均衡、请求日志记录、安全验证等。

19.2.16 WebService 接口请求(SOAP

WebService 是一种基于 SOA(面向服务架构)的应用程序,具有语言和平台无关性。它通过 XML 描述实现不同语言间的相互调用,并利用 HTTP 协议在 Internet 上进行网络应用间的交互。框架支持对 WebService 接口的请求,以下为示例代码:

SOAP 1.1

var result = await httpRemoteService.PostAsStringAsync("http://您的主机地址/Share/DatabaseManager.asmx",
builder => builder.WithHeader("SOAPAction", "http://tempuri.org/GetDatabaseList")
.SetXmlContent("""
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Header>
<Erp7SoapHeader xmlns="http://tempuri.org/">
<ID></ID>
</Erp7SoapHeader>
</soap:Header>
<soap:Body>
<GetDatabaseList xmlns="http://tempuri.org/" />
</soap:Body>
</soap:Envelope>
""", Encoding.UTF8));

SOAP 1.2

var result = await httpRemoteService.PostAsStringAsync("http://您的主机地址/Share/DatabaseManager.asmx",
builder => builder.SetXmlContent("""
<?xml version="1.0" encoding="utf-8"?>
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
<soap12:Header>
<Erp7SoapHeader xmlns="http://tempuri.org/">
<ID></ID>
</Erp7SoapHeader>
</soap12:Header>
<soap12:Body>
<GetDatabaseList xmlns="http://tempuri.org/" />
</soap12:Body>
</soap12:Envelope>
""", Encoding.UTF8, "application/soap+xml"));
确保服务器端支持 SOAP 1.2。

如果服务器仅支持 SOAP 1.1,请调整请求以符合 SOAP 1.1 规范。具体做法是将 XML 内容中的 xmlns:soap12soap12: 分别替换为 xmlns:soapsoap:

SOAP 1.1 和 SOAP 1.2 的区别
特性SOAP 1.1SOAP 1.2
命名空间http://schemas.xmlsoap.org/soap/envelope/http://www.w3.org/2003/05/soap-envelope
Content-Typetext/xmlapplication/soap+xml
SOAPAction必须设置 SOAPAction 请求头可选,可以使用 action 参数
错误处理使用 SOAP Fault使用 SOAP Fault,但结构更规范
协议支持较旧,广泛支持较新,支持更多特性(如 MTOM

在某些 WebService 接口返回的 XML 中,soap:Body 节点可能经过 Base64 编码和 GZip 压缩。此时,可通过以下代码进行解码和解压:

// 使用 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());

19.2.17 HTTP 请求断言(Assert) ✨

在开发或编写单元测试、集成测试时,我们经常需要验证响应结果是否符合预期,这一过程通常称为“断言”。例如,判断响应状态码是否为 200,或检查响应头是否包含 Content-Length。若断言失败,系统默认会抛出 HttpAssertionException 异常。

要启用断言功能,需调用 EnableAssertions() 方法,并配合 Asserts(configure) 方法使用:

HttpRequestBuilder.Get("https://furion.net")
.EnableAssertions()
.Asserts(ast => ast.StatusCode(200)
.HeaderExists("encoding")
.HeaderEquals("framework", "Furion"));

其中,ast 参数为 HttpAssertionBuilder 类型,内置了以下常用断言方法(支持自定义扩展):

  • AddAssertion(assertion):添加自定义断言委托,如 ast.AddAssertion(async context => await...)
  • StatusCode(statusCode):断言响应状态码等于指定值(整数或 HttpStatusCode
    • 失败时抛出:Expected status code to be {expected}, but found {actual}.
  • StatusCodeIn(allowedStatusCodes):断言状态码在允许列表中
    • 失败时抛出:Expected status code to be one of [{string.Join(", ", allowedStatusCodes)}], but found {actual}.
  • IsSuccessStatusCode():断言请求成功(状态码为 2xx)
    • 失败时抛出:Expected request to be successful (2xx status code), but found status code {(int)context.StatusCode}.
  • ContentContains(expectedSubstring):断言响应内容包含指定子字符串(不区分大小写)
    • 失败时抛出:Expected response content to contain '{expectedSubstring}', but it was not found.
  • HeaderExists(name):断言指定响应头存在(包括内容头)
    • 失败时抛出:Expected response header '{name}' to exist, but it was not found.
  • HeaderEquals(name, expectedValue):断言响应头的第一个值严格等于指定字符串(区分大小写)
    • 失败时抛出:Expected response header '{name}' to be '{expectedValue}', but found '{actualValue}'.
  • HeaderContains(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)}].
  • DurationUnder(maxMilliseconds):断言请求耗时低于指定毫秒数
    • 失败时抛出:Expected request duration to be under {maxDuration.TotalMilliseconds:F2}ms, but it took {actualDuration.TotalMilliseconds:F2}ms.

19.2.18 JSON 响应反序列化包装器

在与第三方 API 进行 HTTP 远程通信时,通常会返回统一结构的 JSON 响应,例如 ApiResult<T> 类型,其中实际数据存放在 Data 属性中:

public class ApiResult<T>
{
public bool Success { get; set; }
public T? Data { get; set; } // 实际返回数据
}

在未启用 JSON 响应反序列化包装器功能时,每次调用都需要显式指定 ApiResult<T> 类型:

var content = await httpRemoteService.SendAsAsync<ApiResult<string>>(
HttpRequestBuilder.Get("https://example.com"));

为简化调用流程,可配置 JSON 响应反序列化包装器,使其自动提取 Data 属性内容:

// 配置默认 HTTP 客户端
services.AddHttpClient(string.Empty)
.ConfigureOptions(options =>
{
options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data));
});

配置完成后,通过调用 JsonResponseWrapping() 启用该功能,之后只需指定目标数据类型,无需重复声明 ApiResult<T>

var content = await httpRemoteService.SendAsAsync<string>(
HttpRequestBuilder.Get("https://example.com").JsonResponseWrapping());

框架将在运行时自动创建 ApiResult<string> 实例,并返回其 Data 属性的值。

也可全局启用 JSON 响应反序列化包装器功能,只需设置 UseJsonResponseWrappingtrue

// 配置默认 HTTP 客户端
services.AddHttpClient(string.Empty)
.ConfigureOptions(options =>
{
options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data));
options.UseJsonResponseWrapping = true;
});

全局启用后,所有请求默认使用包装功能:

var content = await httpRemoteService.SendAsAsync<string>(
HttpRequestBuilder.Get("https://example.com")); // 无需显式调用 JsonResponseWrapping()

若需对特定请求禁用该功能,可调用以下方法:

var content = await httpRemoteService.SendAsAsync<ApiResult<string>>(
HttpRequestBuilder.Get("https://example.com").DisableJsonResponseWrapping()); // 或使用 JsonResponseWrapping(false)

默认情况下,未调用 JsonResponseWrapping() 表示未启用该功能,此时需传入完整的响应类型,无需显式调用 DisableJsonResponseWrapping(),除非全局配置了 UseJsonResponseWrapping = true

19.2.19 服务发现(ServiceDiscovery

服务发现是一种允许开发人员使用逻辑名称而非物理地址(如 IP 地址和端口)来引用外部服务的机制。例如,我们可以使用 furion 来代替 https://furion.net这种方式的好处在于,可以在运行时通过配置修改服务地址,而无需更改程序代码,同时还能实现自动选择服务终结点以实现负载均衡。服务发现在微服务架构中尤为常见。

要在 HTTP 远程请求中使用服务发现,可以按照以下步骤进行配置:

1. 安装 Microsoft.Extensions.ServiceDiscovery

dotnet add package Microsoft.Extensions.ServiceDiscovery

2. 配置并启用 ServiceDiscovery 服务

Startup.csProgram.cs 文件中注册并配置 ServiceDiscovery 服务:

services.AddServiceDiscovery();
services.AddHttpRemote()
.ConfigureHttpClientDefaults(clientBuilder =>
{
clientBuilder.AddServiceDiscovery();
});

3. 在配置文件中添加服务终结点

appsettings.json 文件中配置服务终结点。以下示例配置了 furionweixin 两个服务,每个服务都包含多个终结点。每次发送请求时,系统会自动选择一个终结点。

{
"Services": {
"furion": {
"https": ["localhost:5001", "furion.net"]
},
"weixin": {
"https": ["localhost:8080", "weixin.qq.com"]
}
}
}

4. 配置 HttpClient 客户端的 BaseAddress

接下来,配置 HttpClient 客户端的 BaseAddress,以便在请求时使用逻辑名称而非具体的物理地址。

// 配置默认客户端
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 发送远程请求:

// 发送默认客户端请求
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 官方文档

19.2.20 HttpRemoteResult<TResult> 返回类型

HttpRemoteResult<TResult> 是一个泛型类型,专门用于 HTTP 远程请求模块中的响应内容。泛型参数 TResult 代表最终需要转换成的数据类型,除了支持常见的 HTTP 响应类型如 stringbyte[]StreamHttpResponseMessageIActionResult,还支持自定义类型和框架内置的 VoidContent 类型。该类型封装了常用的 HTTP 响应信息和请求耗时等功能。

HTTP 远程请求模块中,所有默认的不包含 As 关键字的泛型请求方法返回值均为 HttpRemoteResult<TResult> 类型。以下是通过不同方式获取 HttpRemoteResult<TResult> 类型返回值的示例:

// 请求谓词方式
var httpResult = await httpRemoteService.GetAsync<string>("https://furion.net/");

// 构建器方式
var httpResult = await httpRemoteService.SendAsync<string>(HttpRequestBuilder.Get("https://furion.net/"));

HttpRemoteResult<TResult> 包含以下属性和方法:

  • 属性
    • ResponseMessage:响应消息(HttpResponseMessage 类型)。
    • ContentType:内容类型(string 类型)。
    • CharSet:字符集(string 类型)。
    • ContentEncoding:内容编码(ICollection<string> 类型)。
    • ContentLength:内容大小(long 类型)。
    • Server:原始响应标头 ServerHttpHeaderValueCollection<ProductInfoHeaderValue> 类型)。
    • RawSetCookies:原始响应标头 Set-Cookie 集合(List<string> 类型)。
    • SetCookies:响应 Cookie 集合(IList<SetCookieHeaderValue> 类型)。
    • StatusCode:响应状态码(HttpStatusCode 类型)。
    • IsSuccessStatusCode:是否请求成功(bool 类型)。
    • Result:目标数据(TResult 泛型类型)。
    • RequestDuration:请求耗时(毫秒)(long 类型)。
    • Headers:响应标头(HttpResponseHeaders 类型)。
    • ContentHeaders:响应内容标头(HttpContentHeaders 类型)。
    • VersionHTTP 版本(Version 类型)。
    • HttpClientNameHttpClient 实例的配置名称(string? 类型)。
  • 方法
    • ToString():输出带缩进的详细请求和响应信息字符串。
返回值类型说明

默认情况下,当返回值类型不是 stringbyte[]StreamHttpResponseMessageVoidContentIActionResult 时,其他类型将使用 System.Text.Json 进行反序列化处理。

如果需要更改此行为,可以在后续章节中了解如何实现 IHttpContentConverter 内容转换器接口进行自定义。

在最新版本中,框架为 HttpRemoteResult<TResult> 类型引入了对解构函数的支持,通过解构表达式简化对象解析过程,使得获取关键属性值变得更加便捷。以下是示例代码:

// 解构表达式用于提取必需的属性值
var (result, response) = await httpRemoteService.GetAsync<string>("https://furion.net/"); // 可调用 ThrowIfNull()/OrDefault() 解决空引用警告问题
var (result, response, isSuccess) = await httpRemoteService.GetAsync<string>("https://furion.net/"); // 可调用 ThrowIfNull()/OrDefault() 解决空引用警告问题
var (result, response, isSuccess, statusCode) = await httpRemoteService.GetAsync<string>("https://furion.net/"); // 可调用 ThrowIfNull()/OrDefault() 解决空引用警告问题

在这几个例子中,resultTResult 类型,responseHttpResponseMessage 类型,isSuccessbool 类型,而 statusCode 则是 HttpStatusCode 类型。

通过使用解构表达式,不仅提升了代码的可读性,也让开发过程更加高效。这种改进允许开发者直接访问所需的数据,减少了手动获取各个属性值的步骤,从而使代码更简洁、直观。


此外,HttpRemoteResult<TResult> 类型还内置了一个 ToString() 方法,该方法能够以缩进格式清晰地打印出请求标头和响应标头的详细信息,如下所示:

Console.WriteLine(httpResult.ToString()); // 或使用 Console.WriteLine(httpResult);

终端控制台输出如下:

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/
HTTP Method: GET
Status Code: 200 OK
HTTP Version: 1.1
HTTP Content:
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

19.2.21 DeepSeek 官方对接 ✨

DeepSeek 是由深度求索公司开发的多功能人工智能模型,具备聊天、写作、编程、数据分析、翻译及教育辅导等能力。其强大的理解能力和快速学习速度使其适用于多种场景,未来发展潜力巨大。

在对接 DeepSeek 人工智能模型之前,您需要先在 DeepSeek 开发平台 注册账号并创建 API key。获取 API key 后,即可在项目中集成 DeepSeek 人工智能模型。框架提供了多种对接 DeepSeek 人工智能模型的方式:

DeepSeek 接口文档

如需了解更多 DeepSeek 开发文档,请访问:https://api-docs.deepseek.com/zh-cn/

1. 标准输出(非流式)

标准输出(非流式)是指将用户提示词一次性发送并返回最终结果。结果会一次性呈现,适合需要完整输出的场景:

[HttpGet]
public async Task<string> DeepSeek(CancellationToken cancellationToken)
{
var result = await httpRemoteService.SendAsync<string>(HttpRequestBuilder
.Post("https://api.deepseek.com/chat/completions")
.Profiler(false) // 建议关闭请求分析工具
.AddJwtBearerAuthentication("您的 APIKEY")
.SetJsonContent("""
{
"model": "deepseek-chat",
"messages": [
{"role": "system", "content": "你是一个专业的 C# 领域人才。"},
{"role": "user", "content": "Furion 框架未来前景?"}
],
"stream": false
}
"""), cancellationToken);

// 使用流变对象获取实际内容
dynamic clay = Clay.Parse(result.Result, ClayOptions.Flexible);
var content = clay.choices[0].message.content;

return content;
}
小知识

在上面的 JSON 数据中,messages 数组包含两个对象,每个对象都有一个 role 键,分别设置为 systemuser,例如:

{
"model": "deepseek-chat",
"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 的支持,可直接使用:

[HttpGet]
public async Task<string> DeepSeek_Stream(CancellationToken cancellationToken)
{
await httpRemoteService.SendAsync(HttpRequestBuilder.ServerSentEvents(HttpMethod.Post,
new Uri("https://api.deepseek.com/chat/completions")
, async (data, token) =>
{
// 输出完成
if (data.Data == "[DONE]")
{
Console.WriteLine("++++++++++++ 结束 ++++++++++++");
return;
}

// 控制打字机速度
await Task.Delay(60, token);

// 使用流变对象获取实际内容
dynamic clay = Clay.Parse(data.Data, ClayOptions.Flexible);
var content = clay.choices[0].delta.content;

Console.WriteLine(content);
}).WithRequest(builder => builder
.Profiler(false) // 建议关闭请求分析工具
.AddJwtBearerAuthentication("您的 APIKEY")
.SetJsonContent("""
{
"model": "deepseek-chat",
"messages": [
{"role": "system", "content": "你是一个专业的 C# 领域人才。"},
{"role": "user", "content": "Furion 框架的作者是谁?"}
],
"stream": true
}
""")), cancellationToken);

return "OK";
}
禁用请求分析工具

在发送 Server-Sent Events(服务器发送事件)时,由于它采用 Stream 流式返回数据,如果启用请求分析工具,会导致流式数据的每个部分被提前加载到内存中读取。这不仅会严重影响流式数据的实时显示效果,还可能在返回大量数据时引发内存过高的问题。

因此,建议在发送 Server-Sent Events 请求时关闭请求分析工具。

3. 浏览器 URL 地址(Web)的流式输出(Server-Sent Events

您还可以在浏览器中通过访问 URL 地址实现流式输出效果:

[HttpGet]
public async Task DeepSeekChat([FromServices] IHttpContextAccessor httpContextAccessor, [FromQuery] string message, CancellationToken cancellationToken)
{
var httpContext = httpContextAccessor.HttpContext!;

// 设置响应头,指定内容类型为 text/event-stream
httpContext.Response.ContentType = "text/event-stream; charset=utf-8";
httpContext.Response.Headers.CacheControl = "no-cache";
httpContext.Response.Headers["X-Accel-Buffering"] = "no";

await httpRemoteService.SendAsync(HttpRequestBuilder.ServerSentEvents(HttpMethod.Post,
new Uri("https://api.deepseek.com/chat/completions")
, async (data, token) =>
{
// DeepSeek 输出完成标记
if (data.Data == "[DONE]")
{
return;
}

// 控制打字机速度
await Task.Delay(60, token);

// 使用流变对象获取实际内容
dynamic clay = Clay.Parse(data.Data, ClayOptions.Flexible);
var content = clay.choices[0].delta.content;

// 确保数据被立即发送到客户端
await httpContext.Response.Body.WriteAsync(Encoding.UTF8.GetBytes(content), token);
await httpContext.Response.Body.FlushAsync(token);
}).WithRequest(builder => builder
.Profiler(false) // 建议关闭请求分析工具
.AddJwtBearerAuthentication("您的 APIKEY")
.SetJsonContent($$"""
{
"model": "deepseek-chat",
"messages": [
{"role": "system", "content": "你是一个专业的 C# 领域人才。"},
{"role": "user", "content": "{{message}}"}
],
"stream": true
}
""")), cancellationToken);

await httpContext.Response.CompleteAsync();
}
禁用请求分析工具

在发送 Server-Sent Events(服务器发送事件)时,由于它采用 Stream 流式返回数据,如果启用请求分析工具,会导致流式数据的每个部分被提前加载到内存中读取。这不仅会严重影响流式数据的实时显示效果,还可能在返回大量数据时引发内存过高的问题。

因此,建议在发送 Server-Sent Events 请求时关闭请求分析工具。

打开浏览器并访问以下地址即可体验流式输出效果:https://localhost:7044/GetStart/DeepSeekChat?message=Furion框架怎么样。如下图所示:

19.3 HttpRequestBuilder 请求构建器 ✨

HttpRequestBuilder 是一个构建器工具,专门用于在通过 HttpClient 发送请求时构建所需的 HttpRequestMessage 对象。可以说,HttpRequestBuilder 是整个 HTTP 远程请求模块的核心组件,负责在发送请求前准备所有必要的请求数据。如下图所示:

查看高清架构图

19.3.1 创建构建器实例

HttpRequestBuilder 类型的构造函数被设计为私有,因此无法直接使用 new 关键字进行实例化。不过,它提供了多个静态方法来方便地创建 HttpRequestBuilder 的实例。

小贴士

HttpRequestBuilder 太长?用 HttpBuilder 更清爽!

1. 使用请求谓词静态方法(推荐)

HttpRequestBuilder 提供了多种基于 HTTP 请求方法(如 GETPOST 等)的静态方法,用于快速创建实例,这些方法支持重载,以适应不同的参数需求。

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 请求,支持多个重载

2. 使用 Create 静态方法

Create 方法允许通过更灵活的方式创建 HttpRequestBuilder 实例,支持直接指定请求方法和 URL,或使用自定义 HttpMethod

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. 通过 JSON 配置创建(FromJson

FromJson 方法允许通过 JSON 字符串配置 HttpRequestBuilder 实例,支持丰富的配置选项。

// 基础使用
var httpRequestBuilder = HttpRequestBuilder.FromJson("""
{
"url": "https://furion.net/",
"method": "GET",
}
""");

// 设置请求内容
var httpRequestBuilder = HttpRequestBuilder.FromJson("""
{
"url": "https://furion.net/adduser",
"method": "POST",
"contentType": "application/json",
"encoding": "utf-8",
"data": {
"id": 1,
"name": "百小僧",
"age": 30
}
}
""");

// 设置多部分表单内容,仅支持 object 对象,不支持二进制数据
var httpRequestBuilder = HttpRequestBuilder.FromJson("""
{
"url": "https://furion.net/adduser",
"method": "POST",
"multipart": {
"id": 1,
"name": "furion",
}
}
""");

// 更多配置
var httpRequestBuilder = HttpRequestBuilder.FromJson("""
{
"url": "/user",
"method": "POST",
"baseAddress": "https://furion.net",
"headers": {
"User-Agent": "HttpAgent/1.0.0",
"Authorization": "Bearer xxxx"
},
"queries": {
"id": 1,
"name": "Furion"
},
"cookies": {
"sessionId": "abcdefg123456",
"userid": "monksoul"
},
"timeout": 20000,
"client": "furion",
"profiler": true
}
""");

JSON 配置字段说明:

  • 必填字段
    • url:请求地址(string 类型,可为 null
    • method:请求方式(string 类型,不可为 null
  • 可选字段
    • baseAddress:请求基地址(string 类型,可为 null
    • headers:请求头(object 类型,可为 null
    • queries:查询参数(object 类型,可为 null
    • cookiesCookiesobject 类型,可为 null
    • timeout:超时时间(number 类型,单位为毫秒,可为 null
    • clientHttpClient 实例名称(string 类型,可为 null
    • profiler:是否启用请求分析工具(boolean 类型,可为 null
  • 请求内容
    • contentType:内容类型(string 类型,不可为 null若存在 data 字段时必须提供
    • data:请求内容(any 类型,可为 null
    • encoding:内容编码(string 类型,可为 null
  • 多部分表单
    • multipart:多部分表单内容(object 类型,可为 null

19.3.2 设置请求地址

HttpRequestBuilder 类型提供的静态方法中,您可以配置请求的地址。以下展示了如何使用 HttpRequestBuilder 类型静态方法来定义不同的请求地址:

// 使用完整 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 合并,以生成最终的请求地址。例如:
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 类型的地址设置。

19.3.3 方法命名原则

在设计 HttpRequestBuilder 对象的方法时,我们遵循了一套明确的命名规则,以确保方法的功能和行为直观易懂。具体来说,所有只能进行操作的方法均以 SetUse 开头,而所有支持重复调用、进行叠加操作的方法则以 WithAdd 开头。

  • SetUse 开头的方法:这类方法用于设置某个属性或参数,若重复调用,则后一次调用会覆盖前一次的设置。例如,SetTraceIdentifier(traceId) 方法,在多次调用时,只有最后一次调用的 traceId 会生效。
  • WithAdd 开头的方法:这类方法用于添加或修改某些内容,且支持重复调用。在重复调用时,它们不会覆盖之前的设置,而是采用叠加的方式。例如,WithHeader(key, value) 方法,在多次调用时,会保留之前的所有头部信息,并添加新的头部信息。

这样的命名原则使得 HttpRequestBuilder 对象的方法更加清晰易懂,便于开发者在使用时快速理解每个方法的功能和行为。

19.3.4 设置跟踪标识

为请求指定一个唯一标识符,便于跟踪和调试。该标识符将被设置在 X-Trace-ID 请求标头中。

HttpRequestBuilder.Get("https://furion.net/")
.SetTraceIdentifier("your-id");

19.3.5 设置内容类型

指定请求的内容类型。

HttpRequestBuilder.Get("https://furion.net/")
.SetContentType("text/plain");

HttpRequestBuilder.Get("https://furion.net/")
.SetContentType("text/plain; charset=utf-8"); // 支持指定字符集

19.3.6 设置内容编码

设置请求的内容编码。

HttpRequestBuilder.Get("https://furion.net/")
.SetContentEncoding(Encoding.UTF8);

HttpRequestBuilder.Get("https://furion.net/")
.SetContentEncoding("utf-8"); // 支持编码字符串
内容编码使用说明

• 当设置内容编码时,系统会自动在 Content-Type 后追加 ;charset=编码 参数。例如:

  • 原始 Content-Typeapplication/json
  • 设置编码为 utf-8 后,最终变为:application/json;charset=utf-8

• 注意事项:

  1. 部分第三方服务器可能不支持带 charset 参数的 Content-Type,这会导致请求失败。
  2. 若无特殊需求,建议保持默认不设置内容编码

19.3.7 设置 JSON 内容 ✨

将请求的内容类型设置为 application/json 并发送 JSON 数据。

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 格式有效性)
推荐使用【原始字符串字面量】设置 JSON

推荐使用原始字符串字面量来设置 JSON 数据。在 C# 11 中,新增了这一特性,允许使用三个双引号(""")包裹的字符串来包含多行文本,同时字符串内的转义字符(如 \n\t 等)将作为普通字符处理,无需转义。例如:

HttpRequestBuilder.Post("https://furion.net/")
.SetJsonContent("""
{
"id": 1,
"name": "Furion"
}
""");

若需在原始字符串中插入变量,只需在首个 """ 前添加 $$,并使用 {{变量名}} 模板来占位。例如:

var val = "Furion";

HttpRequestBuilder.Post("https://furion.net/")
.SetJsonContent($$"""
{
"id": 1,
"name": "{{val}}"
}
""");

使用原始字符串字面量设置 JSON 数据,可以简化代码,避免处理转义符的繁琐操作。

自定义 JSON 属性名称

当对象序列化为 JSON 时,属性默认采用小驼峰命名(CamelCase)。若需自定义序列化后的属性名称,可在属性上添加 [JsonPropertyName("自定义名称")] 特性。

JSON 字符串说明

若传入的 JSON 字符串格式无效,将抛出 JsonException 异常。若无需校验格式,可使用 SetJsonContentWithoutValidation 方法。

19.3.8 设置 HTML 内容

将请求的内容类型设置为 text/html 并发送 HTML 数据。

HttpRequestBuilder.Post("https://furion.net/")
.SetHtmlContent("<html></html>");

HttpRequestBuilder.Post("https://furion.net/")
.SetHtmlContent("<html></html>", Encoding.UTF8); // 设置编码

HttpRequestBuilder.Post("https://furion.net/")
.SetHtmlContent("<html></html>", Encoding.UTF8, "application/html"); // 自定义 content-type
推荐使用【原始字符串字面量】设置 HTML

参考【19.3.7 设置 JSON 内容】。

19.3.9 设置 XML 内容

将请求的内容类型设置为 text/xml 并发送 XML 数据。

HttpRequestBuilder.Post("https://furion.net/")
.SetXmlContent("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");

HttpRequestBuilder.Post("https://furion.net/")
.SetXmlContent("<?xml version=\"1.0\" encoding=\"UTF-8\"?>", Encoding.UTF8); // 设置编码

HttpRequestBuilder.Post("https://furion.net/")
.SetXmlContent("<?xml version=\"1.0\" encoding=\"UTF-8\"?>", Encoding.UTF8, "application/soap+xml"); // 自定义 content-type
推荐使用【原始字符串字面量】设置 XML

参考【19.3.7 设置 JSON 内容】。

19.3.10 设置文本内容

将请求的内容类型设置为 text/plain 并发送纯文本数据。

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
推荐使用【原始字符串字面量】设置文本

参考【19.3.7 设置 JSON 内容】。

19.3.11 设置原始 raw 字符串内容

在诸如 Postman 等现代 API 测试工具中,用户可以通过 raw 数据格式发送请求。在 ASP.NET Core 服务端应用程序中,这通常表现为接收一个标记有 [FromBody] 特性的字符串参数(例如 str):

[HttpPost]
// [Consumes("application/json")]
public string AddBodyString([FromBody] string str)
{
return str;
}

为了设置原始字符串内容,可以采用如下方法:

HttpRequestBuilder.Post("https://furion.net/")
.SetRawStringContent("Furion", "application/json"); // 内容类型必填

HttpRequestBuilder.Post("https://furion.net/")
.SetContent("\"Furion\"", "application/json"); // 等价 SetRawStringContent 方式调用
原始字符串格式

请注意,在调用 SetRawStringContent(text, contentType) 方法时,所传递的字符串内容会自动被双引号包裹后发送。举例来说,若输入字符串为 Furion,则实际发送的内容将是 "Furion"

19.3.12 设置 URL 编码表单内容

将请求的内容类型设置为 application/x-www-form-urlencoded 并发送表单数据。

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" }, useUrlEncode: false); // 可配置不进行 URL 编码处理
URL 编码表单内容说明
  • 默认情况下,URL 编码表单通过 FormUrlEncodedContent 类型进行构建,但此类型不支持自定义请求内容编码,它默认使用 Encoding.Latin1 而不是 UTF-8 这可能在提交到某些接口时引发异常。 为解决此问题,可以通过设置参数 useStringContenttrue 来采用 StringContent 方式构建表单数据,从而允许自定义编码为 UTF-8
    var content = await httpRemoteService.PostAsAsync<YourRemoteModel>("https://localhost:7044/HttpRemote/AddURLForm",
    builder => builder
    .SetFormUrlEncodedContent(new { id = 1, name = "furion" }, useStringContent: true));
  • 某些服务器要求显式声明字符集(charset),此时可通过 contentEncoding 参数指定编码方式,例如使用 UTF-8: 此设置在发送远程请求时会生成如下 Content-Type 请求头:application/x-www-form-urlencoded; charset=UTF-8
    var content = await httpRemoteService.PostAsAsync<YourRemoteModel>("https://localhost:7044/HttpRemote/AddURLForm",
    builder => builder
    .SetFormUrlEncodedContent(new { id = 1, name = "furion" }, Encoding.UTF8));

19.3.13 设置请求内容

支持设置任意类型的请求内容。

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"}, "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<byte>(...), "application/octet-stream");

HttpRequestBuilder.Post("https://furion.net/")
.SetContent(new MultipartContent(), "multipart/form-data");
未提供 Content-Type 时的默认行为

当未指定 Content-Type 时,框架会根据内容类型自动设置 Content-Type,具体规则如下:

  • JsonContent 类型:自动设置为 application/json
  • FormUrlEncodedContent 类型:自动设置为 application/x-www-form-urlencoded
  • byte[]StreamByteArrayContentStreamContentReadOnlyMemoryContentReadOnlyMemory<byte> 类型:自动设置为 application/octet-stream
  • MultipartContent 类型:自动设置为 multipart/form-data

如果以上类型均不匹配,则默认回退为 text/plain。如需修改此回退值,可通过以下代码配置:

services.AddHttpRemote(builder => {})
.ConfigureOptions(options =>
{
// 设置默认的请求内容类型
options.DefaultContentType = "application/json";
});

配置后,当以上类型均不匹配时,默认 Content-Type 将被设置为 application/json

小提示

SetJsonContentSetHtmlContentSetXmlContentSetTextContentSetRawStringContentSetFormUrlEncodedContent 方法均在内部调用了 SetContent 方法。

19.3.14 设置多部分表单内容 ✨

将请求的内容类型设置为 multipart/form-data 并发送多部分表单内容。

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 方法会覆盖其他内容设置方法(SetJsonContentSetHtmlContentSetXmlContentSetTextContentSetRawStringContentSetFormUrlEncodedContentSetContent)。

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),此时可通过该属性实现一些复杂的业务逻辑处理。

19.3.15 设置请求标头

添加或修改请求标头。

HttpRequestBuilder.Get("https://furion.net/")
.WithHeader("X-Header", "X-Value") // 添加单个标头
.WithHeaders(new Dictionary<string, object?> { }) // 添加多个标头
.WithHeaders(new { id = 1, name = "Furion" }); // 添加多个标头,支持多种键值类型

若存在重复的请求标头,它们将被合并,并用逗号加空格(, )分隔多个值。通过设置 replace: true 参数,可以覆盖先前的请求标头设置。

使用 HeaderNames 静态类设置 HTTP 标头

在配置 HTTP 请求或响应的标准标头时,如 Authorization手写标头名称可能会因拼写错误而导致问题

为避免此类错误,并充分利用集成开发环境(IDE)的智能提示功能,推荐使用 HeaderNames 静态类。例如,使用 HeaderNames.AuthorizationHeaderNames.UserAgent 可以确保标头名称的准确性,并提升代码的可读性和维护性。

配置参数支持

请求标头支持配置参数,用于读取配置信息进行替换操作。配置参数使用 [[key]] 语法。

19.3.16 设置移除的请求标头

移除指定的请求标头。

HttpRequestBuilder.Get("https://furion.net/")
.RemoveHeaders("User-Agent", "Host", "Accept"); // 移除多个标头

在发送 HTTP 请求之前,将移除配置中指定的待移除请求标头集合。也就是说,RemoveHeaders 方法会在所有 WithHeader[s] 方法调用之后执行。

19.3.17 设置片段标识符

URL 中添加片段标识符。

HttpRequestBuilder.Get("https://furion.net/")
.SetFragment("About"); // 生成 URL: https://furion.net/#About

HttpRequestBuilder.Get("https://furion.net/")
.SetFragment("#About"); // 支持以 # 符号开头

19.3.18 设置超时时间

为单次请求设置超时时长。

HttpRequestBuilder.Get("https://furion.net/")
.SetTimeout(TimeSpan.FromSeconds(2));

HttpRequestBuilder.Get("https://furion.net/")
.SetTimeout(2000); // 毫秒单位

HttpRequestBuilder.Get("https://furion.net/")
.SetTimeout(-1); // 永不超时,或使用 .WithoutTimeout()

HttpRequestBuilder.Get("https://furion.net/")
.SetTimeout(0); // 立即取消请求

HttpRequestBuilder.Get("https://furion.net/")
.SetTimeout(TimeSpan.FromSeconds(2), () => // 支持超时回调
{
// 超时发生时要执行的操作
});

HttpRequestBuilder.Get("https://furion.net/")
.SetTimeout(2000, () => // 支持超时回调
{
// 超时发生时要执行的操作
});
HttpClient 超时时间说明

HttpClient 中设置超时时间时,请确保单次请求的超时时间不超过 HttpClient 配置的超时时间。例如,如果 HttpClient 超时时间设置为 10 分钟,而单次请求的超时时间设为 15 分钟,那么单次请求在超过 10 分钟时仍会触发超时异常。示例代码如下:

services.AddHttpClient(string.Empty, client =>
{
client.Timeout = TimeSpan.FromMinutes(10); // 默认超时时间为 100 秒,需显式设置
// client.Timeout = System.Threading.Timeout.InfiniteTimeSpan; // 永不超时
});

因此,若单次请求需要更长的超时时间,请确保 HttpClient 的超时时间设置得相应更长。

19.3.19 设置路径片段

添加 URL 路径片段。

HttpRequestBuilder.Get("https://furion.net/")
.WithPathSegment("user") // 添加单个路径片段
.WithPathSegments(["detail", "edit"]) // 添加多个路径片段

生成的最终 URL 为:https://furion.net/user/detail/edit

若存在重复的路径片段,它们将在后续追加中重复出现(如:/docs/docs/users/docs/)。

19.3.20 设置移除的路径片段

移除指定的路径片段。

HttpRequestBuilder.Get("https://furion.net/docs/login/users")
.RemovePathSegments("docs", "user"); // 移除多个路径片段

生成的最终 URL 为:https://furion.net/login

在发送 HTTP 请求之前,将移除配置中指定的待移除路径片段集合。也就是说,RemovePathSegments 方法会在所有 WithPathSegment[s] 方法调用之后执行。

19.3.21 设置查询参数(URL 参数)

添加或修改 URL 查询参数。

HttpRequestBuilder.Get("https://furion.net/")
.WithQueryParameter("id", 1) // 添加单个参数
.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<string, object?> { }) // 添加多个参数
.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<string, object?> { { "str1", null }, {"str2", "test" } }, ignoreNullValues: true); // 忽略空值

若存在重复的查询参数键,它们将合并成多个键值对(如 key1=value1&key1=value2)。通过设置 replace: true 参数,可以覆盖先前的查询参数。默认情况下,值为 null 的查询参数会被添加到 URL 中;若需忽略这些参数,可设置 ignoreNullValues: true

URL 参数格式化程序

在设置 HTTP 请求的查询参数时,所有参数值最终都会通过调用 ToString() 方法转换为字符串,并追加到 URL 中。然而,这种默认处理方式对某些类型(如 DateTime)可能并不理想,无法满足实际需求。

在这种情况下,可以通过自定义 URL 参数格式化程序来实现更精确的控制。例如,下面的 CustomUrlParameterFormatter 类继承自 UrlParameterFormatter,并重写了 Format 方法,以实现对 DateTime 类型的特殊格式化处理:

public class CustomUrlParameterFormatter : UrlParameterFormatter
{
/// <inheritdoc />
public override string? Format(object? value, UrlFormattingContext context)
{
if (value is DateTime dateTime)
{
return dateTime.ToString("yyyyMMdd");
}

return base.Format(value, context);
}
}

完成自定义格式化程序后,可以在配置 HttpRemoteOptions 时将其注册为默认的 URL 参数格式化器:

services.AddHttpRemote(builder => {})
.ConfigureOptions(options =>
{
options.UrlParameterFormatter = new CustomUrlParameterFormatter();
});

如此一来,在构建 URL 查询参数时,若遇到 DateTime 类型的值,框架将自动将其格式化为 yyyyMMdd 格式的字符串,从而确保输出符合预期。

19.3.22 设置移除的查询参数

移除指定的查询参数。

HttpRequestBuilder.Get("https://furion.net/")
.RemoveQueryParameters("id", "name", "age"); // 移除多个参数

在发送 HTTP 请求之前,将移除配置中指定的待移除查询参数集合。也就是说,RemoveQueryParameters 方法会在所有 WithQueryParameter[s] 方法调用之后执行。

19.3.23 设置路径参数(模板/配置参数)

URL 路径中替换对象模板字符串。

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<string, object?> { }) // 添加多个路径参数
.WithPathParameters(new { id = 1, name = "Furion" }) // 添加多个路径参数,{id} 和 {name} 分表被替换为 1 和 Furion
.WithPathParameters(new { id = 1, name = "Furion" }, "user") // 添加带前缀的参数,{user.id} 和 {user.name} 分别被替换为 1 和 Furion

若路径参数键出现重复,则后设置的键值会覆盖先前的设置。


配置参数

除了通过 {key} 模板语法设置路径参数外,框架还提供了配置参数,用于读取配置信息进行替换操作。配置参数使用 [[key]] 语法,例如:

HttpRequestBuilder.Get("https://furion.net?id=[[id]]&name=[[name]]");

启用配置参数支持

要在 HttpRemote 服务中启用配置参数支持,请按照以下步骤进行配置:

services.AddHttpRemote(builder => {})
.ConfigureOptions(options =>
{
// 设置用于替换 URL 地址中配置模板参数的提供源
options.Configuration = builder.Configuration; // 若使用 Furion 框架可直接设置 App.Configuration
});

配置参数的使用

配置参数将从您的配置文件中读取并替换到 URL 中。例如,您的配置文件可能如下所示:

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]]:如果 notfoundbak 都不存在,则查找 other
    • [[notfound | bak:sub | other:sub:nest]]:支持更深层次的备用查找。
  • 默认值:
    • [[notfound || default]]:如果 notfound 不存在,则使用 default 作为值。
    • [[notfound | bak | other || 默认值]]:结合备用查找和默认值,确保总有值可用。

添加或修改 Cookie

HttpRequestBuilder.Get("https://furion.net/")
.WithCookie("id", 1) // 设置单个 Cookie,生成:id=1
.WithCookie("name", new[] { "furion", "monksoul" }) // 添加多个值,生成:name=furion,monksoul
.WithCookie("DeviceId=; ASP.NET_SessionId=dr1kcfupurtqpk42dzhwvsvq; CookieLastUName=sh") // 支持 Cookie 标头值字符串
.WithCookies(new Dictionary<string, object?> { }) // 设置多个 Cookie
.WithCookies(new { id = 1, name = "Furion" }); // 设置多个 Cookie,生成:id=1; name=Furion

Cookie 键出现重复,则后设置的键值会覆盖先前的设置。

配置参数支持

Cookie 值支持配置参数,用于读取配置信息进行替换操作。配置参数使用 [[key]] 语法。

19.3.25 设置移除的 Cookies

移除指定的 Cookie

HttpRequestBuilder.Get("https://furion.net/")
.RemoveCookies("id", "name", "age"); // 移除多个 Cookie

在发送 HTTP 请求之前,将移除配置中指定的待移除 Cookie 键集合。也就是说,RemoveCookies 方法会在所有 WithCookie[s] 方法调用之后执行。

19.3.26 设置 HttpClient 实例的名称(多个基地址)

系统默认使用 IHttpClientFactory 创建 HttpClient 实例,并将默认客户端名称设为空字符串(string.Empty)。您可以通过指定方式设置创建 HttpClient 实例时的客户端名称。

HttpRequestBuilder.Get("https://furion.net/")
.SetHttpClientName(string.Empty); // 使用默认客户端(通常无需显式设置)

HttpRequestBuilder.Get("https://furion.net/")
.SetHttpClientName("weixin"); // 指定为名为 "weixin" 的客户端

您还可以在 Startup.csProgram.cs 文件中为命名 HttpClient 客户端提供配置:

// 配置默认客户端(名称为空字符串)
services.AddHttpClient(string.Empty, client => { });

// 配置名为 "weixin" 的客户端
services.AddHttpClient("weixin", client => { });

19.3.27 设置响应内容的最大缓存大小

为单次请求配置响应内容的最大缓存字节数。

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 异常。示例代码如下:

services.AddHttpClient(string.Empty, client =>
{
client.MaxResponseContentBufferSize = 5 * 1024;
});

因此,若单次请求需要更大的响应内容的最大缓存字节数,请确保 HttpClient 的响应内容的最大缓存字节数设置得相应更大。

19.3.28 设置 HttpClient 实例提供器

系统默认通过 IHttpClientFactory 来创建并自动管理 HttpClient 实例的生命周期。如果需要手动管理 HttpClient 的生命周期,可以针对单次请求单独配置一个 HttpClient 实例。

HttpRequestBuilder.Get("https://furion.net/")
.SetHttpClientProvider(() => (new HttpClient(), client => client.Dispose()));

SetHttpClientProvider 方法说明:

  • 参数类型为 Func<(HttpClient, Action<HttpClient>?)> 委托。
  • 该委托返回一个元组,其中第一个元素是 HttpClient 实例,用于发起请求。
  • 第二个元素(可选)是一个委托,用于在请求完成后释放 HttpClient 实例。

在请求发起前,系统会调用此委托(若存在)并获取 HttpClient 实例进行请求;请求完成后,如果提供了释放委托,则调用该委托以释放 HttpClient 实例。

19.3.29 添加请求内容处理器

IHttpContentProcessor 接口定义了如何根据请求的内容类型或原始类型构建 HttpContent 实例。

HttpRequestBuilder.Post("https://furion.net/")
.AddHttpContentProcessors(() => [ new StringContentProcessor() ]); // 可添加多个处理器

19.3.30 添加响应内容转换器

IHttpContentConverter 接口指定了如何将响应内容 HttpResponseMessage 转换为目标类型的实例。

HttpRequestBuilder.Post("https://furion.net/")
.AddHttpContentConverters(() => [ new StringContentConverter() ]); // 可添加多个转换器

19.3.31 设置添加请求内容前的操作

在将 HttpContent 实例设置给 HttpRequestMessage 对象的 Content 属性之前,您可以执行一些额外的预处理操作。

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 方法支持多次调用,每次调用的结果会累积叠加。

19.3.32 设置发送请求前的操作

在发送 HTTP 远程请求之前,您可以执行一些预处理操作。

HttpRequestBuilder.Post("https://furion.net/")
.SetOnPreSendRequest(requestMessage =>
{
// 示例:添加名为 "header1" 的请求标头
requestMessage.Headers.TryAddWithoutValidation("header1", "value1");
});

注意SetOnPreSendRequest 方法支持多次调用,每次调用的结果会累积叠加。

19.3.33 设置收到响应后的操作

在接收到 HTTP 响应之后,您可以执行一些后续处理操作。

HttpRequestBuilder.Post("https://furion.net/")
.SetOnPostReceiveResponse(responseMessage =>
{
// 示例:打印响应状态码
Console.WriteLine(responseMessage.StatusCode);
});

19.3.34 设置发送请求失败时的处理

HTTP 请求发送过程中发生异常时,您可以执行一些错误处理操作。

HttpRequestBuilder.Post("https://furion.net/")
.SetOnRequestFailed((exception, responseMessage) => // 注意:responseMessage 可能为空
{
// 示例:打印异常信息
Console.WriteLine(exception.Message);
});

注意:可将 SetOnRequestFailed 与异常抑制方法 SuppressExceptions() 配合使用,以便在不中断程序流程的前提下捕获并处理请求失败信息。

推荐使用 WithStatusCodeHandler 方式

推荐使用第 19.3.46 章节的 WithStatusCodeHandler 设置响应状态码处理程序方法。当需要针对特定状态码(如服务器异常状态码,即大于 500)进行处理时,可以这样做:

HttpRequestBuilder.Get("https://furion.net/")
// 表示状态码大于等于 500 配置回调处理
.WithStatusCodeHandler(">=500", async (responseMessage, cancellationToken) =>
{
Console.WriteLine("调用状态码处理程序");
})

19.3.35 确保请求成功

启用该功能后,当 HTTP 响应的状态码不在 200-299 范围内时(即 IsSuccessStatusCode 属性为 false),将自动抛出异常。

HttpRequestBuilder.Post("https://furion.net/")
.EnsureSuccessStatusCode();

HttpRequestBuilder.Post("https://furion.net/")
.EnsureSuccessStatusCode(false); // 关闭验证

19.3.36 设置 Basic 身份验证

向请求中添加 Authorization 标头,其值为 Basic 关键字后接由 用户名:密码 字符串的 Base64 编码组成。

HttpRequestBuilder.Post("https://furion.net/")
.AddBasicAuthentication("username", "password");
Authorization 标头值格式

Authorization 标头的值遵循 Schema 值 的格式,即 Basic 后紧跟一个空格,然后是经过 Base64 编码的 用户名:密码 字符串。

19.3.37 设置 JWT 身份验证

向请求中添加 Authorization 标头,格式为 Bearer 关键字后接 JWT Token 字符串组成。

HttpRequestBuilder.Post("https://furion.net/")
.AddJwtBearerAuthentication("your-jwt-token");
Authorization 标头值格式

Authorization 标头的值遵循 Schema 值 的格式,即 Bearer 后紧跟一个空格,然后是 JWT Token 字符串。

19.3.38 设置 Digest 摘要身份验证

向请求中添加 Authorization 标头,格式为 Digest 关键字后接由用户名和密码生成的摘要字符串组成。

HttpRequestBuilder.Post("https://furion.net/")
.AddDigestAuthentication("username", "password");
Authorization 标头值格式

Authorization 标头的值遵循 Schema 值 的格式,即 Digest 后紧跟一个空格,然后是用户名和密码生成的摘要字符串。

19.3.39 设置自定义身份验证

向请求中添加自定义的 Authorization 标头,遵循 Schema 值 的格式。

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)。

19.3.40 禁用 HTTP 缓存

在发送 HTTP GET 请求时,服务器可能会缓存该请求的结果以提高性能。为了取消其缓存行为,可以在添加以下操作:

HttpRequestBuilder.Get("https://furion.net/")
.DisableCache();

HttpRequestBuilder.Get("https://furion.net/")
.DisableCache(false); // 启用缓存(默认)

在添加该操作之后,HTTP 请求将在发送前自动附带以下请求标头,以确保缓存控制:

Cache-Control: must-revalidate, no-cache, no-store
Pragma: no-cache
If-None-Match: ""

19.3.41 设置请求处理程序

IHttpRequestEventHandler 接口允许您定义 HTTP 请求的预处理操作。通过实现该接口,您可以创建自定义的请求处理程序,例如 CustomRequestEventHandler 类:

public class CustomRequestEventHandler : IHttpRequestEventHandler
{
// 在发送 HTTP 请求之前的操作
public void OnPreSendRequest(HttpRequestMessage httpRequestMessage) {}

// 在收到 HTTP 响应之后的操作
public void OnPostReceiveResponse(HttpResponseMessage httpResponseMessage) {}

// 当发送 HTTP 请求发生异常时的操作
public void OnRequestFailed(Exception exception, HttpResponseMessage? httpResponseMessage = null) {}
}

要在应用程序中启用此处理程序,请在 Startup.csProgram.cs 文件中注册 CustomRequestEventHandler 服务:

services.TryAddTransient<CustomRequestEventHandler>();

接下来,您可以在构建 HTTP 请求时指定此处理程序:

HttpRequestBuilder.Get("https://furion.net/")
.SetEventHandler<CustomRequestEventHandler>();

HttpRequestBuilder.Get("https://furion.net/")
.SetEventHandler(typeof(CustomRequestEventHandler)); // 使用类型方式设置
复用提示

您可以创建自定义的 IHttpRequestEventHandler 接口实现类型,并在多个 HttpRequestBuilder 实例中复用该实现。

触发时机说明

HttpRequestBuilder 实例配置了 SetOnPreSendRequestSetOnPostReceiveResponseSetOnRequestFailed 方法时,这些回调方法将会被触发。

如果同时实现了 IHttpRequestEventHandler 接口,其方法(OnPreSendRequestOnPostReceiveResponseOnRequestFailed)的调用时机将晚于 HttpRequestBuilder 实例设置的系列方法。

19.3.42 启用 HttpClient 池化管理

默认情况下,HttpClient 实例会在每次发送 HTTP 请求时被新建。但在需要频繁请求的场景中,这种做法可能引发性能瓶颈和内存占用过高的问题,特别是在压力测试期间。为了优化性能,我们可以启用 HttpClient 池化管理,以便在请求过程中复用 HttpClient 实例。

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() 方法来释放资源,以防止内存溢出。

19.3.43 添加请求结束时需释放的资源

内存安全是每位程序开发者必须高度重视的问题。在发送 HTTP 请求的过程中,有时需要引入未托管的资源,例如,在发送文件时,需要从本地读取文件并以流的形式发送。这种情况下,若处理不当,可能会遇到流资源无法释放的问题。

为解决这一问题,我们可以添加在请求结束后,自动处理这些需要释放的资源:

// 打开文件并读取文件流(没有 using)
var fileStream = File.OpenRead(@"C:\Workspaces\httptest.jpg");

var httpRequestBuilder = HttpRequestBuilder.Post("https://furion.net/")
.SetContent(fileStream); // 设置请求内容
.AddDisposable(fileStream); // 添加请求结束需要释放的资源

// 发送请求
var responseMessage = await httpRemoteService.SendAsync(httpRequestBuilder);

// 此时,fileStream 已自动释放。✅

AddDisposable 方法可以接受任何实现了 IDisposable 接口的对象作为参数,并且支持重复调用,每次调用都会将新的 IDisposable 对象添加到集合中。

19.3.44 管理和释放资源

请参考第 19.3.421.9.3.43 章节内容,了解如何在请求结束时释放资源以避免内存泄漏。

// 释放资源以避免内存泄漏
httpRequestBuilder.ReleaseResources();

以下是 ReleaseResources 方法的底层实现代码,它负责管理和释放与 HTTP 请求相关的所有资源:

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();
}

19.3.45 模拟浏览器环境(爬虫检测)

在开发爬虫程序时,目标网站可能会根据用户代理(User-Agent)或其他因素提供不同的页面版本,如 PC 端和移动端。此外,一些网站还具备反爬虫机制,能够识别并阻止爬虫程序的访问。为应对这些问题,我们可以配置请求标头以模拟真实的浏览器环境进行请求。

HttpRequestBuilder.Get("https://www.baidu.com/")
.SimulateBrowser(); // 模拟 PC 浏览器环境

HttpRequestBuilder.Get("https://www.baidu.com/")
.SimulateBrowser(simulateMobile: true); // 模拟移动端浏览器环境

在添加该操作之后,HTTP 请求将在发送前自动附带以下请求标头,以确保服务器能够准确识别并处理请求:

# 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

19.3.46 添加响应状态码处理程序

在发送 HTTP 请求并接收响应时,我们经常需要根据不同的响应状态码执行特定的操作。为了实现这一需求,HttpRequestBuilder 提供了 WithStatusCodeHandler 方法,允许我们为特定的状态码配置回调处理逻辑。

以下是如何使用 WithStatusCodeHandler 方法的示例代码:

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 请求与响应的处理能力。

19.3.47 启用请求分析工具

在现代化的浏览器中,通常内置了开发者工具,这些工具能够捕获并直观展示用户访问网站时的所有请求与响应数据。类似地,我们也为 HTTP 远程请求模块提供了一套分析工具。

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 远程请求时,控制台将输出如下详细信息:

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/
HTTP Method: GET
Status Code: 200 OK
HTTP Version: 1.1
HTTP Content:
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 中启用:

// 为默认客户端启用
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 随之移动到尾部。

补充说明: 请求分析工具默认仅展示请求或响应体中最多 8KB 的内容数据。

19.3.48 设置客户端偏好的语言和区域

全球化是互联网应用产品的发展趋势,因此,面向全球的应用产品应具备国际化功能。发送 HTTP 请求时,可通过添加 Accept-Language 标头来指定客户端偏好的自然语言和区域。

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");

19.3.49 设置 HttpRequestMessage 属性

在特定场景下,我们可能需要为 HttpRequestMessage 请求添加额外的属性,而非通过请求标头。这时,可以如下操作:

HttpRequestBuilder.Get("https://furion.net")
.WithProperty("key1", "vallue2") // 设置单个属性
.WithProperties(new Dictionary<string, object?> {}) // 设置多个属性
.WithProperties(new { id = 1, name = "Furion" }); // 设置多个属性

这些属性会被添加到 HttpRequestMessage 对象的 Options 属性中(参考文档)。要获取这些值,可以这样做:

httpRequestMessage.Options.TryGetValue(new HttpRequestOptionsKey<string>("key1"), out var value);

若属性键出现重复,则后设置的键值会覆盖先前的设置。

小提示

此功能常被集成在自定义的 DelegatingHandlerIHttpRequestEventHandler 组件中。

19.3.50 启用性能优化

为了提升应用通过 HTTP 客户端发送网络请求的性能,可以通过配置默认的 HTTP 头部来优化数据传输效率。框架提供了一键式配置方法,方便快速统一设置这些头部:

HttpRequestBuilder.Get("https://furion.net")
.PerformanceOptimization();

HttpRequestBuilder.Get("https://furion.net")
.PerformanceOptimization(false); // 关闭性能优化

此外,除了为单个请求启用性能优化配置,还可以全局注册以在 HttpClient 中启用:

// 为默认客户端启用
services.AddHttpClient(string.Empty, client =>
{
client.PerformanceOptimization();
});

services.AddHttpRemote();

启用性能优化后,请求将自动添加以下头部,以提升传输效率:

  • Accept*/*(接受任意类型的响应内容)
  • Accept-Encodinggzip, deflate, br(支持的内容编码格式,以提高传输效率)
  • Connectionkeep-alive(保持连接,减少 TCP 连接建立和关闭的开销)
重要提示

当需要返回 Stream 内容或进行 HttpContext 网页转发时,请勿启用此配置,因为流会因压缩而变得不可读,同时该配置也不适用于网页转发的场景。

19.3.51 设置自动 Host 标头

Host 标头是 HTTP/1.1 协议中的一个必需标头。Host 标头用于指定请求的目标服务器的主机名和端口号,确保服务器能正确区分同一 IP 地址上的不同域名并进行相应处理。框架提供了简便的方法进行设置:

HttpRequestBuilder.Get("https://furion.net")
.AutoSetHostHeader(); // 启用

HttpRequestBuilder.Get("https://furion.net")
.AutoSetHostHeader(false); // 关闭自动 Host 标头

启用后,发送 HTTP 远程请求时会自动添加 Host: furion.net 标头。

小提示

当对接旧程序提供的 API 接口时,建议启用该配置以提升兼容性。

HttpClient 自动重定向导致的 Host 问题

在发送 HTTP 远程请求时,如果目标服务器返回重定向响应(如 301 Moved Permanently302 Found),框架默认会自动跟随重定向。然而,当启用自动 Host 标头时,可能会遇到无法更新 Host 标头的问题。此时,可以关闭 AllowAutoRedirect 选项,使框架能够正确处理重定向:

// 配置默认客户端
services.AddHttpClient(string.Empty)
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
AllowAutoRedirect = false
});

另外,如需设置框架内置重定向行为的最大重定向次数,可以使用以下方式:

services.AddHttpRemote(builder => {})
.ConfigureOptions(options =>
{
MaximumAutomaticRedirections = 20;
});

这样,可以确保重定向行为符合预期,同时避免 Host 标头设置错误的问题。

19.3.52 配置请求基地址

当需要对接多个第三方 API 时,我们通常会全局注册并配置多个 HttpClient 实例的 BaseAddress。例如:

// 配置默认客户端的基地址
services.AddHttpClient(string.Empty, client =>
{
client.BaseAddress = new Uri("https://furion.net/");
});

// 配置GitHub客户端的基地址
services.AddHttpClient("github", client =>
{
client.BaseAddress = new Uri("https://github.com/");
});

此外,框架还提供了局部设置基地址的方法,允许在构建请求时动态指定:

// 使用字符串设置基地址
HttpRequestBuilder.Get("/api/test")
.SetBaseAddress("https://furion.net");

// 使用 Uri 对象设置基地址
HttpRequestBuilder.Get("/api/test")
.SetBaseAddress(new Uri("https://furion.net"));
特别说明

请确保设置的请求基地址是绝对路径,即以 http://https:// 开头。

处理逻辑说明

  • 若请求地址为绝对路径,则直接使用该地址发送请求。
  • 若请求地址为相对路径且未设置局部 BaseAddress,则拼接全局 HttpClient 实例的 BaseAddress 作为请求地址。
  • 若请求地址为相对路径且已设置局部 BaseAddress,则拼接该局部 BaseAddress 作为请求地址。
配置参数支持

请求基地址支持配置参数,用于读取配置信息进行替换操作。配置参数使用 [[key]] 语法。

19.3.53 配置来源地址(防盗链)

当访问某些第三方服务器时,服务器可能会验证请求头中的 Referer 来源地址。例如在下载图片时,可能因触发防盗链机制导致获取的图片不符合预期。此时,可通过 SetReferer 方法设置 Referer 请求头,模拟来源页面以绕过防盗链检测。

HttpRequestBuilder.Get("https://furion.net/logo.png")
.SetReferer("https://furion.net/"); // 伪造是从首页发出的请求

为简化配置,框架提供了内置模板字符串 "{BASE_ADDRESS}",可自动提取请求地址的基地址作为 Referer

HttpRequestBuilder.Get("https://furion.net/logo.png")
.SetReferer("{BASE_ADDRESS}"); // 发送时自动替换 {BASE_ADDRESS} 为 https://furion.net/

19.3.54 配置 HTTP 版本

在发起 HTTP 远程请求时,默认采用的 HTTP 协议版本为 1.1。不过,在访问部分第三方服务器时,这些服务器可能会对 HTTP 版本进行校验(例如,要求使用 2.0 版本)。此时,可以通过以下两种方式进行设置:

  • 单次请求设置
HttpRequestBuilder.Post("https://furion.net/")
.SetVersion(HttpVersion.Version20); // 推荐
.SetVersion("1.2"); // 重载方法
.SetVersion(new Version("1.2")); // 重载方法
  • 全局配置
// 配置默认客户端
services.AddHttpClient(string.Empty, client =>
{
client.DefaultRequestVersion = HttpVersion.Version10;
});

// 配置特定客户端
services.AddHttpClient("weixin", client =>
{
client.DefaultRequestVersion = HttpVersion.Version10;
});

19.3.55 异常抑制机制(静默处理)

在发起 HTTP 远程请求时,可能会遇到以下异常情况:

  • 目标主机不可达
  • 请求被取消
  • 请求超时
  • 其他网络异常

默认情况下,这些异常会中断程序执行。虽然开发者通常使用 try/catch 进行异常处理,但在某些场景下,我们更希望异常发生时静默返回 null 而不中断流程。为此,框架提供了灵活的异常抑制功能。

  • 抑制所有请求异常
var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/")
.SuppressExceptions()); // 抑制所有异常

当请求发生异常时,代码不会中断,而是返回 null,即 httpResponseMessage 的值为 null

在某些场景下,我们希望在抑制异常的同时,仍能捕获异常信息(例如记录到日志中),而不中断程序的正常执行。此时,可以通过 SetOnRequestFailed 回调来实现:

HttpRequestBuilder.Post("https://furion.net/")
.SuppressExceptions()
.SetOnRequestFailed((exception, responseMessage) => // 注意:responseMessage 可能为空
{
Console.WriteLine(exception.Message);
});

该方法允许你在异常被抑制后,安全地处理错误信息,适用于日志记录、监控或其他错误响应逻辑。

  • 仅抑制特定类型的异常

框架还支持仅抑制特定类型的异常。例如,可以仅抑制超时异常和请求取消异常:

var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/")
.SuppressExceptions([typeof(TimeoutException), typeof(TaskCanceledException)])); // 抑制超时和取消异常
  • 禁用异常抑制配置

若需恢复默认行为(即异常发生时中断程序),可显式禁用异常抑制:

var httpResponseMessage = httpRemoteService.SendAsync(HttpRequestBuilder.Post("https://furion.net/")
.SuppressExceptions(false); // 恢复缺省配置

此配置等效于未调用 SuppressExceptions(),当发生任何异常时,程序将中断执行。

注意事项

当启用异常抑制功能时,请注意以下事项:

  1. 覆盖规则
    多次调用 SuppressExceptions() 或相关配置时,仅最后一次调用生效
  2. 状态码检查与异常抑制的优先级
    即使已配置 EnsureSuccessStatusCode(),被抑制的异常仍会返回 null,不会触发状态码检查逻辑。
  3. 异常抑制的优先级
    异常抑制功能的优先级高于状态码检查。如果同时启用状态码检查和异常抑制,异常抑制会优先生效。
  4. 请求拦截器依旧可用
    若通过 SetOnRequestFailed(ex, res) 或其他请求处理机制捕获异常,即使异常被抑制,拦截器或回调方法仍会被调用。
  5. 异常类型选择建议
    应根据具体业务场景谨慎选择需要抑制的异常类型,避免因过度抑制异常而掩盖潜在问题。

19.3.56 移除内容的默认 Content-Type

在与部分较老版本的 HTTP 服务进行对接时,如果发送请求内容时设置了 Content-Type 请求标头,可能会导致请求处理异常。而现代 HTTP 接口通常不会存在此类限制。

若在发送请求时需要移除请求体对应的 Content-Type 请求标头,可以按照以下方式进行设置:

HttpRequestBuilder.Post("https://furion.net")
.SetOmitContentType(true); // 移除内容默认的 Content-Type

除了采用上述方法外,另一种实现方式是利用 SetOnPreSetContent(Action<HttpContent>) 方法,在设置请求内容之前修改请求头,将 Content-Type 置为 null

HttpRequestBuilder.Post("https://furion.net")
.SetOnPreSetContent(httpContent =>
{
httpContent.Headers.ContentType = null;
});

19.3.57 条件化配置构建器

在构建 HTTP 远程请求的构建器实例时,常需根据不同条件动态配置请求参数。例如,当用户执行搜索操作时,应将 ?search=关键字 查询参数添加到请求中;反之,则无需添加。

针对此类场景,HttpRequestBuilder 提供了 When 方法,用于根据指定条件执行相应的配置操作。该方法支持链式调用,使代码更加简洁清晰。

HttpRequestBuilder.Post("https://furion.net")
.When(!string.IsNullOrEmpty(token), b => b.AddJwtBearerAuthentication(token))
.When(!string.IsNullOrEmpty(keyword), b => b.WithQueryParameter("search", keyword));

示例代码说明:

  • token 不为空或 null 时,自动添加 JWT Bearer 认证头。
  • keyword 不为空或 null 时,将 search 查询参数添加到请求中。

该方式可灵活应用于各种条件判断场景,提升代码的可维护性和可读性。

19.3.58 启用断言功能

在开发或编写单元测试、集成测试时,我们经常需要验证响应结果是否符合预期,这一过程通常称为“断言”。例如,判断响应状态码是否为 200,或检查响应头是否包含 Content-Length。若断言失败,系统默认会抛出 HttpAssertionException 异常。

要启用断言功能,需调用 EnableAssertions() 方法,并配合 Asserts(configure) 方法使用:

HttpRequestBuilder.Get("https://furion.net")
.EnableAssertions();

HttpRequestBuilder.Get("https://furion.net")
.EnableAssertions(false); // 禁用断言功能

19.3.59 配置断言逻辑

启用断言功能后,可通过 Asserts(configure) 方法配置具体的断言逻辑。示例如下:

HttpRequestBuilder.Get("https://furion.net")
.EnableAssertions()
.Asserts(ast => ast.StatusCode(200)
.HeaderExists("encoding")
.HeaderEquals("framework", "Furion"));

其中,ast 参数为 HttpAssertionBuilder 类型,内置了以下常用断言方法(支持自定义扩展):

  • AddAssertion(assertion):添加自定义断言委托,如 ast.AddAssertion(async context => await...)
  • StatusCode(statusCode):断言响应状态码等于指定值(整数或 HttpStatusCode
    • 失败时抛出:Expected status code to be {expected}, but found {actual}.
  • StatusCodeIn(allowedStatusCodes):断言状态码在允许列表中
    • 失败时抛出:Expected status code to be one of [{string.Join(", ", allowedStatusCodes)}], but found {actual}.
  • IsSuccessStatusCode():断言请求成功(状态码为 2xx)
    • 失败时抛出:Expected request to be successful (2xx status code), but found status code {(int)context.StatusCode}.
  • ContentContains(expectedSubstring):断言响应内容包含指定子字符串(不区分大小写)
    • 失败时抛出:Expected response content to contain '{expectedSubstring}', but it was not found.
  • HeaderExists(name):断言指定响应头存在(包括内容头)
    • 失败时抛出:Expected response header '{name}' to exist, but it was not found.
  • HeaderEquals(name, expectedValue):断言响应头的第一个值严格等于指定字符串(区分大小写)
    • 失败时抛出:Expected response header '{name}' to be '{expectedValue}', but found '{actualValue}'.
  • HeaderContains(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)}].
  • DurationUnder(maxMilliseconds):断言请求耗时低于指定毫秒数
    • 失败时抛出:Expected request duration to be under {maxDuration.TotalMilliseconds:F2}ms, but it took {actualDuration.TotalMilliseconds:F2}ms.

自定义断言方法

除了内置方法,你还可以通过扩展方法为 HttpAssertionBuilder 添加自定义断言逻辑,以减少重复代码并提升可读性。例如,实现一个 IsJson 方法,用于验证响应内容是否为 application/json 类型:

public static class HttpAssertionBuilderExtensions
{
public static HttpAssertionBuilder IsJson(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,包含以下属性和方法::

  • 属性
    • ResponseMessage:响应消息(HttpResponseMessage 类型)
    • StatusCode:响应状态码(HttpStatusCode 类型)
    • IsSuccessStatusCode:是否请求成功(bool 类型)
    • RequestDuration:请求耗时(毫秒,long 类型)
    • ServiceProvider:服务提供器(IServiceProvider 类型)
  • 方法
    • ReadAsStringAsync():读取响应内容字符串(自动缓存)

使用自定义方法示例:

HttpRequestBuilder.Get("https://furion.net")
.EnableAssertions()
.Asserts(ast => ast.IsJson().StatusCode(200)); // 支持链式调用

借助 C# 扩展方法,你可以灵活扩展 HttpAssertionBuilder 的功能,提升代码的可维护性和复用性。

19.3.60 启用 JSON 响应反序列化包装器

在与第三方 API 进行 HTTP 远程通信时,通常会返回统一结构的 JSON 响应,例如 ApiResult<T> 类型,其中实际数据存放在 Data 属性中:

public class ApiResult<T>
{
public bool Success { get; set; }
public T? Data { get; set; } // 实际返回数据
}

在未启用 JSON 响应反序列化包装器功能时,每次调用都需要显式指定 ApiResult<T> 类型:

var content = await httpRemoteService.SendAsAsync<ApiResult<string>>(
HttpRequestBuilder.Get("https://example.com"));

为简化调用流程,可配置 JSON 响应反序列化包装器,使其自动提取 Data 属性内容:

// 配置默认 HTTP 客户端
services.AddHttpClient(string.Empty)
.ConfigureOptions(options =>
{
options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data));
});

配置完成后,通过调用 JsonResponseWrapping() 启用该功能,之后只需指定目标数据类型,无需重复声明 ApiResult<T>

var content = await httpRemoteService.SendAsAsync<string>(
HttpRequestBuilder.Get("https://example.com").JsonResponseWrapping());

框架将在运行时自动创建 ApiResult<string> 实例,并返回其 Data 属性的值。

也可全局启用 JSON 响应反序列化包装器功能,只需设置 UseJsonResponseWrappingtrue

// 配置默认 HTTP 客户端
services.AddHttpClient(string.Empty)
.ConfigureOptions(options =>
{
options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data));
options.UseJsonResponseWrapping = true;
});

全局启用后,所有请求默认使用包装功能:

var content = await httpRemoteService.SendAsAsync<string>(
HttpRequestBuilder.Get("https://example.com")); // 无需显式调用 JsonResponseWrapping()

若需对特定请求禁用该功能,可调用以下方法:

var content = await httpRemoteService.SendAsAsync<ApiResult<string>>(
HttpRequestBuilder.Get("https://example.com").DisableJsonResponseWrapping()); // 或使用 JsonResponseWrapping(false)

默认情况下,未调用 JsonResponseWrapping() 表示未启用该功能,此时需传入完整的响应类型,无需显式调用 DisableJsonResponseWrapping(),除非全局配置了 UseJsonResponseWrapping = true

19.3.61 设置在构建最终请求 URL 的操作

在某些特殊场景下(例如需要动态拼接服务路径或查询参数),可以通过自定义逻辑来修改请求的 URL。使用 SetUriBuilder 方法即可调整 UriBuilder 对象的各个组成部分,从而构建出最终的请求地址。

HttpRequestBuilder.Post("https://furion.net/")
.SetUriBuilder(uriBuilder =>
{
uriBuilder.Query = "?id=10";
});

注意SetUriBuilder 方法支持多次调用,每次调用的结果会累积叠加。

19.3.62 HttpRequestBuilder 统一配置器

在通过 HttpRequestBuilder 类构建 HttpRequestMessage 对象时,若需对所有请求进行全局配置,框架提供了统一的配置机制。

开发者可通过实现 IHttpRequestBuilderConfigurer 接口,对 HttpRequestBuilder 实例进行统一设置。例如,以下 HttpRequestBuilderConfigurer 类在 Configure 方法中为所有请求添加了一个公共请求头:

public class HttpRequestBuilderConfigurer : IHttpRequestBuilderConfigurer
{
/// <inheritdoc />
public void Configure(HttpRequestBuilder httpRequestBuilder)
{
httpRequestBuilder.WithHeader("global", "form_furion");
}
}

实现自定义配置器后,需在配置 HttpRemoteOptions 时将其赋值给 HttpRequestBuilderConfigurer 属性:

services.AddHttpRemote(builder => {})
.ConfigureOptions(options =>
{
options.HttpRequestBuilderConfigurer = new HttpRequestBuilderConfigurer();
});

配置生效后,所有 HttpRequestBuilder 实例在调用 Build() 方法构建 HttpRequestMessage 之前,均会执行此统一配置逻辑。

19.3.62 添加 HttpRequestBuilder 扩展

除了系统自带的 HttpRequestBuilder 方法,您还可以为其添加自定义扩展方法,以简化代码并减少重复。例如,您可以添加一个 SetAccept 方法,用于在 HTTP 请求头中添加 Accept 字段。具体实现如下:

public static class HttpRequestBuilderExtensions
{
public static HttpRequestBuilder SetAccept(this HttpRequestBuilder httpRequestBuilder, string accept)
{
// 空检查
ArgumentException.ThrowIfNullOrWhiteSpace(accept);

return httpRequestBuilder.WithHeader("Accept", accept, replace: true);
}
}

之后,您可以轻松地在 HttpRequestBuilder 实例中使用此方法:

HttpRequestBuilder.Get("https://furion.net")
.SetAccept("text/html");

利用 C# 扩展方法的特性,您可以极大地丰富 HttpRequestBuilder 的功能,减少重复代码,同时提高代码的可读性和可维护性。

19.4 HttpMultipartFormDataBuilder 表单构建器

在互联网应用中,保存用户自定义数据最常用的方法是使用 Form 表单提交。Form 表单不仅能传输文本数据,还能传输二进制数据(例如文件)。

为了构建包含这些多部分表单内容,我们使用 HttpMultipartFormDataBuilder 表单构建器。该构建器最终会生成一个 MultipartFormDataContent 对象,并将其设置为 HttpRequestMessageContent 属性,同时指定请求的内容类型为 multipart/form-data

19.4.1 创建构建器实例

由于 HttpMultipartFormDataBuilder 的构造函数是私有的,因此无法直接使用 new 关键字进行实例化。若想在 HTTP 远程请求中设置多部分表单内容,必须通过 HttpRequestBuilder 对象提供的 SetMultipartContent(Action<HttpMultipartFormDataBuilder>) 方法进行设置。

HttpRequestBuilder.Post("https://furion.net")
.SetMultipartContent(multipart =>
{
// multipart 的类型是 HttpMultipartFormDataBuilder
});
重要说明

使用 SetMultipartContent 方法会覆盖其他内容设置方法(SetJsonContentSetHtmlContentSetXmlContentSetTextContentSetRawStringContentSetFormUrlEncodedContentSetContent)。

HttpRequestBuilder.Post("https://furion.net/")
.SetMultipartContent(multipart => {
multipart.AddFileAsStream(@"C:\Workspaces\httptest.jpg", "file");
})
.SetContent(new { id = 1, name = "Furion" }, "application/json"); // 将被覆盖

19.4.2 设置内容边界

在构建多部分表单内容时,可以通过以下链式调用方法为多部分表单内容设置边界(Boundary):

HttpRequestBuilder.Post("https://furion.net")
.SetMultipartContent(multipart =>
{
// 属性方式设置边界(非推荐)
multipart.Boundary = "--------------------";

// 方法方式设置边界(推荐),支持链式调用
multipart.SetBoundary("--------------------");
});
小提示

框架默认提供 Boundary,其默认值为:$"--------------------------{DateTime.Now.Ticks:x}"

此外,虽然两种方式都可以设置边界,但推荐使用 SetBoundary 方法,因为它支持链式调用,使代码更加简洁和易读。

19.4.3 保留内容的默认 Content-Type

在与一些较老的 HTTP 服务对接时,提交表单数据时不应设置多部分表单内容的 Content-Type,否则可能引发异常。 而现代 HTTP 接口则无此限制。因此,框架默认在提交表单数据时会自动移除多部分表单内容的 Content-Type

若需取消此操作,可通过以下方式设置:

HttpRequestBuilder.Post("https://furion.net")
.SetMultipartContent(multipart =>
{
multipart.OmitContentType = false; // 保留多部分内容默认的 Content-Type

// 或使用 multipart.SetOmitContentType(false);
});

// 【推荐】使用 SetMultipartContent(Action<HttpMultipartFormDataBuilder> configure, bool omitContentType) 重载方法
HttpRequestBuilder.Post("https://furion.net/")
.SetMultipartContent(multipart =>
{
// ...
}, false); // 保留多部分内容默认的 Content-Type

19.4.4 添加单个表单项内容

向多部分表单内容添加独立的项,即添加单个表单属性。

HttpRequestBuilder.Post("https://furion.net")
.SetMultipartContent(multipart =>
{
multipart.AddFormItem(1, "id"); // 将被赋值给 FormClass 的 Id 属性
multipart.AddFormItem("Furion", "name"); // 将被赋值给 FormClass 的 Name 属性
});

上述代码对应于服务端接收的类定义,如:

public class FormClass
{
public int Id { get; set; }
public string Name { get; set; }
// 其他属性
}

19.4.5 添加 JSON 内容

当需要将 JSON 数据添加到多部分表单内容中时,HttpMultipartFormDataBuilder 提供了灵活的处理方式,具体取决于是否指定了表单名。

1. 未指定表单名:在这种情况下,JSON 数据会被解析并遍历,其属性将作为独立的表单项进行设置。无论是传入匿名类型还是 JSON 字符串,结果都是相同的。

HttpRequestBuilder.Post("https://furion.net")
.SetMultipartContent(multipart =>
{
multipart.AddJson(new { id = 1, name = "furion" }); // 未指定表单名(将被赋值给 FormClass 的 Id 和 Name 属性)
// multipart.AddJson("{\"id\":1,\"name\":\"furion\"}"); // 支持 JSON 字符串。同上。
});

上述代码将生成两个表单项:IdName,它们对应于服务端接收的类定义,如:

public class FormClass
{
public int Id { get; set; }
public string Name { get; set; }
// 其他属性
}

2. 指定表单名:如果为 JSON 数据指定了表单名,则整个 JSON 对象将作为表单的一个嵌套项进行设置。这通常用于服务端期望接收具有嵌套结构的对象。

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 字符串。同上。
});

在这种情况下,服务端接收的类定义应包含一个嵌套类,如:

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; }
// 其他属性...
}
推荐使用【原始字符串字面量】设置 JSON

推荐使用原始字符串字面量来设置 JSON 数据。在 C# 11 中,新增了这一特性,允许使用三个双引号(""")包裹的字符串来包含多行文本,同时字符串内的转义字符(如 \n\t 等)将作为普通字符处理,无需转义。例如:

HttpRequestBuilder.Post("https://furion.net")
.SetMultipartContent(multipart =>
{
multipart.AddJson("""
{
"id": 1,
"name": "Furion"
}
""");
});

若需在原始字符串中插入变量,只需在首个 """ 前添加 $$,并使用 {{变量名}} 模板来占位。例如:

var val = "Furion";

HttpRequestBuilder.Post("https://furion.net")
.SetMultipartContent(multipart =>
{
multipart.AddJson($$"""
{
"id": 1,
"name": "{{val}}"
}
""");
});

使用原始字符串字面量设置 JSON 数据,可以简化代码,避免处理转义符的繁琐操作。

JSON 字符串说明

若传入的 JSON 字符串格式无效,将抛出 JsonException 异常。

19.4.6 添加 HTML 内容

向多部分表单内容中添加 HTML 内容。

HttpRequestBuilder.Post("https://furion.net")
.SetMultipartContent(multipart =>
{
multipart.AddHtml("<html></html>", "data");
});
推荐使用【原始字符串字面量】设置 HTML

参考【19.4.5 设置 JSON 内容】。

19.4.7 添加 XML 内容

向多部分表单内容中添加 XML 内容。

HttpRequestBuilder.Post("https://furion.net")
.SetMultipartContent(multipart =>
{
multipart.AddXml("<?xml version=\"1.0\" encoding=\"UTF-8\"?>", "data");
});
推荐使用【原始字符串字面量】设置 XML

参考【19.4.5 设置 JSON 内容】。

19.4.8 添加文本内容

向多部分表单内容中添加文本内容。

HttpRequestBuilder.Post("https://furion.net")
.SetMultipartContent(multipart =>
{
multipart.AddText("Furion", "data");
});
推荐使用【原始字符串字面量】设置文本

参考【19.4.5 设置 JSON 内容】。

19.4.9 添加对象内容

当需要将对象添加到多部分表单内容中时,HttpMultipartFormDataBuilder 提供了灵活的处理方式,具体取决于是否指定了表单名。

1. 未指定表单名:在这种情况下,对象会被解析并遍历,其属性将作为独立的表单项进行设置。

HttpRequestBuilder.Post("https://furion.net")
.SetMultipartContent(multipart =>
{
multipart.AddObject(new { id = 1, name = "furion" }); // 未指定表单名(将被赋值给 FormClass 的 Id 和 Name 属性)
});

上述代码将生成两个表单项:IdName,它们对应于服务端接收的类定义,如:

public class FormClass
{
public int Id { get; set; }
public string Name { get; set; }
// 其他属性
}

2. 指定表单名:如果为对象指定了表单名,则整个对象将作为表单的一个嵌套项进行设置。这通常用于服务端期望接收具有嵌套结构的对象。

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 属性)
});

在这种情况下,服务端接收的类定义应包含一个嵌套类,如:

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; }
// 其他属性...
}

3. 复杂表单:对象不仅可以包含基础数据类型字段,还可以包含文件或二进制数据(如 StreamMultipartFile)。若需包含文件字段应使用 MultipartFile 类型进行声明。

示例代码如下:

HttpRequestBuilder.Post("https://furion.net")
.SetMultipartContent(multipart =>
{
multipart.AddObject(new FormClass { Id = 1, Name = "furion", File = MultipartFile.CreateFromPath("文件路径") });
});

对应的模型类定义如下:

public class FormClass // 支持属性 [AliasAs] 定义别名
{
public int Id { get; set; }
public string Name { get; set; }
public MultipartFile File { get; set; }
}
JSON 序列化配置说明

注意:当传入类型对象时,框架会先将对象转换为 IDictionary<string, object?> 类型,再逐条添加为表单项。因此,该过程不会直接使用 JSON 序列化的配置。如需为属性指定别名,请通过 [AliasAs] 特性或 multipart.SetFormNameTransformer(namingPolicy) 进行定义。

小提示

AddJsonAddFormItemAddHtmlAddXmlAddText 方法均在内部调用了 AddObject 方法。

19.4.10 添加互联网文件内容

在多部分表单内容中添加来自互联网地址的文件内容。

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: (client,request) => {}); // 支持配置 HttpClient 和 HttpRequestMessage 实例
});

注意:从互联网地址添加文件时,文件大小限制为 100MB

contentType 参数说明
  • 若提供 contentType 参数,则使用该值。
  • 若未提供 contentType 但提供了 fileName,则根据文件名扩展名解析 MIME 类型。
  • 若两者均未提供,则尝试根据 URL 文件名扩展名解析。
  • 若解析失败,则默认使用 application/octet-stream

19.4.11 添加 Base64 字符串文件内容

在多部分表单内容中添加来自 Base64 字符串的文件内容。

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

19.4.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 相比,它允许您实时获取文件传输进度。例如:

// 创建文件传输进度信息的通道
var progressChannel = Channel.CreateUnbounded<FileTransferProgress>();

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 进行上传资源时,建议禁用请求分析工具,以确保能够获取准确的上传进度信息。

19.4.13 添加 Stream 内容

在多部分表单内容中添加 Stream 内容。

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", disposeStreamOnRequestCompletion: true); // 可设置请求完成后自动释放流
});
contentType 参数说明
  • 若提供 contentType 参数,则使用该值。
  • 若未提供 contentType 但提供了 fileName,则根据文件名扩展名解析 MIME 类型。
  • 若两者均未提供,则默认使用 application/octet-stream

19.4.14 添加字节数组内容

在多部分表单内容中添加字节数组内容。

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

19.4.15 添加 MultipartFile 内容 ✨

MultipartFile 类型专为处理多部分表单文件而设计,它的构造函数是私有的,因此无法直接使用 new 关键字进行实例化,不过,框架提供了 MultipartFile.CreateFrom[Source] 的多个静态重载方法创建 MultipartFile 的实例。示例如下:

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:调用 HttpMultipartFormDataBuilderAddByteArray 方法。
  • CreateFromStream:调用 HttpMultipartFormDataBuilderAddStream 方法。
  • CreateFromPath:调用 HttpMultipartFormDataBuilderAddFileAsStream 方法。
  • CreateFromBase64String:调用 HttpMultipartFormDataBuilderAddFileFromBase64String 方法。
  • CreateFromRemote:调用 HttpMultipartFormDataBuilderAddFileFromRemote 方法。

要了解更多请访问 HttpAgent - 官方仓库 - HttpMultipartFormDataBuilder 进行查阅。

19.4.16 添加 IFormFileIFormFileCollection 内容

ASP.NET Core 中,IFormFile 接口用于处理单个文件上传,而 IFormFileCollection 接口则管理多个文件上传。这两个接口简化了文件上传功能的实现。框架提供了 AddFile(IFormFile)AddFiles(IFormFileCollection) 扩展方法,便于为多部分表单内容添加文件。示例如下:

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"); // 自定义表单名称
});

19.4.17 添加 URL 编码表单内容

在多部分表单内容中添加 URL 编码表单内容。

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 类型进行构建,但此类型不支持自定义请求内容编码,它默认使用 Encoding.Latin1 而不是 UTF-8 这可能在提交到某些接口时引发异常。 为解决此问题,可以通过设置参数 useStringContenttrue 来采用 StringContent 方式构建表单数据,从而允许自定义编码为 UTF-8
    var content = await httpRemoteService.PostAsAsync<YourRemoteModel>("https://localhost:7044/HttpRemote/AddURLForm",
    builder => builder
    .SetFormUrlEncodedContent(new { id = 1, name = "furion" }, useStringContent: true));
  • 某些服务器要求显式声明字符集(charset),此时可通过 contentEncoding 参数指定编码方式,例如使用 UTF-8: 此设置在发送远程请求时会生成如下 Content-Type 请求头:application/x-www-form-urlencoded; charset=UTF-8
    var content = await httpRemoteService.PostAsAsync<YourRemoteModel>("https://localhost:7044/HttpRemote/AddURLForm",
    builder => builder
    .SetFormUrlEncodedContent(new { id = 1, name = "furion" }, Encoding.UTF8));

19.4.18 添加多部分表单内容

在多部分表单内容中添加多部分表单内容的需求并不常见。

HttpRequestBuilder.Post("https://furion.net")
.SetMultipartContent(multipart =>
{
multipart.AddMultipartFormData(new MultipartFormDataContent(), "form");
});

19.4.19 添加 HttpContent 内容

添加所有派生自 HttpContent 的请求内容。

HttpRequestBuilder.Post("https://furion.net")
.SetMultipartContent(multipart =>
{
multipart.Add(new StringContent("test"));
multipart.Add(new StringContent("test"), "name"); // 设置表单名
multipart.Add(new FormUrlEncodedContent([
new KeyValuePair<string, string>("id", "1"), new KeyValuePair<string, string>("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<byte>(bytes));
multipart.Add(new MultipartFormDataContent());
});
表单名和内容类型未设置情况

使用 Add 方法添加 HttpContent 时:

  • 如果未指定表单名称,系统会从 HttpContent.Headers.ContentDispositionName 属性中自动解析名称。
  • 如果未设置内容类型,系统则会尝试从 HttpContent.Headers.ContentDispositionFileName 属性中自动推断文件的 MIME 类型作为内容类型。

19.4.20 设置添加表单项内容前的操作

在将 HttpContent 实例添加到 MultipartFormDataContent 对象之前,您可以执行一些预处理操作。例如,在与某些对象存储服务(如阿里云 OSS)对接时,您可能需要移除 Content-Type 设置(框架已内置该操作)。

HttpRequestBuilder.Post("https://furion.net")
.SetMultipartContent(multipart =>
{
multipart.SetBoundary("--------------------------")
.AddFormItem("1", "id")
.AddFormItem("Furion", "name")
.SetOnPreAddContent((content, name) => // 委托参数类型为:Action<HttpContent, string>
{
content.Headers.ContentType = null;
});
});

注意SetOnPreAddContent 方法支持多次调用,每次调用的结果会累积叠加。

19.4.21 设置表单名称策略(转换器)

在发送 HTTP 表单数据时,与直接发送 application/json 格式的 JSON 数据不同,无法直接利用自定义 JSON 序列化选项对属性名称进行格式化。当将对象设置为表单数据时,框架会先将对象转换为 IDictionary<string, object?> 类型,再逐项添加为表单字段。因此,该过程不会沿用 JSON 序列化的命名规则。

由于 C# 语言中通常采用大驼峰命名法(PascalCase)命名属性,而在与某些第三方服务(如 Java 编写的 API)交互时,对方可能对字段名称大小写敏感,导致请求失败。为此,框架提供了 SetFormNameTransformer 方法,用于配置表单字段名称的转换规则。

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

此外,你还可以通过自定义转换器委托实现特定格式,例如为所有字段名称统一添加 _ 前缀:

HttpRequestBuilder.Post("https://furion.net")
.SetMultipartContent(multipart =>
{
multipart.AddObject(new { Id = 1, Name = "Furion"})
.SetFormNameTransformer(name => "_" + name);
});

19.4.22 添加 HttpMultipartFormDataBuilder 扩展

除了系统自带的 HttpMultipartFormDataBuilder 方法,您还可以为其添加自定义扩展方法,以简化代码并减少重复。例如,您可以添加一个 AddRawString 方法,用于为多部分表单添加原始 raw 字符串内容。具体实现如下:

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 实例中使用此方法:

HttpRequestBuilder.Post("https://furion.net")
.SetMultipartContent(multipart =>
{
multipart.AddRawString("Furion", "body");
});

利用 C# 扩展方法的特性,您可以极大地丰富 HttpMultipartFormDataBuilder 的功能,减少重复代码,同时提高代码的可读性和可维护性。

19.5 HTTP 声明式请求 ✨

HTTP 声明式请求机制通过实现 IHttpDeclarative 接口,在程序运行时动态地构建实现类。该机制会智能地拦截符合特定规则的方法调用,并自动生成相应的 HTTP 远程请求代码。这种方法不仅极大地减轻了开发人员编写 HTTP 请求代码的负担,而且使得代码结构更加条理分明,更易于进行组织、维护和复用。

19.5.1 接口定义与使用

在利用 HTTP 声明式请求之前,您需要定义一个接口,并确保它实现 IHttpDeclarative 接口:

public interface IHttpService : IHttpDeclarative
{
}

随后,在 Startup.csProgram.cs 文件中,配置并注册 HttpRemote 服务,以启用 HTTP 声明式请求功能:

services.AddHttpRemote(builder =>
{
// 使用泛型方式注册 IHttpService 声明式接口
builder.AddHttpDeclarative<IHttpService>();

// 或者使用类型方式
// builder.AddHttpDeclarative(typeof(IHttpService));

// 若需注册多个接口,可使用以下方法(非示例中的数组语法)
// builder.AddHttpDeclaratives(new[] { typeof(IHttpService), typeof(IHttpService) });

// 推荐:从程序集中扫描并批量注册
// builder.AddHttpDeclarativesFromAssemblies([Assembly.GetEntryAssembly()]);

// 若使用 Furion 框架,可直接传入 App.Assemblies
// builder.AddHttpDeclarativesFromAssemblies(App.Assemblies);
});

在服务中使用 IHttpService 声明式请求时,可通过构造函数注入:

public class YourService
{
private readonly IHttpService _httpService;

public YourService(IHttpService httpService)
{
_httpService = httpService;
}
}

若您使用的是 .NET 8 及以上版本时,可利用主构造函数注入进一步简化代码:

public class YourService(IHttpService httpService)
{
// 使用 httpService 变量
}

另外,您还可以在需要时,通过特定方法的参数注入来使用 IHttpService

public class YourService
{
public Task<string> GetResource([FromServices] IHttpService httpService)
{
// 您的业务逻辑
}
}

19.5.2 定义请求方法

IHttpService 声明式接口中,您可以定义各种 API 请求方法。这些方法需要标记有从 HttpMethodAttribute 派生的特性,以指明其对应的 HTTP 请求类型。系统预置了多种常见的 HTTP 请求方法特性,同时也支持自定义方法特性:

public interface IHttpService : IHttpDeclarative
{
// 定义 HTTP GET 请求
[Get("https://furion.net/")]
Task<string> GetMethodAsync();

// 定义 HTTP PUT 请求
[Put("https://furion.net/")]
Task<string> PutMethodAsync();

// 定义 HTTP POST 请求
[Post("https://furion.net/")]
Task<string> PostMethodAsync();

// 定义 HTTP DELETE 请求
[Delete("https://furion.net/")]
Task<string> DeleteMethodAsync();

// 定义 HTTP HEAD 请求
[Head("https://furion.net/")]
Task<string> HeadMethodAsync();

// 定义 HTTP OPTIONS 请求
[Options("https://furion.net/")]
Task<string> OptionsMethodAsync();

// 定义 HTTP TRACE 请求
[Trace("https://furion.net/")]
Task<string> TraceMethodAsync();

// 定义 HTTP PATCH 请求
[Patch("https://furion.net/")]
Task<string> PatchMethodAsync();

// 自定义 HTTP 请求方法
[HttpMethod("Connect", "https://furion.net/")]
Task<string> ConnectMethodAsync();
}
接口方法命名规则

在接口方法命名上,建议遵循异步方法命名惯例,即在方法名后添加 Async 后缀,以清晰地表明这些方法执行异步操作。

未标记 HttpMethodAttribute 特性的方法

若接口中的方法未标记有 HttpMethodAttribute 派生特性,在调用时将会抛出 InvalidOperationException 异常,提示信息为“No '[HttpMethod]' annotation was found in method 'System.Threading.Tasks.Task<System.String> UnknownMethodAsync()' of type 'HttpAgent.Samples.IHttpService'.”。

public interface IHttpService : IHttpDeclarative
{
// 缺少 [HttpMethod] 特性,将导致异常
Task<string> UnknownMethodAsync();
}

自定义请求方法

除了直接利用 [HttpMethod("Connect", "https://furion.net/")] 来添加自定义的 HTTP 请求方法外,我们还可以创建一个具体的 ConnectAttribute 特性类,以提高代码的复用性和可读性。这个特性类将继承自 HttpMethodAttribute,并专门用于表示 Connect 请求。

[AttributeUsage(AttributeTargets.Method)]
public sealed class ConnectAttribute : HttpMethodAttribute
{
public ConnectAttribute(string? requestUri = null)
: base("Connect", requestUri)
{
}
}

现在,我们可以在 IHttpService 接口中使用自定义的 [Connect] 特性来替代之前的 [HttpMethod("Connect",...)] 特性:

public interface IHttpService : IHttpDeclarative
{
// 使用自定义 Connect 特性
[Connect("https://furion.net/")]
Task<string> ConnectMethodAsync();
}

这样的代码更加简洁明了,同时提升了代码的可维护性和复用性。

19.5.3 定义请求地址

HttpMethodAttribute 及其派生特性的构造函数中,您可以配置请求的地址。以下展示了如何在 IHttpService 接口中利用这些特性来定义不同的请求地址:

public interface IHttpService : IHttpDeclarative
{
// 使用完整 URL 地址
[Get("https://furion.net/")]
Task<string> GetFullUrlMethodAsync();

// 使用相对地址(不含前导斜杠)
[Get("api/get/user")]
Task<string> GetRelativeUrlMethod1Async();

// 使用相对地址(含前导斜杠)
[Get("/api/get/user")]
Task<string> GetRelativeUrlMethod2Async();

// 请求地址为空字符串,实际请求为 BaseAddress
[Get("")]
Task<string> GetEmptyUrlMethodAsync();

// 请求地址为 null,实际请求为 BaseAddress
[Get(null)]
Task<string> GetNullUrlMethodAsync();

// 请求地址为 null,实际请求为 BaseAddress
[Get]
Task<string> GetNullUrlMethodAsync();
}
  • 当提供的请求地址为完整 URL 时,它将直接作为最终的请求地址。
  • 若请求地址为相对地址(无论是否包含前导斜杠 /),框架将尝试将其与 HttpClient 配置的 BaseAddress 合并,以生成最终的请求地址。例如:
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/"

19.5.4 同步与异步方法

IHttpService 的声明式请求接口方法定义中,我们既提供了异步方法的实现,也支持同步方法的定义。例如:

public interface IHttpService : IHttpDeclarative
{
// 异步请求方法
[Get("https://furion.net/")]
Task<string> GetMethodAsync();

// 同步请求方法
[Get("https://furion.net/")]
string GetMethod();
}
小建议

尽管同步方法使用起来更为直观,但为了最大化硬件资源利用率并提升应用程序的吞吐量,我们强烈建议采用异步方法。异步方法不仅能有效避免死锁和资源竞争等问题,还能使您的应用程序在处理 I/O 密集型任务时更加高效和响应迅速。

19.5.5 定义返回值类型

HTTP 声明式请求接口方法中,除了支持常见的 HTTP 响应类型如 stringbyte[]StreamHttpResponseMessagevoidIActionResult 以及它们的异步版本外,还支持自定义类型和框架内置的 HttpRemoteResult<T>VoidContent 类型及其异步版本。

public interface IHttpService : IHttpDeclarative
{
// 字符串类型
[Get("https://furion.net/")]
Task<string> GetStringAsync();
[Get("https://furion.net/")]
string GetString();

// 字节数组类型
[Get("https://furion.net/")]
Task<byte[]> GetBytesAsync();
[Get("https://furion.net/")]
byte[] GetBytes();

// Stream 类型
[Get("https://furion.net/")]
Task<Stream> GetStreamAsync();
[Get("https://furion.net/")]
Stream GetStream();

// HttpResponseMessage 类型
[Get("https://furion.net/")]
Task<HttpResponseMessage> GetHttpResponseMessageAsync();
[Get("https://furion.net/")]
HttpResponseMessage GetHttpResponseMessage();

// 无返回值
[Get("https://furion.net/")]
Task GetVoidAsync();
[Get("https://furion.net/")]
void GetVoid();

[Get("https://furion.net/")]
Task<VoidContent> GetVoidContentAsync();
[Get("https://furion.net/")]
VoidContent GetVoidContent();

// 框架内置 HttpRemoteResult<T> 类型
[Get("https://furion.net/")]
Task<HttpRemoteResult<string>> GetHttpRemoteResultAsync();
[Get("https://furion.net/")]
HttpRemoteResult<string> GetHttpRemoteResult();

// IActionResult 类型
[Get("https://furion.net/")]
Task<IActionResult> GetYourModelAsync();
[Get("https://furion.net/")]
IActionResult GetYourModel();

// 自定义类型
[Get("https://furion.net/")]
Task<YourModel> GetYourModelAsync();
[Get("https://furion.net/")]
YourModel GetYourModel();
}
关于 VoidContent 类型

由于 void 关键字不能用作泛型的类型参数,系统提供了 VoidContent 类型来表示无返回值的情况。例如,GetAsAsync<VoidContent> 表示不接收响应内容。

返回值类型说明

默认情况下,当返回值类型不是 stringbyte[]StreamHttpResponseMessagevoidVoidContentIActionResultHttpRemoteResult<T> 时,其他类型将使用 System.Text.Json 进行反序列化处理。

如果需要更改此行为,可以在后续章节中了解如何实现 IHttpContentConverter 内容转换器接口进行自定义。

19.5.6 设置跟踪标识

为请求指定一个唯一标识符,便于跟踪和调试。该标识符将被设置在 X-Trace-ID 请求标头中。

HTTP 声明式请求通过 TraceIdentifierAttribute 特性来设置跟踪标识。相应的 HTTP 声明式提取器实现为 TraceIdentifierDeclarativeExtractor 类型,该类型负责解析 TraceIdentifierAttribute 特性并构建 HttpRequestBuilder 实例所需的跟踪标识配置。

// 在接口定义上应用,影响所有方法
[TraceIdentifier("your-id")]
public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/")]
Task<string> GetStringAsync();

// 在方法上应用
[TraceIdentifier("your-method-id")]
[Get("https://furion.net/")]
Task<string> GetStringAsync();
}
TraceIdentifierAttribute 特性作用范围

TraceIdentifierAttribute 特性适用于方法或接口。

TraceIdentifierAttribute 包含以下构造函数和属性:

  • 构造函数
    • new(traceIdentifier):作用于方法或接口,设置跟踪标识。
  • 属性
    • Identifier:跟踪标识(string 类型)。

19.5.7 设置超时时间

为单次请求设置超时时长。

HTTP 声明式请求通过 TimeoutAttribute 特性来设置超时时间。相应的 HTTP 声明式提取器实现为 TimeoutDeclarativeExtractor 类型,该类型负责解析 TimeoutAttribute 特性并构建 HttpRequestBuilder 实例所需的超时时间配置。

// 在接口定义上应用,影响所有方法
[Timeout(100_000)] // 100 秒
public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/")]
Task<string> GetStringAsync();

// 在方法上应用
[Timeout(200_000)] // 200 秒
[Get("https://furion.net/")]
Task<string> GetStringAsync();
}
HttpClient 超时时间说明

HttpClient 中设置超时时间时,请确保单次请求的超时时间不超过 HttpClient 配置的超时时间。例如,如果 HttpClient 超时时间设置为 10 分钟,而单次请求的超时时间设为 15 分钟,那么单次请求在超过 10 分钟时仍会触发超时异常。示例代码如下:

services.AddHttpClient(string.Empty, client =>
{
client.Timeout = TimeSpan.FromMinutes(10); // 默认超时时间为 100 秒,需显式设置
});

因此,若单次请求需要更长的超时时间,请确保 HttpClient 的超时时间设置得相应更长。

TimeoutAttribute 特性作用范围

TimeoutAttribute 特性适用于方法或接口。

TimeoutAttribute 包含以下构造函数和属性:

  • 构造函数
    • new(timeoutMilliseconds):作用于方法或接口,设置超时时间。
  • 属性
    • Timeout:超时时间(毫秒)(double 类型)。

19.5.8 设置路径片段

添加或移除 URL 路径片段。

HTTP 声明式请求通过 PathSegmentAttribute 特性来设置或移除路径片段。相应的 HTTP 声明式提取器实现为 PathSegmentDeclarativeExtractor 类型,该类型负责解析 PathSegmentAttribute 特性并构建 HttpRequestBuilder 实例所需的路径片段配置。

1. 添加路径片段

利用 PathSegmentAttribute 特性,可以便捷地在接口、方法或参数上添加路径片段。

// 在接口定义上应用,影响所有方法
[PathSegment("segment1")]
[PathSegment("segment2")]
public interface IHttpService : IHttpDeclarative
{
// 在方法上应用
[PathSegment("segment3")]
[PathSegment("segment4")]
[Get("https://furion.net/")]
Task<string> GetStringAsync();

// 在参数上应用,支持多重指定
[PathSegment("segment3")]
[Get("https://furion.net/")]
Task<string> GetStringAsync([PathSegment] string segment3, [Query][Query] int lastSegment);

// 在参数上可通过 Segment 属性设定默认值,同样可为 segment 参数设定,例如 string? segment = "default"
[Get("https://furion.net/")]
Task<string> GetStringAsync([PathSegment(Segment = "default")] string? segment);

// 冻结参数类型将被忽略
[Get("https://furion.net/")]
Task<string> GetStringAsync([PathSegment]CancellationToken cancellationToken);
}

若存在重复的路径片段,它们将在后续追加中重复出现(如:/docs/docs/users/docs/)。

2. 移除路径片段

PathSegmentAttribute 特性中,设置 Remove = true,即表示移除该路径片段。在接口、方法或参数上应用有效。

[PathSegment("segment1")] // 添加 segment1 路径片段
[PathSegment("segment2", Remove = true)] // 标记 segment2 为待移除
public interface IHttpService : IHttpDeclarative
{
[PathSegment("segment2")] // 添加 segment2 路径片段
[PathSegment("segment3")] // 添加 segment2 路径片段
[PathSegment("segment3", Remove = true)] // 标记 segment3 为待移除
[Get("https://furion.net/")]
Task<string> GetStringAsync([PathSegment(Remove = true)] string seg); // 动态根据 seg 值标记为待移除
}

在发送 HTTP 请求之前,将移除配置中指定的待移除路径片段集合。也就是说,移除操作会在所有设置操作调用之后执行。

在上述示例中,尽管 GetStringAsync 方法尝试通过 [PathSegment] 特性添加 segment2segment3 路径片段,但由于随后分别有 [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<HttpRequestBuilder>Action<HttpMultipartFormDataBuilder>HttpCompletionOption 以及 CancellationToken 被视为冻结参数类型,它们专门服务于特定的操作执行。因此,PathSegmentAttribute 特性作用于这些参数类型时将被忽略。

小知识

C# 支持特性合并,使代码更简洁:

[PathSegment("segment1"), PathSegment("segment2")]
public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/"), PathSegment("segment3"), PathSegment("segment4")]
Task<string> GetStringAsync();
}

19.5.9 设置查询参数(URL 参数)

添加、修改或移除 URL 查询参数。

HTTP 声明式请求通过 QueryAttribute 特性来设置或移除查询参数。相应的 HTTP 声明式提取器实现为 QueryDeclarativeExtractor 类型,该类型负责解析 QueryAttribute 特性并构建 HttpRequestBuilder 实例所需的查询参数配置。

1. 添加查询参数

利用 QueryAttribute 特性,可以便捷地在接口、方法或参数上添加查询参数。

// 在接口定义上应用,影响所有方法
[Query("query1", "value1")]
[Query("query2", "value2")]
public interface IHttpService : IHttpDeclarative
{
// 在方法上应用
[Query("query3", "value3")]
[Query("query4", "value4")]
[Get("https://furion.net/")]
Task<string> GetStringAsync();

// 在参数上应用,支持 AliasAs 属性指定别名,且可多重指定
[Query("query3", "value3")]
[Get("https://furion.net/")]
Task<string> GetStringAsync([Query] string query4, [Query][Query(AliasAs = "query5")] int lastQuery);

// 在参数上可通过 Value 属性设定默认值,同样可为 age 参数设定,例如 int? age = 30
[Get("https://furion.net/")]
Task<string> GetStringAsync([Query(Value = 30)] int? age);

// 支持将对象作为查询参数,并指定前缀
[Get("https://furion.net/")]
Task<string> GetStringAsync([Query(Prefix = "user")] object obj);

// 支持 [AliasAs] 定义别名
[Get("https://furion.net/")]
Task<string> GetStringAsync([Query][AliasAs("query5")] int lastQuery);

// 支持忽略空值参数,若 str1 值为 null 则忽略
[Get("https://furion.net/")]
Task<string> GetStringAsync([Query(IgnoreNullValues = true)] string? str1, [Query] string? str2);

// 冻结参数类型将被忽略
[Get("https://furion.net/")]
Task<string> GetStringAsync([Query]CancellationToken cancellationToken);
}

若存在重复的查询参数键,它们将合并成多个键值对(如 key1=value1&key1=value2)。通过设置 Replace = true 属性,可以覆盖先前的查询参数。默认情况下,值为 null 的查询参数会被添加到 URL 中;若需忽略这些参数,可设置 IgnoreNullValues = true

2. 移除查询参数

QueryAttribute 特性中,仅指定查询参数键而不赋值,即表示移除该参数。在接口或方法上应用有效。

[Query("query1", "value1")] // 添加 query1 参数
[Query("query2")] // 标记 query2 为待移除
public interface IHttpService : IHttpDeclarative
{
[Query("query2", "value2")] // 添加 query2 参数
[Query("query3", "value3")] // 添加 query3 参数
[Query("query3")] // 标记 query3 为待移除
[Get("https://furion.net/")]
Task<string> GetStringAsync();
}

在发送 HTTP 请求之前,将移除配置中指定的待移除查询参数集合。也就是说,移除操作会在所有设置操作调用之后执行。

在上述示例中,尽管 GetStringAsync 方法尝试通过 [Query] 特性添加 query2query3 参数,但由于随后分别有 [Query("query2")][Query("query3")] 特性仅指定了查询参数键而未赋值,因此这两个键在最终构建请求 URL 时会被移除。只有 query1 参数会保留在请求 URL 中。

3. URL 参数格式化程序

在设置 HTTP 请求的查询参数时,所有参数值最终都会通过调用 ToString() 方法转换为字符串,并追加到 URL 中。然而,这种默认处理方式对某些类型(如 DateTime)可能并不理想,无法满足实际需求。

在这种情况下,可以通过自定义 URL 参数格式化程序来实现更精确的控制。例如,下面的 CustomUrlParameterFormatter 类继承自 UrlParameterFormatter,并重写了 Format 方法,以实现对 DateTime 类型的特殊格式化处理:

public class CustomUrlParameterFormatter : UrlParameterFormatter
{
/// <inheritdoc />
public override string? Format(object? value, UrlFormattingContext context)
{
if (value is DateTime dateTime)
{
return dateTime.ToString("yyyyMMdd");
}

return base.Format(value, context);
}
}

完成自定义格式化程序后,可以在配置 HttpRemoteOptions 时将其注册为默认的 URL 参数格式化器:

services.AddHttpRemote(builder => {})
.ConfigureOptions(options =>
{
options.UrlParameterFormatter = new CustomUrlParameterFormatter();
});

如此一来,在构建 URL 查询参数时,若遇到 DateTime 类型的值,框架将自动将其格式化为 yyyyMMdd 格式的字符串,从而确保输出符合预期。

QueryAttribute 包含以下构造函数和属性:

  • 构造函数
    • 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(不忽略)。
冻结参数类型说明

在系统中,Action<HttpRequestBuilder>Action<HttpMultipartFormDataBuilder>HttpCompletionOption 以及 CancellationToken 被视为冻结参数类型,它们专门服务于特定的操作执行。因此,QueryAttribute 特性作用于这些参数类型时将被忽略。

小知识

C# 支持特性合并,使代码更简洁:

[Query("query1", "value1"), Query("query2", "value2")]
public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/"), Query("query3", "value3"), Query("query4", "value4")]
Task<string> GetStringAsync();
}

19.5.10 设置请求标头

添加、修改或移除请求标头。

HTTP 声明式请求通过 HeaderAttribute 特性来设置或移除请求标头。相应的 HTTP 声明式提取器实现为 HeaderDeclarativeExtractor 类型,该类型负责解析 HeaderAttribute 特性并构建 HttpRequestBuilder 实例所需的请求标头配置。

1. 添加请求标头

利用 HeaderAttribute 特性,可以便捷地在接口、方法或参数上添加请求标头。

// 在接口定义上应用,影响所有方法
[Header("header1", "value1")]
[Header("header2", "value2")]
public interface IHttpService : IHttpDeclarative
{
// 在方法上应用
[Header("header3", "value3")]
[Header("header4", "value4")]
[Get("https://furion.net/")]
Task<string> GetStringAsync();

// 在参数上应用,支持 AliasAs 属性指定别名,且可多重指定
[Header("header3", "value3")]
[Get("https://furion.net/")]
Task<string> GetStringAsync([Header] string header4, [Header][Header(AliasAs = "header5")] int lastHeader);

// 在参数上可通过 Value 属性设定默认值,同样可为 age 参数设定,例如 int? age = 30
[Get("https://furion.net/")]
Task<string> GetStringAsync([Header(Value = 30)] int? age);

// 支持 [AliasAs] 定义别名
[Get("https://furion.net/")]
Task<string> GetStringAsync([Header][AliasAs("header5")] int lastHeader);

// 冻结参数类型将被忽略
[Get("https://furion.net/")]
Task<string> GetStringAsync([Header]CancellationToken cancellationToken);
}

若存在重复的请求标头,它们将被合并,并用逗号加空格(, )分隔多个值。通过设置 Replace = true 属性,可以覆盖先前的请求标头设置。

2. 移除请求标头

HeaderAttribute 特性中,仅指定请求标头键而不赋值,即表示移除该标头。在接口或方法上应用有效。

[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<string> GetStringAsync();
}

在发送 HTTP 请求之前,将移除配置中指定的待移除请求标头集合。也就是说,移除操作会在所有设置操作调用之后执行。

在上述示例中,尽管 GetStringAsync 方法尝试通过 [Header] 特性添加 header2header3 标头,但由于随后分别有 [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(追加)。
冻结参数类型说明

在系统中,Action<HttpRequestBuilder>Action<HttpMultipartFormDataBuilder>HttpCompletionOption 以及 CancellationToken 被视为冻结参数类型,它们专门服务于特定的操作执行。因此,HeaderAttribute 特性作用于这些参数类型时将被忽略。

小知识

C# 支持特性合并,使代码更简洁:

[Header("header1", "value1"), Header("header2", "value2")]
public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/"), Header("header3", "value3"), Header("header4", "value4")]
Task<string> GetStringAsync();
}
配置参数支持

请求标头支持配置参数,用于读取配置信息进行替换操作。配置参数使用 [[key]] 语法。

19.5.11 设置路径参数(模板/配置参数)

URL 路径中替换对象模板字符串。

HTTP 声明式请求通过 PathAttribute 特性和方法定义的非冻结类型参数来配置路径参数。相应的 HTTP 声明式提取器为 PathDeclarativeExtractor 类型,它负责解析这些 PathAttribute 特性及方法中定义的非冻结类型参数,并构建 HttpRequestBuilder 实例所需的路径参数配置。

// 在接口定义上应用,影响所有方法
[Path("path1", "value1")]
[Path("path2", "value2")]
public interface IHttpService : IHttpDeclarative
{
// 在方法上应用
[Path("path3", "value3")]
[Get("https://furion.net/{path1}/{path2}/{path3}")]
Task<string> GetStringAsync();

// 方法上定义的非冻结类型参数默认会添加到路径参数中,可在 URL 地址中直接使用
[Get("https://furion.net/{path1}/{path2}/?id={id}&name={name}&address={address}&age={age}&name1={user.Name}&obj={obj}")]
Task<string> GetStringAsync(int id, string name, string[] address, int age, User user, object? obj);

// 冻结参数类型将被忽略
[Get("https://furion.net/")]
Task<string> GetStringAsync(CancellationToken cancellationToken);
}

若存在重复的路径参数键,则后设置的键值会覆盖先前的设置。

PathAttribute 特性作用范围

PathAttribute 特性仅适用于方法或接口,不适用于参数。因为方法上定义的非冻结类型参数默认会添加到路径参数中,因此无需手动标记。

冻结参数类型说明

在系统中,Action<HttpRequestBuilder>Action<HttpMultipartFormDataBuilder>HttpCompletionOption 以及 CancellationToken 被视为冻结参数类型,它们专门服务于特定的操作执行。因此,这些参数类型时将被忽略作为路径参数。

PathAttribute 包含以下构造函数和属性:

  • 构造函数
    • new(name, value):作用于接口或方法,表示添加路径参数,键为参数 name 的值。
  • 属性
    • Name:路径参数键(string 类型)。
    • Value:路径参数的值(object 类型)。
小知识

C# 支持特性合并,使代码更简洁:

[Path("path1", "value1"), Path("path2", "value2")]
public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/{path1}/{path2}/{path3}"), Path("path3", "value3")]
Task<string> GetStringAsync();
}

配置参数

除了通过 {key} 模板语法设置路径参数外,框架还提供了配置参数,用于读取配置信息进行替换操作。配置参数使用 [[key]] 语法,例如:

public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net?id=[[id]]&name=[[name]]")]
Task<string> GetStringAsync();
}

启用配置参数支持

要在 HttpRemote 服务中启用配置参数支持,请按照以下步骤进行配置:

services.AddHttpRemote(builder => {})
.ConfigureOptions(options =>
{
// 设置用于替换 URL 地址中配置模板参数的提供源
options.Configuration = builder.Configuration; // 若使用 Furion 框架可直接设置 App.Configuration
});

配置参数的使用

配置参数将从您的配置文件中读取并替换到 URL 中。例如,您的配置文件可能如下所示:

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]]:如果 notfoundbak 都不存在,则查找 other
    • [[notfound | bak:sub | other:sub:nest]]:支持更深层次的备用查找。
  • 默认值:
    • [[notfound || default]]:如果 notfound 不存在,则使用 default 作为值。
    • [[notfound | bak | other || 默认值]]:结合备用查找和默认值,确保总有值可用。

添加、修改或移除 Cookie

HTTP 声明式请求通过 CookieAttribute 特性来设置或移除 Cookie 。相应的 HTTP 声明式提取器实现为 CookieDeclarativeExtractor 类型,该类型负责解析 CookieAttribute 特性并构建 HttpRequestBuilder 实例所需的 Cookie 配置。

1. 添加 Cookie

利用 CookieAttribute 特性,可以便捷地在接口、方法或参数上添加 Cookie

// 在接口定义上应用,影响所有方法
[Cookie("cookie1", "value1")]
[Cookie("cookie2", "value2")]
public interface IHttpService : IHttpDeclarative
{
// 在方法上应用
[Cookie("cookie3", "value3")]
[Cookie("cookie4", "value4")]
[Get("https://furion.net/")]
Task<string> GetStringAsync();

// 在参数上应用,支持 AliasAs 属性指定别名,且可多重指定
[Cookie("cookie3", "value3")]
[Get("https://furion.net/")]
Task<string> GetStringAsync([Cookie] string cookie4, [Cookie][Cookie(AliasAs = "cookie5")] int lastCookie);

// 在参数上可通过 Value 属性设定默认值,同样可为 age 参数设定,例如 int? age = 30
[Get("https://furion.net/")]
Task<string> GetStringAsync([Cookie(Value = 30)] int? age);

// 支持 [AliasAs] 定义别名
[Get("https://furion.net/")]
Task<string> GetStringAsync([Cookie][AliasAs("cookie5")] int lastCookie);

// 冻结参数类型将被忽略
[Get("https://furion.net/")]
Task<string> GetStringAsync([Cookie]CancellationToken cancellationToken);
}

若存在重复的 Cookie 键,则后设置的键值会覆盖先前的设置。

2. 移除 Cookie

CookieAttribute 特性中,仅指定 Cookie 键而不赋值,即表示移除该 Cookie。在接口或方法上应用有效。

[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<string> GetStringAsync();
}

在发送 HTTP 请求之前,将移除配置中指定的待移除 Cookie 集合。也就是说,移除操作会在所有设置操作调用之后执行。

在上述示例中,尽管 GetStringAsync 方法尝试通过 [Cookie] 特性添加 cookie2cookie3,但由于随后分别有 [Cookie("cookie2")][Cookie("cookie3")] 特性仅指定了 Cookie 键而未赋值,因此这两个键在最终构建请求标头 Cookie 时会被移除。只有 cookie1 参数会保留在请求标头 Cookie 中。

CookieAttribute 包含以下构造函数和属性:

  • 构造函数
    • new():作用于参数时有效,表示添加 Cookie ,默认键为参数名。
    • new(name):作用于方法或接口时,则表示移除指定 Cookie 操作;作用于参数时,表示添加 Cookie ,键为参数 name 的值。
    • new(name, value):作用于接口、方法或参数,表示添加 Cookie ,键为参数 name 的值,优先级低于 AliasAs 属性。
  • 属性
    • NameCookie 键(string 类型),优先级低于 AliasAs 属性。
    • ValueCookie 的值(object 类型),当特性作用于参数时,表示默认值。
    • AliasAsCookie 键别名(string 类型),优先级高于 Name 属性。
冻结参数类型说明

在系统中,Action<HttpRequestBuilder>Action<HttpMultipartFormDataBuilder>HttpCompletionOption 以及 CancellationToken 被视为冻结参数类型,它们专门服务于特定的操作执行。因此,CookieAttribute 特性作用于这些参数类型时将被忽略。

小知识

C# 支持特性合并,使代码更简洁:

[Cookie("cookie1", "value1"), Cookie("cookie2", "value2")]
public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/"), Cookie("cookie3", "value3"), Cookie("cookie4", "value4")]
Task<string> GetStringAsync();
}
配置参数支持

Cookie 值支持配置参数,用于读取配置信息进行替换操作。配置参数使用 [[key]] 语法。

19.5.13 设置 HttpClient 实例的名称(多个基地址)

系统默认使用 IHttpClientFactory 创建 HttpClient 实例,并将默认客户端名称设为空字符串(string.Empty)。您可以通过 HttpClientNameAttribute 特性设置创建 HttpClient实例时的客户端名称。

HTTP 声明式请求通过 HttpClientNameAttribute 特性来设置 HttpClient 实例的名称。相应的 HTTP 声明式提取器实现为 HttpClientNameDeclarativeExtractor 类型,该类型负责解析 HttpClientNameAttribute 特性并构建 HttpRequestBuilder 实例所需的 HttpClient 实例的名称配置。

// 在接口定义上应用,影响所有方法
[HttpClientName(string.Empty)]
public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/")]
Task<string> GetStringAsync(); // 默认客户端

// 在方法上应用
[HttpClientName("weixin")] // 指定为名为 "weixin" 的客户端
[Get("https://furion.net/")]
Task<string> GetStringAsync();
}

您还可以在 Startup.csProgram.cs 文件中为命名 HttpClient 客户端提供配置:

// 配置默认客户端(名称为空字符串)
services.AddHttpClient(string.Empty, client => { });

// 配置名为 "weixin" 的客户端
services.AddHttpClient("weixin", client => { });
HttpClientNameAttribute 特性作用范围

HttpClientNameAttribute 特性适用于方法或接口。

HttpClientNameAttribute 包含以下构造函数和属性:

  • 构造函数
    • new(name):作用于方法或接口,设置 HttpClient 实例的名称。
  • 属性
    • NameHttpClient 实例的名称(string 类型)。

19.5.14 设置请求内容

支持设置任意类型的请求内容。

HTTP 声明式请求通过 BodyAttribute 特性来配置请求内容。相应的 HTTP 声明式提取器为 BodyDeclarativeExtractor 类型,它负责解析单个 BodyAttribute 特性,并构建 HttpRequestBuilder 实例所需的请求内容配置。

public interface IHttpService : IHttpDeclarative
{
// 标记参数为请求内容
[Post("https://furion.net/")]
Task<string> PostStringAsync([Body] object body);

// 支持设置 Content-Type
[Post("https://furion.net/")]
Task<string> PostStringAsync([Body("application/json")] object body); // 或使用 [Body(MediaTypeNames.Application.Json)]

// 支持设置 Content-Type 和字符集
[Post("https://furion.net/")]
Task<string> PostStringAsync([Body("application/json; charset=utf-8")] object body);

// URL 编码表单
[Post("https://furion.net/")]
Task<string> PostStringAsync([Body("application/x-www-form-urlencoded")] object body);

// 使用 StringContent 构建 URL 编码表单
[Post("https://furion.net/")]
Task<string> PostStringAsync([Body("application/x-www-form-urlencoded", UseStringContent = true)] object body);

// 可配置不进行 URL 编码处理
[Post("https://furion.net/")]
Task<string> PostStringAsync([Body("application/x-www-form-urlencoded", useUrlEncode = false)] object body);

// 支持原始 raw 字符串内容
[Post("https://furion.net/")]
Task<string> PostStringAsync([Body("application/json", RawString = true)] string body);

// 冻结参数类型将被忽略
[Post("https://furion.net/")]
Task<string> PostStringAsync([Body]CancellationToken cancellationToken);
}
多种 Body 参数声明特性

为方便快速标注请求内容参数,框架提供了多种常用的 Body 特性,如 [JsonBody][HtmlBody][FormUrlEncodedBody][RawStringBody][TextBody][XmlBody]

未提供 Content-Type 时的默认行为

[Body] 未指定 Content-Type 时,框架会根据内容类型自动设置 Content-Type,具体规则如下:

  • JsonContent 类型:自动设置为 application/json
  • FormUrlEncodedContent 类型:自动设置为 application/x-www-form-urlencoded
  • byte[]StreamByteArrayContentStreamContentReadOnlyMemoryContentReadOnlyMemory<byte> 类型:自动设置为 application/octet-stream
  • MultipartContent 类型:自动设置为 multipart/form-data

如果以上类型均不匹配,则默认回退为 text/plain。如需修改此回退值,可通过以下代码配置:

services.AddHttpRemote(builder => {})
.ConfigureOptions(options =>
{
// 设置默认的请求内容类型
options.DefaultContentType = "application/json";
});

配置后,当以上类型均不匹配时,默认 Content-Type 将被设置为 application/json

BodyAttribute 标记的参数通过底层的 httpRequestBuilder.SetContent 方法进行设置,支持任意非冻结类型的参数。

BodyAttribute 特性作用范围

BodyAttribute 特性仅适用于参数。

冻结参数类型说明

在系统中,Action<HttpRequestBuilder>Action<HttpMultipartFormDataBuilder>HttpCompletionOption 以及 CancellationToken 被视为冻结参数类型,它们专门服务于特定的操作执行。因此,BodyAttribute 特性作用于这些参数类型时将被忽略。

多个参数标记 BodyAttribute 特性

由于请求内容只能包含一个值,如果方法中有多个 BodyAttribute 特性标记的参数,将抛出 InvalidOperationException 异常。异常消息为:The input sequence contains more than one element.

URL 编码表单内容说明
  • 默认情况下,URL 编码表单通过 FormUrlEncodedContent 类型进行构建,但此类型不支持自定义请求内容编码,它默认使用 Encoding.Latin1 而不是 UTF-8 这可能在提交到某些接口时引发异常。 为解决此问题,可以通过设置属性 UseStringContenttrue 来采用 StringContent 方式构建表单数据,从而允许自定义编码为 UTF-8
    public interface IHttpService : IHttpDeclarative
    {
    [Post("https://furion.net/")]
    Task<string> PostStringAsync([Body("application/x-www-form-urlencoded", UseStringContent = true)] object body); // body 同时支持 URL 编码字符串,例如:id=1&name=furion
    }
  • 某些服务器要求显式声明字符集(charset),此时可通过 contentEncoding 参数指定编码方式,例如使用 UTF-8: 此设置在发送远程请求时会生成如下 Content-Type 请求头:application/x-www-form-urlencoded; charset=UTF-8
    public interface IHttpService : IHttpDeclarative
    {
    [Post("https://furion.net/")]
    Task<string> PostStringAsync([Body("application/x-www-form-urlencoded", "UTF-8")] object body); // body 同时支持 URL 编码字符串,例如:id=1&name=furion
    }

BodyAttribute 包含以下构造函数和属性:

  • 构造函数
    • new():作用于参数,将参数作为请求内容。
    • new(contentType):作用于参数,将参数作为请求内容,支持设置内容类型。
    • new(contentType, contentEncoding):作用于参数,将参数作为请求内容,支持设置内容类型和编码。
  • 属性
    • ContentType:内容类型(string 类型)。
    • ContentEncoding:内容编码(string 类型)。
    • UseStringContent:是否使用 StringContent 构建 FormUrlEncodedContent,默认值为 false,仅当 ContentTypeapplication/x-www-form-urlencoded 时有效。
    • RawString:是否为原始字符串内容,默认值为 false,仅当参数为字符串类型且此属性为 true 时有效。

19.5.15 设置多部分表单内容(上传文件)

将请求的内容类型设置为 multipart/form-data 并发送多部分表单内容。

HTTP 声明式请求通过 MultipartAttribute 特性来设置多部分表单内容。相应的 HTTP 声明式提取器实现为 MultipartDeclarativeExtractor 类型,该类型负责解析 MultipartAttributeMultipartFormAttribute 特性并构建 HttpRequestBuilder 实例所需的多部分表单内容配置。

public interface IHttpService : IHttpDeclarative
{
// 添加常见表单项内容
[Post("https://furion.net/")]
Task<string> 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<string> 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<string> PostStringAsync([Multipart(AsFormItem = false)] object obj); // 推荐使用 [MultipartObject]

// 设置多部分表单内容的边界
[MultipartForm("--------------------")]
[Post("https://furion.net/")]
Task<string> PostStringAsync([Multipart] int id);

// 设置表单名称命名策略(转换器)
[Post("https://furion.net/")]
[MultipartForm(NamingPolicy = FormNamingPolicy.CamelCase)]
Task<string> PostStringAsync([MultipartObject] object obj);

// 冻结参数类型将被忽略
[Post("https://furion.net/")]
Task<string> PostStringAsync([Multipart]CancellationToken cancellationToken);
}

在处理包含基础数据与文件(或二进制数据)的复杂表单时,可以使用 [MultipartObject] 特性标记对应的复杂类型。其中,文件字段应使用 MultipartFile 类型进行声明。示例接口定义如下:

public interface IHttpService : IHttpDeclarative
{
[Post("https://furion.net/")]
Task<string> PostStringAsync([MultipartObject] FormClass data);
}

对应的模型类定义如下:

public class FormClass // 支持属性 [AliasAs] 定义别名
{
public int Id { get; set; }
public string Name { get; set; }
public MultipartFile File { get; set; }
}
JSON 序列化配置说明

注意:当传入类型对象时,框架会先将对象转换为 IDictionary<string, object?> 类型,再逐条添加为表单项。因此,该过程不会直接使用 JSON 序列化的配置。如需为属性指定别名,请通过 [AliasAs] 特性或使用 [MultipartForm(NamingPolicy)] 特性进行定义。

通过这种方式,框架会自动将对象中的基本类型属性作为普通表单项提交,并将 MultipartFile 类型的属性作为文件上传内容进行正确编码和传输。

MultipartAttribute 标记的参数通过底层的 httpRequestBuilder.SetMultipartContent 方法进行设置,支持任意非冻结类型的参数。以下代码示例展示了如何使用 HttpRequestBuilder 达到相同配置效果:

// 添加常见表单项内容
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<HttpRequestBuilder>Action<HttpMultipartFormDataBuilder>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-Typebool 类型)。默认值为 true
    • NamingPolicy:表单名称命名策略(转换器)(FormNamingPolicy 类型)。默认值为 FormNamingPolicy.None

19.5.16 禁用 HTTP 缓存

在发送 HTTP GET 请求时,服务器可能会缓存该请求的结果以提高性能。为了取消其缓存行为,可以添加 DisableCacheAttribute 特性。

HTTP 声明式请求通过 DisableCacheAttribute 特性来禁用 HTTP 缓存。相应的 HTTP 声明式提取器实现为 DisableCacheDeclarativeExtractor 类型,该类型负责解析 DisableCacheAttribute 特性并构建 HttpRequestBuilder 实例所需的禁用 HTTP 缓存配置。

// 在接口定义上应用,影响所有方法
[DisableCache]
public interface IHttpService : IHttpDeclarative
{
// 在方法上应用
[DisableCache]
[Get("https://furion.net/")]
Task<string> GetStringAsync();

[DisableCache(false)] // 启用缓存(默认)
[Get("https://furion.net/")]
Task<string> GetStringAsync();
}

在添加该特性之后,HTTP 请求将在发送前自动附带以下请求标头,以确保缓存控制:

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(禁用)。

19.5.17 确保请求成功

添加 EnsureSuccessStatusCodeAttribute 特性后,当 HTTP 响应的状态码不在 200-299 范围内时(即 IsSuccessStatusCode 属性为 false),将自动抛出异常。

HTTP 声明式请求通过 EnsureSuccessStatusCodeAttribute 特性来确保请求成功。相应的 HTTP 声明式提取器实现为 EnsureSuccessStatusCodeDeclarativeExtractor 类型,该类型负责解析 EnsureSuccessStatusCodeAttribute 特性并构建 HttpRequestBuilder 实例所需的确保请求成功配置。

// 在接口定义上应用,影响所有方法
[EnsureSuccessStatusCode]
public interface IHttpService : IHttpDeclarative
{
// 在方法上应用
[EnsureSuccessStatusCode]
[Get("https://furion.net/")]
Task<string> GetStringAsync();

[EnsureSuccessStatusCode(false)] // 关闭验证
[Get("https://furion.net/")]
Task<string> GetStringAsync();
}
EnsureSuccessStatusCodeAttribute 特性作用范围

EnsureSuccessStatusCodeAttribute 特性适用于方法或接口。

EnsureSuccessStatusCodeAttribute 包含以下构造函数和属性:

  • 构造函数
    • new():作用于方法或接口,确保请求成功。
    • new(enabled):作用于方法或接口,设置是否确保请求成功。
  • 属性
    • Enabled:是否启用(bool 类型),默认值为 true(启用)。

19.5.18 模拟浏览器环境(爬虫检测)

在开发爬虫程序时,目标网站可能会根据用户代理(User-Agent)或其他因素提供不同的页面版本,如 PC 端和移动端。此外,一些网站还具备反爬虫机制,能够识别并阻止爬虫程序的访问。为应对这些问题,我们可以配置请求标头以模拟真实的浏览器环境进行请求。

HTTP 声明式请求通过 SimulateBrowserAttribute 特性来模拟浏览器环境。相应的 HTTP 声明式提取器实现为 SimulateBrowserDeclarativeExtractor 类型,该类型负责解析 SimulateBrowserAttribute 特性并构建 HttpRequestBuilder 实例所需的模拟浏览器环境配置。

// 在接口定义上应用,影响所有方法
[SimulateBrowser]
public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/")]
Task<string> GetStringAsync();

// 在方法上应用
[SimulateBrowser(Mobile = true)] // 模拟移动端浏览器环境
[Get("https://furion.net/")]
Task<string> GetStringAsync();
}

在添加该特性之后,HTTP 请求将在发送前自动附带以下请求标头,以确保服务器能够准确识别并处理请求:

# 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():作用于方法或接口,启用模拟浏览器环境。
  • 属性
    • Mobile:是否模拟移动端浏览器环境(bool 类型),默认值为 falsePC 端)。

19.5.19 启用请求分析工具

在现代化的浏览器中,通常内置了开发者工具,这些工具能够捕获并直观展示用户访问网站时的所有请求与响应数据。类似地,我们也为 HTTP 远程请求模块提供了一套分析工具。

HTTP 声明式请求通过 ProfilerAttribute 特性来启用请求分析工具。相应的 HTTP 声明式提取器实现为 ProfilerDeclarativeExtractor 类型,该类型负责解析 ProfilerAttribute 特性并构建 HttpRequestBuilder 实例所需的启用请求分析工具配置。

// 在接口定义上应用,影响所有方法
[Profiler]
public interface IHttpService : IHttpDeclarative
{
// 在方法上应用
[Profiler]
[Get("https://furion.net/")]
Task<string> GetStringAsync();

[Profiler(false)] // 禁用请求分析工具
[Get("https://furion.net/")]
Task<string> GetStringAsync();
}

启用后,当执行 HTTP 远程请求时,控制台将输出如下详细信息:

General:
Request URL: https://furion.net/
HTTP Method: GET
Status Code: 200 OK
HTTP Version: 1.1
HTTP Content:
Declarative: System.Threading.Tasks.Task<System.String> 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 中启用:

// 为默认客户端启用
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 随之移动到尾部。

补充说明: 请求分析工具默认仅展示请求或响应体中最多 8KB 的内容数据。

19.5.20 设置客户端偏好的语言和区域

全球化是互联网应用产品的发展趋势,因此,面向全球的应用产品应具备国际化功能。发送 HTTP 请求时,可通过添加 AcceptLanguageAttribute 特性来指定客户端偏好的自然语言和区域。

HTTP 声明式请求通过 AcceptLanguageAttribute 特性来设置客户端偏好的语言和区域。相应的 HTTP 声明式提取器实现为 AcceptLanguageDeclarativeExtractor 类型,该类型负责解析 AcceptLanguageAttribute 特性并构建 HttpRequestBuilder 实例所需的客户端偏好的语言和区域配置。

// 在接口定义上应用,影响所有方法
[AcceptLanguage("en-US")]
public interface IHttpService : IHttpDeclarative
{
// 在方法上应用
[AcceptLanguage("zh-CN,en;q=0.5")]
[Get("https://furion.net/")]
Task<string> GetStringAsync();

[AcceptLanguage("fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5")]
[Get("https://furion.net/")]
Task<string> GetStringAsync();
}
AcceptLanguageAttribute 特性作用范围

AcceptLanguageAttribute 特性适用于方法或接口。

AcceptLanguageAttribute 包含以下构造函数和属性:

  • 构造函数
    • new(language):作用于方法或接口,配置客户端偏好的语言和区域。
  • 属性
    • Language:客户端偏好的语言和区域(string 类型)。

19.5.21 设置 HttpRequestMessage 属性

在特定场景下,我们可能需要为 HttpRequestMessage 请求添加额外的属性,而非通过请求标头。

HTTP 声明式请求通过 PropertyAttribute 特性来设置 HttpRequestMessage 请求属性。相应的 HTTP 声明式提取器实现为 PropertyDeclarativeExtractor 类型,该类型负责解析 PropertyAttribute 特性并构建 HttpRequestBuilder 实例所需的 HttpRequestMessage 请求属性配置。

利用 PropertyAttribute 特性,可以便捷地在接口、方法或参数上添加 HttpRequestMessage 请求属性。

// 在接口定义上应用,影响所有方法
[Property("property1", "value1")]
[Property("property2", "value2")]
[Property("property0")] // 值为 null
public interface IHttpService : IHttpDeclarative
{
// 在方法上应用
[Property("property3", "value3")]
[Property("property4", "value4")]
[Get("https://furion.net/")]
Task<string> GetStringAsync();

// 在参数上应用,支持 AliasAs 属性指定别名,且可多重指定
[Property("property3", "value3")]
[Get("https://furion.net/")]
Task<string> GetStringAsync([Property] string property4, [Property][Property(AliasAs = "property5")] int lastProperty);

// 在参数上可通过 Value 属性设定默认值,同样可为 age 参数设定,例如 int? age = 30
[Get("https://furion.net/")]
Task<string> GetStringAsync([Property(Value = 30)] int? age);

// 支持 [AliasAs] 定义别名
[Get("https://furion.net/")]
Task<string> GetStringAsync([Property][AliasAs("property5")] int lastProperty);

// 添加对象内容,AsItem 为 false 时,对象会被解析并遍历,其属性将作为独立的 HttpRequestMessage 请求属性项进行设置
[Get("https://furion.net/")]
Task<string> GetStringAsync([Property(AsItem = false)] object obj);

// 冻结参数类型将被忽略
[Get("https://furion.net/")]
Task<string> GetStringAsync([Property]CancellationToken cancellationToken);
}

这些属性会被添加到 HttpRequestMessage 对象的 Options 属性中(参考文档)。要获取这些值,可以这样做:

httpRequestMessage.Options.TryGetValue(new HttpRequestOptionsKey<string>("key1"), out var value);

若属性键出现重复,则后设置的键值会覆盖先前的设置。

小提示

此功能常被集成在自定义的 DelegatingHandlerIHttpRequestEventHandler 组件中。

PropertyAttribute 包含以下构造函数和属性:

  • 构造函数
    • new():作用于参数时有效,表示添加 HttpRequestMessage 请求属性,默认键为参数名。
    • new(name):作用于方法或接口时,则表示添加 HttpRequestMessage 请求属性操作,值为 null;作用于参数时,表示添加 HttpRequestMessage 请求属性,键为参数 name 的值。
    • new(name, value):作用于接口、方法或参数,表示添加 HttpRequestMessage 请求属性,键为参数 name 的值,优先级低于 AliasAs 属性。
  • 属性
    • NameHttpRequestMessage 请求属性键(string 类型),优先级低于 AliasAs 属性。
    • ValueHttpRequestMessage 请求属性的值(object 类型),当特性作用于参数时,表示默认值。
    • AliasAsHttpRequestMessage 请求属性键别名(string 类型),优先级高于 Name 属性。
    • AsItem:表示是否作为 HttpRequestMessage 请求属性的一项(bool 类型),默认值为 true(作为),仅当参数为对象类型时有效。为 false(不作为) 时,对象会被解析并遍历,其属性将作为独立的 HttpRequestMessage 请求属性项进行设置。
冻结参数类型说明

在系统中,Action<HttpRequestBuilder>Action<HttpMultipartFormDataBuilder>HttpCompletionOption 以及 CancellationToken 被视为冻结参数类型,它们专门服务于特定的操作执行。因此,PropertyAttribute 特性作用于这些参数类型时将被忽略。

小知识

C# 支持特性合并,使代码更简洁:

[Property("property1", "value1"), Property("property2", "value2")]
public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/"), Property("property3", "value3"), Property("property4", "value4")]
Task<string> GetStringAsync();
}

19.5.22 启用性能优化

为了提升应用通过 HTTP 客户端发送网络请求的性能,可以通过配置默认的 HTTP 头部来优化数据传输效率。框架提供了一键式配置方法,方便快速统一设置这些头部:

HTTP 声明式请求通过 PerformanceOptimizationAttribute 特性来启用性能优化。相应的 HTTP 声明式提取器实现为 PerformanceOptimizationDeclarativeExtractor 类型,该类型负责解析 PerformanceOptimizationAttribute 特性并构建 HttpRequestBuilder 实例所需的启用性能优化配置。

// 在接口定义上应用,影响所有方法
[PerformanceOptimization]
public interface IHttpService : IHttpDeclarative
{
// 在方法上应用
[PerformanceOptimization]
[Get("https://furion.net/")]
Task<string> GetStringAsync();

[PerformanceOptimization(false)] // 关闭性能优化
[Get("https://furion.net/")]
Task<string> GetStringAsync();
}

此外,除了为单个请求启用性能优化配置,还可以全局注册以在 HttpClient 中启用:

// 为默认客户端启用
services.AddHttpClient(string.Empty, client =>
{
client.PerformanceOptimization();
});

services.AddHttpRemote();

启用性能优化后,请求将自动添加以下头部,以提升传输效率:

  • Accept*/*(接受任意类型的响应内容)
  • Accept-Encodinggzip, deflate, br(支持的内容编码格式,以提高传输效率)
  • Connectionkeep-alive(保持连接,减少 TCP 连接建立和关闭的开销)
重要提示

当需要返回 Stream 内容或进行 HttpContext 网页转发时,请勿启用此配置,因为流会因压缩而变得不可读,同时该配置也不适用于网页转发的场景。

PerformanceOptimizationAttribute 特性作用范围

PerformanceOptimizationAttribute 特性适用于方法或接口。

PerformanceOptimizationAttribute 包含以下构造函数和属性:

  • 构造函数
    • new():作用于方法或接口,启用性能优化。
    • new(enabled):作用于方法或接口,设置是否启用性能优化。
  • 属性
    • Enabled:是否启用(bool 类型),默认值为 true(启用)。

19.5.23 设置自动 Host 标头

Host 标头是 HTTP/1.1 协议中的一个必需标头。Host 标头用于指定请求的目标服务器的主机名和端口号,确保服务器能正确区分同一 IP 地址上的不同域名并进行相应处理。框架提供了简便的方法进行设置:

HTTP 声明式请求通过 AutoSetHostHeaderAttribute 特性来设置自动 Host 标头。相应的 HTTP 声明式提取器实现为 AutoSetHostHeaderDeclarativeExtractor 类型,该类型负责解析 AutoSetHostHeaderAttribute 特性并构建 HttpRequestBuilder 实例所需的自动 Host 标头配置。

// 在接口定义上应用,影响所有方法
[AutoSetHostHeader] // 启用
public interface IHttpService : IHttpDeclarative
{
// 在方法上应用
[AutoSetHostHeader]
[Get("https://furion.net/")]
Task<string> GetStringAsync();

[AutoSetHostHeader(false)] // 关闭自动 Host 标头
[Get("https://furion.net/")]
Task<string> GetStringAsync();
}

启用后,发送 HTTP 远程请求时会自动添加 Host: furion.net 标头。

小提示

当对接旧程序提供的 API 接口时,建议启用该配置以提升兼容性。

HttpClient 自动重定向导致的 Host 问题

在发送 HTTP 远程请求时,如果目标服务器返回重定向响应(如 301 Moved Permanently302 Found),框架默认会自动跟随重定向。然而,当启用自动 Host 标头时,可能会遇到无法更新 Host 标头的问题。此时,可以关闭 AllowAutoRedirect 选项,使框架能够正确处理重定向:

// 配置默认客户端
services.AddHttpClient(string.Empty)
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
AllowAutoRedirect = false
});

另外,如需设置框架内置重定向行为的最大重定向次数,可以使用以下方式:

services.AddHttpRemote(builder => {})
.ConfigureOptions(options =>
{
MaximumAutomaticRedirections = 20;
});

这样,可以确保重定向行为符合预期,同时避免 Host 标头设置错误的问题。

AutoSetHostHeaderAttribute 特性作用范围

AutoSetHostHeaderAttribute 特性适用于方法或接口。

AutoSetHostHeaderAttribute 包含以下构造函数和属性:

  • 构造函数
    • new():作用于方法或接口,设置自动 Host 标头。
    • new(enabled):作用于方法或接口,设置是否设置自动 Host 标头。
  • 属性
    • Enabled:是否启用(bool 类型),默认值为 true(启用)。

19.5.24 设置请求基地址

当需要对接多个第三方 API 时,我们通常会全局注册并配置多个 HttpClient 实例的 BaseAddress。例如:

// 配置默认客户端的基地址
services.AddHttpClient(string.Empty, client =>
{
client.BaseAddress = new Uri("https://furion.net/");
});

// 配置GitHub客户端的基地址
services.AddHttpClient("github", client =>
{
client.BaseAddress = new Uri("https://github.com/");
});

此外,框架还提供了局部设置基地址的方法,允许在构建请求时动态指定。

HTTP 声明式请求通过 BaseAddressAttribute 特性来设置请求基地址。相应的 HTTP 声明式提取器实现为 BaseAddressDeclarativeExtractor 类型,该类型负责解析 BaseAddressAttribute 特性并构建 HttpRequestBuilder 实例所需的请求基地址配置。

// 在接口定义上应用,影响所有方法
[BaseAddress("https://furion.net")]
public interface IHttpService : IHttpDeclarative
{
[Get("/api/test")]
Task<string> GetStringAsync();

// 在方法上应用
[BaseAddress("https://baiqian.com")]
[Get("/api/test/2")]
Task<string> GetStringAsync();
}

启用后,发送 HTTP 远程请求时会自动设置请求基地址。

特别说明

请确保设置的请求基地址是绝对路径,即以 http://https:// 开头。

处理逻辑说明

  • 若请求地址为绝对路径,则直接使用该地址发送请求。
  • 若请求地址为相对路径且未设置局部 BaseAddress,则拼接全局 HttpClient 实例的 BaseAddress 作为请求地址。
  • 若请求地址为相对路径且已设置局部 BaseAddress,则拼接该局部 BaseAddress 作为请求地址。
BaseAddressAttribute 特性作用范围

BaseAddressAttribute 特性适用于方法或接口。

BaseAddressAttribute 包含以下构造函数和属性:

  • 构造函数
    • new(baseAddress):作用于方法或接口,设置请求基地址。
  • 属性
    • BaseAddress:请求基地址(string 类型)。
配置参数支持

请求基地址支持配置参数,用于读取配置信息进行替换操作。配置参数使用 [[key]] 语法。

19.5.25 设置来源地址(防盗链)

当访问某些第三方服务器时,服务器可能会验证请求头中的 Referer 来源地址。例如在下载图片时,可能因触发防盗链机制导致获取的图片不符合预期。此时,可通过设置 Referer 请求头,模拟来源页面以绕过防盗链检测。

HTTP 声明式请求通过 RefererAttribute 特性来设置请求来源地址。相应的 HTTP 声明式提取器实现为 RefererDeclarativeExtractor 类型,该类型负责解析 RefererAttribute 特性并构建 HttpRequestBuilder 实例所需的请求来源地址配置。

// 在接口定义上应用,影响所有方法
[Referer("https://furion.net")]
public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/logo.png")]
Task<string> GetStringAsync();

// 在方法上应用
[Referer("https://baiqian.com")]
[Get("https://furion.net/logo2.png")]
Task<string> GetStringAsync();
}

启用后,发送 HTTP 远程请求时会自动设置请求来源地址。

为简化配置,框架提供了内置模板字符串 "{BASE_ADDRESS}",可自动提取请求地址的基地址作为 Referer

[Referer("{BASE_ADDRESS}")] // 发送时自动替换 {BASE_ADDRESS} 为 https://furion.net/
public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/logo.png")]
Task<string> GetStringAsync();
}
RefererAttribute 特性作用范围

RefererAttribute 特性适用于方法或接口。

RefererAttribute 包含以下构造函数和属性:

  • 构造函数
    • new(referer):作用于方法或接口,设置请求来源地址。
  • 属性
    • Referer:请求来源地址(string 类型)。

19.5.26 配置 HTTP 版本

在发起 HTTP 远程请求时,默认采用的 HTTP 协议版本为 1.1。不过,在访问部分第三方服务器时,这些服务器可能会对 HTTP 版本进行校验(例如,要求使用 2.0 版本)。

HTTP 声明式请求通过 HttpVersionAttribute 特性来设置请求来源地址。相应的 HTTP 声明式提取器实现为 HttpVersionDeclarativeExtractor 类型,该类型负责解析 HttpVersionAttribute 特性并构建 HttpRequestBuilder 实例所需的 HTTP 版本配置。

// 在接口定义上应用,影响所有方法
[HttpVersion("1.2")]
public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/logo.png")]
Task<string> GetStringAsync();

// 在方法上应用
[HttpVersion("2.0")]
[Get("https://furion.net/logo2.png")]
Task<string> GetStringAsync();
}

启用后,发送 HTTP 远程请求时会自动设置 HTTP 版本。

除可通过 [HttpVersion] 特性进行配置外,系统还支持全局设置方式,具体示例如下:

// 配置默认客户端
services.AddHttpClient(string.Empty, client =>
{
client.DefaultRequestVersion = HttpVersion.Version10;
});

// 配置特定客户端
services.AddHttpClient("weixin", client =>
{
client.DefaultRequestVersion = HttpVersion.Version10;
});
HttpVersionAttribute 特性作用范围

HttpVersionAttribute 特性适用于方法或接口。

HttpVersionAttribute 包含以下构造函数和属性:

  • 构造函数
    • new(version):作用于方法或接口,设置 HTTP 版本。
  • 属性
    • VersionHTTP 版本(string 类型)。

19.5.27 异常抑制机制(静默处理)

在发起 HTTP 远程请求时,可能会遇到以下异常情况:

  • 目标主机不可达
  • 请求被取消
  • 请求超时
  • 其他网络异常

默认情况下,这些异常会中断程序执行。虽然开发者通常使用 try/catch 进行异常处理,但在某些场景下,我们更希望异常发生时静默返回 null 而不中断流程。为此,框架提供了灵活的异常抑制功能。

HTTP 声明式请求通过 SuppressExceptionsAttribute 特性来设置请求来源地址。相应的 HTTP 声明式提取器实现为 SuppressExceptionsDeclarativeExtractor 类型,该类型负责解析 SuppressExceptionsAttribute 特性并构建 HttpRequestBuilder 实例所需异常抑制配置。

// 在接口定义上应用,影响所有方法
[SuppressExceptions] // 抑制所有异常
public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/logo.png")]
Task<string> GetStringAsync();

// 在方法上应用
[SuppressExceptions(typeof(TimeoutException), typeof(TaskCanceledException))] // 抑制超时和取消异常
[Get("https://furion.net/logo2.png")]
Task<string> GetStringAsync();

[SuppressExceptions(false)] // 禁用抑制异常(恢复缺省配置)
[Get("https://furion.net/logo2.png")]
Task<string> 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. 异常类型选择建议
    应根据具体业务场景谨慎选择需要抑制的异常类型,避免因过度抑制异常而掩盖潜在问题。

19.5.28 启用参数验证

在调用 HTTP 声明式接口方法时,支持验证传递的参数数据的合法性。

HTTP 声明式请求通过派生自 ValidationAttribute 的特性来启用参数验证。相应的 HTTP 声明式提取器实现为 ValidationDeclarativeExtractor 类型,该类型负责解析派生自 ValidationAttribute 的特性并对传递的参数数据进行合法性验证。

  • 验证单个值和对象数据
public interface IHttpService : IHttpDeclarative
{
// 无需验证参数合法性
[Get("https://furion.net/")]
Task<string> GetStringAsync(string str, object obj);

// 验证参数合法性,支持验证对象模型内部验证特性
[Get("https://furion.net/")]
Task<string> GetStringAsync([Length(10, 20)] string str, [Required] ValidationModel obj);

// 支持为参数添加多个验证规则
[Get("https://furion.net/")]
Task<string> GetStringAsync(
[Required]
[MinLength(2)]
[MaxLength(5)] string str,
[Range(0, 10)] int age);

// 冻结参数类型将被忽略
[Get("https://furion.net/")]
Task<string> GetStringAsync([Required] CancellationToken cancellationToken);
}

// 对象属性验证
public class ValidationModel
{
public int Id { get; set; }

[Required]
[MinLength(3)]
public string? Name { get; set; }
}
  • 验证实现 IValidatableObject 接口的对象数据
public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/")]
Task<string> GetStringAsync(ValidationObject obj);
}

// 实现 IValidatableObject 进行复杂验证
public class ValidationObject : IValidatableObject
{
public int Id { get; set; }

[Required]
[MinLength(3)]
public string? Name { get; set; }

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Id < 0)
{
yield return new ValidationResult("Id must be greater than or equal to 0.", [nameof(Id)]);
}
}
}
  • 验证自定义 ValidationAttribute
public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/")]
Task<string> GetStringAsync([StringEqual("Furion")] string str);
}

// 自定义验证特性
public class StringEqualAttribute : ValidationAttribute
{
public StringEqualAttribute(string value) => Value = value;

public string Value { get; }

/// <inheritdoc />
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.ValidateValueValidator.ValidateObject 方法进行数据验证,支持任意非冻结类型的参数。

派生自 ValidationAttribute 的特性作用范围

派生自 ValidationAttribute 的特性仅适用于参数。

冻结参数类型说明

在系统中,Action<HttpRequestBuilder>Action<HttpMultipartFormDataBuilder>HttpCompletionOption 以及 CancellationToken 被视为冻结参数类型,它们专门服务于特定的操作执行。因此,派生自 ValidationAttribute 的特性作用于这些参数类型时将被忽略。

小知识

C# 支持特性合并,使代码更简洁:

public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/")]
Task<string> GetStringAsync([Required, MinLength(2), MaxLength(5)] string str, [Range(0, 10)] int age)
}

19.5.29 启用 JSON 响应反序列化包装器

在与第三方 API 进行 HTTP 远程通信时,通常会返回统一结构的 JSON 响应,例如 ApiResult<T> 类型,其中实际数据存放在 Data 属性中:

public class ApiResult<T>
{
public bool Success { get; set; }
public T? Data { get; set; } // 实际返回数据
}

HTTP 声明式请求通过 JsonResponseWrappingAttribute 特性来启用 JSON 响应反序列化包装器。相应的 HTTP 声明式提取器实现为 JsonResponseWrappingDeclarativeExtractor 类型,该类型负责解析 JsonResponseWrappingAttribute 特性并构建 HttpRequestBuilder 实例所需的启用 JSON 响应反序列化包装器配置。

在未启用 JSON 响应反序列化包装器功能时,每次调用都需要显式指定 ApiResult<T> 类型:

public interface IHttpService : IHttpDeclarative
{
[Get("https://example.com")]
Task<ApiResult<string>> GetStringAsync();

[Get("https://furion.net/")]
Task<ApiResult<JsonModel>> GetJsonModelAsync();
}

为简化调用流程,可配置 JSON 响应反序列化包装器,使其自动提取 Data 属性内容:

// 配置默认 HTTP 客户端
services.AddHttpClient(string.Empty)
.ConfigureOptions(options =>
{
options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data));
});

配置完成后,通过 [JsonResponseWrapping] 启用该功能,之后只需指定目标数据类型,无需重复声明 ApiResult<T>

// 在接口定义上应用,影响所有方法
[JsonResponseWrapping]
public interface IHttpService : IHttpDeclarative
{
// 默认自动应用
[Get("https://furion.net/")]
Task<string> GetStringAsync();

// 在方法上应用
[JsonResponseWrapping] // 可显示启用(无需)
[Get("https://furion.net/")]
Task<string> GetStringAsync();

[JsonResponseWrapping(false)] // 禁用 JSON 响应反序列化包装器,需传入完整的响应类型
[Get("https://furion.net/")]
Task<ApiResult<JsonModel>> GetJsonModelAsync();
}

框架将在运行时自动创建 ApiResult<string> 实例,并返回其 Data 属性的值。

也可全局启用 JSON 响应反序列化包装器功能,只需设置 UseJsonResponseWrappingtrue

// 配置默认 HTTP 客户端
services.AddHttpClient(string.Empty)
.ConfigureOptions(options =>
{
options.JsonResponseWrapper = new JsonResponseWrapper(typeof(ApiResult<>), nameof(ApiResult<>.Data));
options.UseJsonResponseWrapping = true;
});

全局启用后,所有请求默认使用包装功能:

// [JsonResponseWrapping] // 无需显式设置 [JsonResponseWrapping]
public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/")]
Task<string> GetStringAsync();
}

若需对特定请求禁用该功能,可设置 [JsonResponseWrapping(false)] 特性。

JsonResponseWrappingAttribute 特性作用范围

JsonResponseWrappingAttribute 特性适用于方法或接口。

JsonResponseWrappingAttribute 包含以下构造函数和属性:

  • 构造函数
    • new():作用于方法或接口,启用 JSON 响应反序列化包装器。
    • new(enabled):作用于方法或接口,设置是否启用 JSON 响应反序列化包装器。
  • 属性
    • Enabled:是否启用(bool 类型),默认值为 true(启用)。

19.5.30 设置请求处理程序

IHttpRequestEventHandler 接口允许您定义 HTTP 请求的预处理操作。通过实现该接口,您可以创建自定义的请求处理程序,例如 CustomRequestEventHandler 类:

public class CustomRequestEventHandler : IHttpRequestEventHandler
{
// 在发送 HTTP 请求之前的操作
public void OnPreSendRequest(HttpRequestMessage httpRequestMessage) {}

// 在收到 HTTP 响应之后的操作
public void OnPostReceiveResponse(HttpResponseMessage httpResponseMessage) {}

// 当发送 HTTP 请求发生异常时的操作
public void OnRequestFailed(Exception exception, HttpResponseMessage? httpResponseMessage = null) {}
}

发送 HTTP 请求时,可通过添加 RequestEventHandlerAttribute 特性来设置请求处理程序。

HTTP 声明式请求通过 RequestEventHandlerAttribute 特性来设置请求处理程序。相应的 HTTP 声明式提取器实现为 RequestEventHandlerDeclarativeExtractor 类型,该类型负责解析 RequestEventHandlerAttribute 特性并构建 HttpRequestBuilder 实例所需的请求处理程序配置。

// 在接口定义上应用,影响所有方法
[RequestEventHandler(typeof(CustomRequestEventHandler))]
public interface IHttpService : IHttpDeclarative
{
// 自动应用接口声明
[Get("https://furion.net/")]
Task<string> GetStringAsync();

// 在方法上应用
[RequestEventHandler(typeof(CustomRequestEventHandler))]
[Get("https://furion.net/")]
Task<string> GetStringAsync();
}
RequestEventHandlerAttribute 特性作用范围

RequestEventHandlerAttribute 特性适用于方法或接口。

RequestEventHandlerAttribute 包含以下构造函数和属性:

  • 构造函数
    • new(handlerType):作用于方法或接口,配置请求处理程序。
  • 属性
    • HandlerType:请求处理程序(Type 类型)。

19.5.31 继承与复用

HTTP 声明式请求支持面向对象的封装与继承等特性。可将通用接口定义在父接口中,再由派生接口继承,实现复用。例如:

public interface IHttpBaseService : IHttpDeclarative
{
[Get("https://furion.net/")]
Task<string> GetStringAsync();
}

public interface IHttp1 : IHttpBaseService
{
[Get("https://baiqian.com/")]
Task<string> GetWebsiteAsync();
}

此外,也支持继承未实现 IHttpDeclarative 接口的普通接口,例如:

public interface IHttpNormal
{
[Get("https://baiqian.com/")]
Task<string> GetBaiduAsync();
}

public interface IHttp2 : IHttpNormal, IHttpBaseService
{
// ...
}

借助封装与继承,可以更高效地组织与复用代码。

19.5.32 冻结参数类型

在之前的章节中,我们多次提及冻结参数类型,现在终于可以对其进行深入探讨了。

在系统中,Action<HttpRequestMessage>Action<HttpRequestBuilder>Action<HttpMultipartFormDataBuilder>HttpCompletionOption 以及 CancellationToken 被定义为冻结参数类型,它们专门服务于 HTTP 声明式请求接口,以提供额外的配置和操作功能。这些冻结参数类型能够极大地扩展 HTTP 声明式请求接口的功能,使其覆盖更广泛的使用场景。

  • Action<HttpRequestMessage>

此参数允许开发者在调用 HTTP 声明式接口时,对 HttpClient 发送的 HttpRequestMessage 进行额外的配置。例如,添加自定义的 HTTP 头、设置认证信息等。相应的 HTTP 声明式提取器实现为 HttpRequestMessageDeclarativeExtractor 类型,该类型负责解析单个 Action<HttpRequestMessage> 类型参数,并提供发送请求前的操作。

public interface IHttpService : IHttpDeclarative
{
[Post("https://furion.net/")]
Task<string> PostStringAsync([Query] int id, [Body("application/json")] object body, Action<HttpRequestMessage>? configure = null)
}
// 默认调用
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<HttpRequestBuilder>

此参数允许开发者在调用 HTTP 声明式接口时,对 HttpRequestBuilder 进行额外的配置。例如,添加自定义的 HTTP 头、设置认证信息等。相应的 HTTP 声明式提取器实现为 HttpRequestBuilderDeclarativeExtractor 类型,该类型负责解析单个 Action<HttpRequestBuilder> 类型参数,并提供构建 HttpRequestBuilder 实例额外的配置。

public interface IHttpService : IHttpDeclarative
{
[Post("https://furion.net/")]
Task<string> PostStringAsync([Query] int id, [Body("application/json")] object body, Action<HttpRequestBuilder>? configure = null)
}
// 默认调用
await httpService.PostStringAsync(1, new { id = 1, name = "Furion" });

// 提供更多 HttpRequestBuilder 配置
await httpService.PostStringAsync(1, new { id = 1, name = "Furion" }, builder =>
{
builder.AddJwtBearerAuthentication("your-jwt-token"); // 例如添加 JWT 授权
});
  • Action<HttpMultipartFormDataBuilder>

此参数用于配置多部分表单数据的设置。通过它,开发者可以添加文件、设置文件类型等。相应的 HTTP 声明式提取器实现为 HttpMultipartFormDataBuilderDeclarativeExtractor 类型,该类型负责解析单个 Action<HttpMultipartFormDataBuilder> 类型参数,并提供构建 HttpMultipartFormDataBuilder 多部分表单实例额外的配置。

public interface IHttpService : IHttpDeclarative
{
[Post("https://furion.net/")]
Task<string> PostStringAsync([Multipart] string name, Action<HttpMultipartFormDataBuilder>? configure = null);
}
// 默认调用
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 响应的读取方式。例如,是否等待整个响应内容读取完毕再返回,还是只读取响应头即返回。

public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/")]
Task<Stream> GetStreamAsync([Query] string version, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead);
}
// 默认调用
await httpService.GetStreamAsync("v4");

// 自定义响应读取方式
await httpService.GetStreamAsync("v5", HttpCompletionOption.ResponseHeadersRead);
  • CancellationToken

此参数允许开发者在发送 HTTP 请求时,提供可取消操作的配置。通过它,可以设定请求在特定条件下被取消。

public interface IHttpService : IHttpDeclarative
{
[Get("https://furion.net/")]
Task<Stream> GetStreamAsync(CancellationToken cancellationToken = default);
}
// 默认调用(无法取消)
await httpService.GetStreamAsync("v4");

// 设置 100 毫秒后取消请求
using var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.CancelAfter(100);

await httpService.GetStreamAsync("v5", cancellationTokenSource.Token); // 假设该请求时间大于 100 毫秒

值得注意的是,这些冻结参数类型可以组合使用,并且通常(建议)放在方法参数列表的最后面,作为可选配置。但同一方法参数定义中,同类型的冻结参数必须是唯一的,否则将抛出 InvalidOperationException 异常。

public interface IHttpService : IHttpDeclarative
{
// 支持组合使用
[Post("https://furion.net/")]
Task<string> PostStringAsync([Query] int id, [Body("application/json")] object body,
Action<HttpMultipartFormDataBuilder>? multipartConfigure = null,
Action<HttpRequestBuilder>? configure = null,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
CancellationToken cancellationToken = default);

// Action<HttpRequestBuilder> 类型参数不是唯一的,将抛出异常 ❎
[Post("https://furion.net/")]
Task<string> PostStringAsync([Query] int id, [Body("application/json")] object body,
Action<HttpRequestBuilder>? configure = null,
Action<HttpRequestBuilder>? configure1 = null);
}
冻结参数类型执行顺序

为了确保这些冻结参数类型能够按照预期的顺序执行,它们都实现了 IFrozenHttpDeclarativeExtractor 接口,该接口包含一个 Order 属性,用于指示执行顺序。

它们执行顺序为:Action<HttpRequestMessage> -> Action<HttpMultipartFormDataBuilder> -> Action<HttpRequestBuilder> -> HttpCompletionOption -> CancellationToken

通过这些冻结参数类型,HTTP 声明式请求接口不仅极大地减轻了开发人员编写 HTTP 请求代码的负担,而且使得代码结构更加清晰、易于维护和复用。

19.5.33 自定义 HTTP 声明提取器

19.5 声明式请求 章节中,我们了解到每种特性或参数类型都对应着一种 HTTP 声明式提取器。以下系统预置的特性提取器及其对应的实现:

通过自定义 HTTP 声明式提取器,您可以为 HTTP 声明式接口提供额外的功能。以下是一个自定义 AcceptAttribute 特性及其提取器的示例:

1. 定义 AcceptAttribute 特性

设置 AcceptAttribute 特性作用范围为方法或接口上。

[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 实例。

public sealed class AcceptDeclarativeExtractor : IHttpDeclarativeExtractor
{
// 实现 Extract 方法
public void Extract(HttpRequestBuilder httpRequestBuilder, HttpDeclarativeExtractorContext context)
{
// 获取方法或接口定义的 AcceptAttribute 特性
if (!context.IsMethodDefined<AcceptAttribute>(out var acceptAttribute, true))
{
return;
}

// 设置 Accept 头
httpRequestBuilder.WithHeader("Accept", acceptAttribute.Accept, replace: true);
}
}

提取器 Extract 方法的 context 参数的类型为 HttpDeclarativeExtractorContext,包含以下属性和方法:

  • 属性
    • Method:被调用方法(MethodInfo 类型)。
    • Args:被调用方法的参数值数组(object[] 类型)。
    • MethodMetadata:被调用方法的元数据(HttpDeclarativeMethodMetadata 类型)。
    • Parameters:被调用方法的参数键值字典(IReadOnlyDictionary<ParameterInfo, object?> 类型)。
    • UnFrozenParameters:被调用方法的非冻结类型参数键值字典(IReadOnlyDictionary<ParameterInfo, object?> 类型)。
  • 方法
    • IsFrozenParameter(parameter):判断参数是否是冻结参数类型。
    • IsMethodDefined<TAttribute>(out var attribute, inherit):检查被调用方法是否定义了指定特性。
    • GetMethodDefinedCustomAttributes(inherit, methodScanFirst):获取被调用方法指定特性的所有实例。

3. 在配置中注册自定义提取器

Startup.csProgram.cs 文件中,配置并注册 HttpRemote 服务,以启用自定义 HTTP 声明式提取器功能。

services.AddHttpRemote(builder =>
{
builder.AddHttpDeclarativeExtractors(() => [ new AcceptDeclarativeExtractor() ]);
});

4. 在 HTTP 声明式接口中使用自定义特性

[Accept("text/html")]
public interface IHttpService : IHttpDeclarative
{
// 在方法上应用
[Accept("text/xml")]
[Get("https://furion.net/")]
Task<string> GetStringAsync();
}
小知识

当自定义特性允许在参数中使用时,请务必通过 HttpDeclarativeExtractorContext.IsFrozenParameter(parameter) 方法来排除冻结类型的参数。示例代码如下:

// 通过 UnFrozenParameters 属性返回非冻结类型参数键值对
context.UnFrozenParameters;

// 通过 HttpDeclarativeExtractorContext.IsFrozenParameter 静态方法手动判断
var parameters = context.Parameters.Where(param =>
!HttpDeclarativeExtractorContext.IsFrozenParameter(param.Key) && // 过滤掉冻结类型的参数
param.Key.IsDefined(typeof(YourAttribute), true)).ToArray();

此代码段展示了如何筛选出不包含冻结参数且标记了特定特性的参数数组。

通过上述步骤,您已经成功创建了一个自定义的 HTTP 声明式提取器。这不仅可以增强 HTTP 声明式接口的功能,还可以使代码更加简洁和易于维护。您可以根据自己的需求继续扩展和自定义其他 HTTP 声明式提取器。

如需更多自定义的 HTTP 声明式提取器,请参考框架内置的 HTTP 声明式提取器代码实现。

19.5.34 自定义 HTTP 声明提取器(授权)

以下是一个示例,展示了如何通过自定义 AuthenticationAttributeAllowAnonymousAttribute 特性,并添加相应的提取器,以实现自动授权和匿名访问功能。

1. 定义 AuthenticationAttribute 特性

AuthenticationAttribute 特性应用于方法或接口上。

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)]
public class AuthenticationAttribute : Attribute;

2. 实现 AuthenticationDeclarativeExtractorAllowAnonymousDeclarativeExtractor 提取器

/// <summary>
/// [Authentication] 特性提取器
/// </summary>
public class AuthenticationDeclarativeExtractor : IHttpDeclarativeExtractor
{
/// <inheritdoc />
public void Extract(HttpRequestBuilder httpRequestBuilder, HttpDeclarativeExtractorContext context)
{
// 如果贴了 [AllowAnonymous] 特性则跳过
if (context.IsMethodDefined<AllowAnonymousAttribute>(out _, true)) return;

// 检查是否已经设置了授权信息
if (httpRequestBuilder.AuthenticationHeader is not null) return;

// 添加授权标头(这里可以实现任何授权的逻辑,比如从参数获取 token 等等)
httpRequestBuilder.AddJwtBearerAuthentication(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c");
}
}

/// <summary>
/// [AllowAnonymous] 特性提取器
/// </summary>
public class AllowAnonymousDeclarativeExtractor : IHttpDeclarativeExtractor
{
/// <inheritdoc />
public void Extract(HttpRequestBuilder httpRequestBuilder, HttpDeclarativeExtractorContext context)
{
// 如果没有贴 [AllowAnonymous] 特性则跳过
if (!context.IsMethodDefined<AllowAnonymousAttribute>(out _, true)) return;

// 移除授权标头
httpRequestBuilder.RemoveHeaders("Authorization");
}
}

3. 在配置中注册自定义提取器

Startup.csProgram.cs 文件中,配置并注册 HttpRemote 服务,以启用自定义 HTTP 声明式提取器功能。

services.AddHttpRemote(builder =>
{
// 添加自定义 HTTP 声明式提取器
builder.AddHttpDeclarativeExtractors(() => [ new AuthenticationDeclarativeExtractor(), new AllowAnonymousDeclarativeExtractor() ]);

// 扫描程序集批量添加 HTTP 声明式提取器(推荐)
// builder.AddHttpDeclarativeExtractorsFromAssemblies([ assembly1, assembly2, ... ]); // 若使用 Furion 框架可直接设置 App.Assemblies
});

4. 在 HTTP 声明式接口中使用自定义特性

[Authentication] // 添加全局授权
public interface IAuthService : IHttpDeclarative
{
[Get("https://furion.net/")]
Task<string> GetDataAsync(); // 访问这个接口需要授权

[AllowAnonymous] // 匿名访问
[Get("https://furion.net/")]
Task<string> LoginAsync(string username, string password);
}

当调用 GetDataAsync 方法时,将自动添加授权标头(实现授权)。调用 LoginAsync 方法时,将自动移除授权请求标头(实现匿名访问)。

通过这个示例可以看出,自定义 HTTP 声明提取器为实现复杂的授权逻辑提供了极大的灵活性。

19.5.35 HttpDeclarativeBuilder 构建器(动态构建)

HttpDeclarativeBuilder 构建器是框架提供专门用来动态构建 HTTP 声明式请求所需的各项设置。HttpDeclarativeBuilder 的构造函数是私有的,因此无法直接使用 new 关键字进行实例化,不过,框架提供了 HttpRequestBuilder.Declarative 的多个静态重载方法创建 HttpDeclarativeBuilder 的实例。

HttpRequestBuilder.Declarative(methodInfo, args);

上述代码演示了如何利用 MethodInfo 类型参数和参数数组来动态构建一个 HTTP 声明式请求构建器。这一机制使得我们能够针对任意类型的方法实现 HTTP 声明式请求功能。以下是一个具体的例子:

public class NormalClass
{
[Get("https://furion.net"/)]
public Task<string> GetStringAsync()
{
throw new NotImplementedException(); // 无需实现
}
}

通过以下步骤,我们可以动态地基于 NormalClassGetStringAsync 方法构建 HTTP 声明式请求:

// 获取 NormalClass 类型的 GetStringAsync 方法
var getStringMethod = typeof(NormalClass).GetMethod(nameof(NormalClass.GetStringAsync), BindingFlags.Instance | BindingFlags.Public);

// 发送 HTTP 请求
var str = await httpRemoteService.SendAsAsync<string>(HttpRequestBuilder.Declarative(getStringMethod, []));

通过这种方式,我们实现了对任意类型方法的 HTTP 声明式请求的动态构建。此外,HTTP 声明式请求还支持多种方法,包括但不限于:

httpRemoteService.Declarative(method, args);
await httpRemoteService.DeclarativeAsync<T>(method, args);

httpRemoteService.SendAs(httpDeclarativeBuilder);
await httpRemoteService.SendAsAsync<T>(httpDeclarativeBuilder);