Skip to content

19. 远程请求

📝 模块更新日志 新特性*

+ 新增 远程请求上传文件时可以配置是否对文件名进行转义参数 `escape` 4\.9\.4\.1 ⏱️2024\.06\.17 [60836ff](https://gitee.com/dotnetchina/Furion/commit/60836ff6020ba479a19d5d8f4910a4dfda7f6fe7)
+ 新增 远程请求发送 `application/x-www-form-urlencoded` 请求时支持字符串 `Body` 类型 4\.9\.3\.16 ⏱️2024\.06\.04 [f5c2a20](https://gitee.com/dotnetchina/Furion/commit/f5c2a20e2875f65f5f5035a31ec9addc630f2703)
+ 新增 远程请求支持返回 `HttpResponseModel<T>` 类型,包含 `HttpResponseMessage`、返回值等属性 4\.9\.2\.34 ⏱️2024\.04\.30 [42ccdaa](https://gitee.com/dotnetchina/Furion/commit/42ccdaa6cb1dcd3381ee8788d254bcb2f99acc69)
+ 新增 远程请求代理模式支持 `[BaseAddress]` 特性快速设置 `HttpClient` 客户端 `BaseAddress` 4\.9\.2\.25 ⏱️2024\.04\.19 [ea88c95](https://gitee.com/dotnetchina/Furion/commit/ea88c95eeca855a0692a7516a72f8b10b3e47637)
+ 新增 远程请求且出现异常时输出重试日志 4\.9\.2\.1 ⏱️2024\.03\.29 [e4549eb](https://gitee.com/dotnetchina/Furion/commit/e4549eb6f37b5f5036c69952d1df2284e8e33417)
+ 新增 远程请求支持自动处理状态码 `301、302` 和响应头带 `Location` 4\.9\.1\.27 ⏱️2024\.01\.29 [65aa221](https://gitee.com/dotnetchina/Furion/commit/65aa221ee6b99904b5349dfdad34b701296c0f5c)
+ 新增 远程请求 `[HttpMethod]ToSaveAsync` 下载远程文件并保存到磁盘方法 4\.8\.7\.32 ⏱️2023\.04\.02 [bfd02c1](https://gitee.com/dotnetchina/Furion/commit/bfd02c1a2ce4229e90fc825fe5657ada59e1892f)
+ 新增 远程请求支持 `Content-Type` 为 `text/html` 和 `text/plain` 处理 4\.8\.7\.22 ⏱️2023\.03\.27 [\#I6QMLR](https://gitee.com/dotnetchina/Furion/issues/I6QMLR)
+ 新增 远程请求 `HttpRequestMessage` 拓展方法 `AppendHeaders` 4\.8\.7\.10 ⏱️2023\.03\.14 [\#I6MVHT](https://gitee.com/dotnetchina/Furion/issues/I6MVHT)
+ 新增 远程请求配置 `SetHttpVersion(version)` 配置,可配置 `HTTP` 请求版本,默认为 `1.1` 4\.8\.5\.8 ⏱️2023\.02\.06 [\#I6D64H](https://gitee.com/dotnetchina/Furion/issues/I6D64H)
+ 新增 远程请求 `[QueryString]` 特性添加时间格式化 `Format` 属性 4\.8\.1\.2 ⏱️2022\.11\.24 [!670](https://gitee.com/dotnetchina/Furion/pulls/670)
  • 突破性变化

    • 调整 远程请求 [Method]AsStreamAsync 返回值类型 4.9.1.44 ⏱️2024.03.08 ef03308
    • 问题修复

    • 修复 远程请求解析 Content-Type 参数不准确问题 4.9.5.6 ⏱️2024.09.02 281f496

    • 修复 远程请求不支持设置携带 charsetContent-Type 字符串 4.9.4.10 ⏱️2024.07.30 09ba7fb
    • 修复 远程请求出现 HttpRequestException 异常时 HttpResponseMessage 对象为空问题 4.9.3.15 ⏱️2024.06.04 8d5f30b
    • 修复 远程请求不支持通过 SetHeaders 设置请求内容头信息 4.9.3.6 ⏱️2024.05.20 #I9QLAY d43581f
    • 修复 远程请求 HttpResponseModel<T> 不支持重复读 Response.Content 流问题 4.9.2.35 ⏱️2024.04.30 7ca0650
    • 修复 远程请求 IHttpDispatchProxy 模式配置重试策略无效 4.9.2.1 ⏱️2024.03.29 #I9CK7X
    • 修复 远程请求重试操作出现 The request message was already sent. Cannot send the same request message multiple times. 异常 4.9.1.40 ⏱️2024.03.07 #I96MOY
    • 修复 远程请求解析不标准的响应头 charset 设置导致异常问题 4.9.1.29 ⏱️2024.02.05 d5d03e5
    • 修复 远程请求获取响应 Cookies 被截断问题 4.8.8.54 ⏱️2023.11.08 #I8EV1Z
    • 修复 远程请求上传文件在其他编程语言获取文件名存在双引号问题 4.8.8.53 ⏱️2023.11.07 #I8EF1S
    • 修复 远程请求在被请求端返回非 200 状态码但实际请求已处理也抛异常问题 4.8.8.14 ⏱️2023.05.12 b14a51f
    • 修复 远程请求 Body 参数为粘土对象 Clay 类型序列化有误 4.8.8.1 ⏱️2023.04.18 #I6WKRZ
    • 修复 远程请求获取 Cookies 时如果包含相同 Key 异常问题 4.8.7.44 ⏱️2023.04.12 #I6V3T7
    • 修复 远程请求代理模式配置了 WithEncodeUrl = false 无效问题 4.8.6.4 ⏱️2023.02.16 89639ba
    • 修复 由于 #I6D64H 导致远程请求出现 Specified method is not supported. 问题 4.8.5.9 ⏱️2023.02.07 #I6DEEE #I6D64H
    • 修复 ~~优化远程请求 ReadAsStringAsync 底层方法,尝试修复 Error while copying content to a stream. 错误 4.8.5.8 ⏱️2023.02.06 #I6D64H~~
    • 修复 远程请求配置 WithEncodeUrl(false)application/x-www-form-urlencoded 请求类型无效 4.8.4 ⏱️2022.12.30 #I682DX
    • 其他更改

    • 优化 远程请求适配 Content-Type 逻辑 4.9.3.1 ⏱️2024.05.15 #I9OLV8

    • 优化 远程请求核心类型 HttpRequestMessageHttpResponseMessage 对象创建和销毁方式 4.9.1.43 ⏱️2024.03.08 03034c9
    • 调整 取消远程请求 GET/HEAD 不能传递 Body 的限制 4.8.8.39 ⏱️2023.08.02 8113460
    • 文档

    • 新增 远程请求 [QueryString] 配置时间类型 Format 格式化文档 4.8.1.2 ⏱️2022.11.25 !673

版本说明以下内容仅限 Furion 1.16.0 + 版本使用。

19.1 关于远程请求

在互联网大数据的驱动下,平台或系统免不了需要和第三方进行数据交互,而第三方往往提供了 RESTful API 接口规范,这个时候就需要通过 Http 请求第三方接口进行数据传输交互。

也就是本章节所说的远程请求。

19.2 远程请求的作用

  • 跨系统、跨设备通信
  • 实现多个系统数据传输交互
  • 跨编程语言协同开发

19.3 基础使用

19.3.1 注册服务

使用之前需在 Startup.cs 注册 远程请求服务

public void ConfigureServices(IServiceCollection services)  
{  
    services.AddRemoteRequest();  
}  

19.3.2 使用方式

Furion 提供两种方式访问发送远程请求。

  • IHttpDispatchProxy 代理方式
  • 字符串拓展方式

定义代理请求的 接口 并继承 IHttpDispatchProxy 接口

public interface IHttp : IHttpDispatchProxy  
{  
    [Get("https://furion.net/get")]  
    Task<Result> GetXXXAsync();  

    [Post("https://furion.net/post")]  
    Task<Result> PostXXXAsync();  

    [Put("https://furion.net/put")]  
    Task<Result> PutXXXAsync();  

    [Delete("https://furion.net/delete")]  
    Task<Result> DeleteXXXAsync();  

    [Patch("https://furion.net/patch")]  
    Task<Result> PatchXXXAsync();  

    [Head("https://furion.net/head")]  
    Task<Result> HeadXXXAsync();  
}  

通过构造函数注入 接口

using Furion.DynamicApiController;  
using Furion.RemoteRequest.Extensions;  

namespace Furion.Application  
{  
    public class RemoteRequestService : IDynamicApiController  
    {  
        private readonly IHttp _http;  
        public RemoteRequestService(IHttp http)  
        {  
            _http = http;  
        }  

        public async Task GetData()  
        {  
            var data = await _http.GetXXXAsync();  
        }  
    }  
}  

using var response = await "https://furion.net/get".GetAsync();  

using var response = await "https://furion.net/post".PostAsync();  

using var response = await "https://furion.net/put".PutAsync();  

using var response = await "https://furion.net/delete".DeleteAsync();  

using var response = await "https://furion.net/patch".PatchAsync();  

using var response = await "https://furion.net/head".HeadAsync();  

需引入 using Furion.RemoteRequest.Extensions 命名空间。

19.4 字符串方式使用示例

温馨提示推荐使用 《19.5 代理方式》代替本小节功能。代理方式 能够提供更容易且更易维护的方式。

关于 IPV6如果使用 IPV6,那么 URL 地址格式为:"http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]"

19.4.1 内置请求方式

// 发送 Get 请求  
using var response = await "https://furion.net/get".GetAsync();  

// 发送 Post 请求  
using var response = await "https://furion.net/post".PostAsync();  

// 发送 Put 请求  
using var response = await "https://furion.net/put".PutAsync();  

// 发送 Delete 请求  
using var response = await "https://furion.net/delete".DeleteAsync();  

// 发送 Patch 请求  
using var response = await "https://furion.net/patch".PatchAsync();  

// 发送 Head 请求  
using var response = await "https://furion.net/head".HeadAsync();  

// 手动指定发送特定请求  
using var response = await "https://furion.net/post".SetHttpMethod(HttpMethod.Post)  
                                                  .SendAsync();  

19.4.2 设置请求地址

// 该方式在 Furion v3.0.0 已移除,多此一举了  
await "".SetRequestUrl("https://furion.net/");  

19.4.3 设置请求方式

await "https://furion.net/post".SetHttpMethod(HttpMethod.Get);  

19.4.4 设置地址模板

// 字典方式  
await "https://furion.net/post/{id}?name={name}&id={p.Id}".SetTemplates(new Dictionary<string , object> {  
    { "id", 1 },  
    { "name", "Furion" },  
    { "p.Id", new Person { Id = 1 } }  
});  

// 对象/匿名对象方式  
await "https://furion.net/post/{id}?name={name}".SetTemplates(new {  
    id = 1,  
    name = "Furion"  
});  

注:模板替换区分大小写。

19.4.5 设置请求报文头

// 字典方式  
await "https://furion.net/post".SetHeaders(new Dictionary<string , object> {  
    { "Authorization", "Bearer 你的token"},  
    { "X-Authorization", "Bearer 你的刷新token"}  
});  

// 对象/匿名对象方式  
await "https://furion.net/post".SetHeaders(new {  
    Authorization = "Bearer 你的token"  
});  

19.4.6 设置 URL 地址参数

// 字典方式  
await "https://furion.net/get".SetQueries(new Dictionary<string , object> {  
    { "id", 1 },  
    { "name", "Furion"}  
});  

// 对象/匿名对象方式  
await "https://furion.net/get".SetQueries(new {  
    id = 1,  
    name = "Furion"  
});  

// Furion 4.7.3+ 新增忽略 null 值重载  
await "https://furion.net/get".SetQueries(new {  
    id = 1,  
    name = "Furion",  
    nullValue = default(object)  
}, true);   // 设置 true 则忽略 null 值  

最终输出格式为:https://furion.net/get?id=1&name=Furion

19.4.7 设置请求客户端

  • 全局配置方式
services.AddRemoteRequest(options=>  
{  
    // 配置 Github 基本信息  
    options.AddHttpClient("github", c =>  
    {  
        c.BaseAddress = new Uri("https://api.github.com/");  
        c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");  
        c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");  
    });  
});  

await "get".SetClient("github");  

最终生成请求地址为:https://api.github.com/get

  • 局部配置方式

版本说明以下内容仅限 Furion 4.3.8 + 版本使用。

await "https://furion.net".SetClient(() => new HttpClient());  

19.4.8 设置 Body 参数

// 传入对象  
await "https://furion.net/api/user/add".SetBody(new User { Id = 1, Name = "Furion" });  

// 配置 Content-Type  
await "https://furion.net/api/user/add".SetBody(new { Id = 1, Name = "Furion" }, "application/json");  

// 设置 Encoding 编码  
await  "https://furion.net/api/user/add".SetBody(new User { Id = 1, Name = "Furion" }, "application/json", Encoding.UTF8);  

// 处理 application/x-www-form-urlencoded 请求  
await "https://furion.net/api/user/add".SetBody(new Dictionary<string , object> {  
    { "Id", 1 },  
    { "Name", "Furion"}  
}, "application/x-www-form-urlencoded");  

// 处理 application/xml、text/xml  
await "https://furion.net/api/user/add".SetBody("<SomeDto><SomeTag>somevalue</SomeTag></SomeDto>", "application/xml");  

特别注意如果请求 Content-Type 设置为 application/x-www-form-urlencoded 类型,那么底层自动将数据进行 UrlEncode 编码处理,无需外部处理。

19.4.9 设置 Content-Type

await "https://furion.net/post".SetContentType("application/json");  

19.4.10 设置内容编码

await "https://furion.net/post".SetContentEncoding(Encoding.UTF8);  

19.4.11 设置 JSON 序列化提供程序

Furion 默认情况下采用 System.Text.Json 进行 JSON 序列化处理,如需设置第三方 JSON 提供器,则可以通过以下配置:

// 泛型方式  
await "https://furion.net/api/user/add".SetJsonSerialization<NewtonsoftJsonSerializerProvider>();  

// 非泛型方式  
await "https://furion.net/api/user/add".SetJsonSerialization(typeof(NewtonsoftJsonSerializerProvider));  

// 添加更多配置  
await "https://furion.net/api/user/add".SetJsonSerialization<NewtonsoftJsonSerializerProvider>(new JsonSerializerSettings {  

});  

// 比如配置缺省的序列化选项  
await "https://furion.net".SetJsonSerialization(default, new JsonSerializerOptions  
    {  
        // 中文乱码  
        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping  
    })  
    .GetAsAsync();  

关于 JSON 序列化提供器如需了解更多 JSON 序列化知识可查阅 23. JSON 序列化 章节

19.4.12 启用 Body 参数验证

await "https://furion.net/api/user/add".SetValidationState();  

// 设置不验证 null 值  
await "https://furion.net/api/user/add".SetValidationState(includeNull: true);  

支持类中 [Required] 等完整模型验证特性。

19.4.13 请求拦截

await "https://furion.net/".OnRequesting((client, req) => {  
    // req 为 HttpRequestMessage 对象  
    // 追加更多参数  
    req.AppendQueries(new Dictionary<string, object> {  
        { "access_token", "xxxx"}  
    });  
});  

支持多次拦截

19.4.14 HttpClient 拦截

await "https://furion.net/".OnClientCreating(client => {  
    // client 为 HttpClient 对象  
    client.Timeout = TimeSpan.FromSeconds(30); // 设置超时时间  
});  

支持多次拦截

19.4.15 请求之前拦截

await "https://furion.net/".OnRequesting((client, req) => {  
    // req 为 HttpRequestMessage 对象  
});  

支持多次拦截

19.4.16 成功请求拦截

await "https://furion.net/".OnResponsing((client, res) => {  
    // res 为 HttpResponseMessage 对象  
});  

支持多次拦截

19.4.17 请求异常拦截

await "https://furion.net/".OnException((client, res, errors) => {  
    // res 为 HttpResponseMessage 对象  
});  

支持多次拦截

19.4.18 各种返回值处理

Furion 远程请求默认提供五种返回值类型:

  • HttpResponseMessage:请求响应消息类型
  • Stream:流类型,可用来下载文件
  • T:泛型 T 类型
  • String:字符串类型,也就是直接将网络请求结果内容字符串化
  • Byte[]:字节数组类型
  • HttpResponseModel<T>:请求响应模型,Furion 4.9.2.34+ 版本支持,T类型仅支持 Stream、String、Byte[]和自定义类型,不支持传入自身类型和 HttpResponseMessage

如:

// HttpResponseMessage  
using var response = await "https://furion.net/".GetAsync();  

// Stream,可用来下载文件  
var (stream, encoding) = await "https://furion.net/".GetAsStreamAsync();  
var (stream, encoding, response) = await "https://furion.net/".GetAsStreamAsync();  // Furion 4.9.1.44+  

// T  
var user = await "https://furion.net/".GetAsAsync<User>();  

// String  
var str = await "https://www.baidu.com".GetAsStringAsync();  

// Byte[]  
var bytes = await "https://www.baidu.com".GetAsByteArrayAsync();  

// HttpResponseModel<T> 类型,Furion 4.9.2.34+ 版本支持  
using var httpResponseModel1 = await "https://furion.net/".GetAsync<HttpResponseModel<string>>();  
using var httpResponseModel2 = await "https://furion.net/".GetAsync<HttpResponseModel<Stream>>();  
using var httpResponseModel3 = await "https://furion.net/".GetAsync<HttpResponseModel<byte[]>>();  
using var httpResponseModel4 = await "https://furion.net/".GetAsync<HttpResponseModel<User>>();  

19.4.19 设置 Byte[]/Stream 类型/上传文件

Furion 4.4.0 以下版本在 Furion 4.4.0+ 版本移除了 .SetBodyBytes 方式,原因是拓展性太差,新版本请使用 .SetFiles 方式

有时候我们需要上传文件,需要设置 Content-Typemultipart/form-data 类型,如:

// 支持单文件,bytes 可以通过 File.ReadAllBytes(文件路径) 获取  
var res = await "https://furion.net/upload".SetContentType("multipart/form-data")  
                                               .SetBodyBytes(("键", bytes, "文件名")).PostAsync();  

// 支持多个文件  
var res = await "https://furion.net/upload".SetContentType("multipart/form-data")  
                                               .SetBodyBytes(("键", bytes, "文件名"),("键", bytes, "文件名")).PostAsync();  

// 支持单文件,Furion 4.5.8 版本支持 Stream 方式更新  
var res = await "https://furion.net/upload".SetContentType("multipart/form-data")  
                                               .SetBodyBytes(("键", fileStream, "文件名")).PostAsync();  

// 支持多个文件,Furion 4.5.8 版本支持 Stream 方式更新  
var res = await "https://furion.net/upload".SetContentType("multipart/form-data")  
                                               .SetBodyBytes(("键", fileStream, "文件名"),("键", fileStream, "文件名")).PostAsync();  

关于微信上传接口如果遇到微信上传出现问题,则可设置 Content-Type 为:application/octet-stream,如:

var result = await $"https://api.weixin.qq.com/wxa/img_sec_check?access_token={token}"  
                .SetBodyBytes(("media", bytes, Path.GetFileName(imgPath)))  
                .SetContentType("application/octet-stream")  
                .PostAsStringAsync();  

Furion 4.4.0+ 版本如果使用 Furion 4.4.0+ 版本,请使用以下的 .SetFiles 替代 .SetBodyBytes 操作。

// bytes 可以通过 File.ReadAllBytes(文件路径) 获取  
var res = await "https://furion.net/upload".SetContentType("multipart/form-data")  
                                               .SetFiles(HttpFile.Create("file", bytes, "image.png")).PostAsync();  

// 支持多个文件  
var res = await "https://furion.net/upload".SetContentType("multipart/form-data")  
                                               .SetFiles(HttpFile.CreateMultiple("files", (bytes, "image1.png"), (bytes, "image2.png"))).PostAsync();  

19.4.20 设置 IServiceProvider

有时候我们需要构建一个作用域的 IServiceProvider,这时只需要设置即可:

var res = await "https://furion.net/upload".SetRequestScoped(services);  

19.4.21 支持模板配置

模板格式为:#(配置路径)

var res = await "#(Furion:Address)/upload".GetAsync();  

{  
  "Furion": {  
    "Address": "https://furion.net"  
  }  
}  

19.4.22 重试策略

Furion v2.18+ 版本支持配置重试策略,如:

var res = await "https://furion.net".SetRetryPolicy(3, 1000).GetAsync();  

以上代码表示请求失败重试 3 次,每次延迟 1000ms

19.4.23 支持 GZip 压缩

Furion v3.2.0+ 版本支持GZip 压缩,如:

var res = await "https://furion.net".WithGZip().GetAsync();  

19.4.24 设置 Url 转码

过去版本会对所有的 Url 进行 Uri.EscapeDataString 转码,在 Furion v3.8.0+ 版本支持 Url 转码设置,如:

var res = await "https://furion.net".WithEncodeUrl(false).GetAsync();  

19.4.25 设置 HTTP 版本

可解决一些 HTTPHTTPS 请求问题。

var res = await "https://furion.net".SetHttpVersion("1.0").GetAsync();  // Furion 4.8.5.8+ 支持  

19.4.26 下载远程文件并保存

版本说明以下内容仅限 Furion 4.8.7.32 + 版本使用。

await "https://furion.net/img/rm1.png".GetToSaveAsync("D:/rm3.png");  
await "https://furion.net/img/rm1.png".PostToSaveAsync("D:/rm3.png");  
...  

19.4.27 设置 Cookies

  • 方式一
await "https://furion.net/getuser"  
    .OnRequesting((client, request) =>  
    {  
        request.Headers.Add("Cookie", "username=monksoul"); // 多个使用分号 ; 追加  
    })  
    .GetAsync();  

  • 方式二(推荐)
await "https://furion.net/getuser"  
    .OnRequesting((client, request) =>  
    {  
        var cookieContainer = new CookieContainer();  
        cookieContainer.Add(request.RequestUri, new Cookie("username", "monksoul"));    // 重复 .Add 操作可设置多个  
        request.Headers.Add("Cookie", cookieContainer.GetCookieHeader(request.RequestUri));  
    })  
    .GetAsync();  

  • 方式三
// 创建 HttpClientHandler 实例并设置 CookieContainer  
var handler = new HttpClientHandler();  
var cookieContainer = new CookieContainer();  
cookieContainer.Add("https://furion.net", new Cookie("username", "monksoul"));  // 重复 .Add 操作可设置多个  
handler.CookieContainer = cookieContainer;  

await "https://furion.net/getuser"  
    .SetClient(() => new HttpClient(handler));  
    .GetAsync();  

19.4.28 设置客户端 BaseAddress

版本说明以下内容仅限 Furion 4.9.2.25 + 版本使用。

在最新版本中,Furion 框架提供了便捷设置客户端 BaseAddress 的方法 .SetBaseAddress(),如:

var res = await "api/get-user/10".SetBaseAddress("https://furion.net").GetAsync();  // 最后生成的 Url 地址为:https://furion.net/api/get-user/10  

19.5 IHttpDispatchProxy 代理方式

关于 IPV6如果使用 IPV6,那么 URL 地址格式为:"http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]"

19.5.1 支持多种代理方式

public interface IHttp : IHttpDispatchProxy  
{  
    // 发送 Get 请求  
    [Get("https://furion.net/get")]  
    Task<HttpResponseMessage> GetXXXAsync();  

    // 发送 Post 请求  
    [Post("https://furion.net/post")]  
    Task<HttpResponseMessage> PostXXXAsync();  

    // 发送 Put 请求  
    [Put("https://furion.net/put")]  
    Task<HttpResponseMessage> PutXXXAsync();  

    // 发送 Delete 请求  
    [Delete("https://furion.net/delete")]  
    Task<HttpResponseMessage> DeleteXXXAsync();  

    // 发送 Patch 请求  
    [Patch("https://furion.net/patch")]  
    Task<HttpResponseMessage> PatchXXXAsync();  

    // 发送 Head 请求  
    [Head("https://furion.net/head")]  
    Task<HttpResponseMessage> HeadXXXAsync();  
}  

19.5.2 设置地址模板

public interface IHttp : IHttpDispatchProxy  
{  
    [Get("https://furion.net/get/{id}?name={name}&number={p.PersonDetail.PhonNumber}")]  
    Task<HttpResponseMessage> GetXXXAsync(int id, string name, Person p);  
}  

注:模板替换区分大小写。

19.5.3 设置请求报文头

Furion 框架远程请求代理模式提供三种方式设置请求报文头:

  • 支持在接口中声明
  • 支持在方法中声明
  • 支持在参数中声明
[Headers("key","value")]  
[Headers("key1","value2")] // 设置多个  
public interface IHttp : IHttpDispatchProxy  
{  
    [Get("https://furion.net/get/{id}?name={name}"), Headers("key2","value2")]  
    Task<HttpResponseMessage> GetXXXAsync(int id, string name);  

    [Get("https://furion.net")]  
    Task<HttpResponseMessage> GetXXX2Async(int id, [Headers]string token = default);  

    [Get("https://furion.net")]  
    Task<HttpResponseMessage> GetXXX2Async(int id, string name, [Headers("别名")]string token = default);  

    [Get("https://furion.net")]  
    Task<HttpResponseMessage> GetXXX2Async(int id, string name, [Headers]Dictionary<string, object> headers = default);  
}  


如需动态设置,可使用以下方式(添加参数拦截器):

public interface IHttp : IHttpDispatchProxy  
{  
    // 通过参数拦截  
    [Post("https://furion.net/post")]  
    Task<HttpResponseMessage> PostXXXAsync(string name, [Interceptor(InterceptorTypes.Request)] Action<HttpClient, HttpRequestMessage> action = default);  
}  

调用:

await _http.PostXXXAsync("百小僧", (client, requestMessage) =>  
{  
    requestMessage.AppendHeaders(new Dictionary<string , object> {  
        { "Authorization", "Bearer 你的token"},  
        { "X-Authorization", "Bearer 你的刷新token"}  
    });  

    // 也支持对象,匿名方式  
    requestMessage.AppendHeaders(new {  
        Authorization = "Bearer 你的token",  
        Others = "其他"  
    });  

    // 也可以使用原生  
    requestMessage.Headers.TryAddWithoutValidation("Authorization", "Bearer 你的token");  
    requestMessage.Headers.TryAddWithoutValidation("key", "value");  
});  

19.5.4 设置 URL 地址参数

public interface IHttp : IHttpDispatchProxy  
{  
    [Get("https://furion.net/get/{id}?name={name}")]  
    Task<HttpResponseMessage> GetXXXAsync(int id, string name);  

    [Get("https://furion.net/get/{p.Id}?name={p.Name}")]  
    Task<HttpResponseMessage> GetXXXAsync(Person p);  

    [Get("https://furion.net/get")]  
    Task<HttpResponseMessage> GetXXXAsync([QueryString]int id, [QueryString]string name);  

    [Get("https://furion.net/get")]  
    Task<HttpResponseMessage> GetXXXAsync([QueryString]int id, [QueryString("别名")]string name);  

    // Furion 4.8.1.4 新增 [QueryString(Format)] 配置时间类型格式化  
    [Get("https://furion.net/get")]  
    Task<HttpResponseMessage> GetXXXAsync([QueryString(Format = "yyyy-MM-dd HH:mm:ss")] DateTime queryStartTime, [QueryString(Format = "yyyy-MM-dd HH:mm:ss")] DateTime queryEndTime);  

    // Furion 4.8.1.4 新增 [QueryString(Format)] 配置时间类型格式化  
    [Get("https://furion.net/get")]  
    Task<HttpResponseMessage> GetXXXAsync([QueryString(Format = "yyyy-MM-dd HH:mm:ss")] DateTimeOffset queryStartTime, [QueryString(Format = "yyyy-MM-dd HH:mm:ss")] DateTimeOffset queryEndTime);  

    // Furion 4.7.3 新增 IgnoreNullValueQueries 配置忽略空值  
    [Get("https://furion.net/get", IgnoreNullValueQueries = true)]  
    Task<HttpResponseMessage> GetXXXAsync([QueryString]int id, [QueryString]string name, [QueryString]string nullValue);  
}  

最终输出格式为:https://furion.net/get?id=1&name=Furion

关于对象类型直接作为模板参数在对接某些第三方接口的时候可能遇到一种情况,需要把对象序列化或者进行某种处理后作为 Url 参数,如:

[Get("https://furion.net/get?json={p}", WithEncodeUrl = false)]    // 这里将 p 作为模板传入  
Task<HttpResponseMessage> GetXXXAsync(Person p);  

如果 Person 类型不做任何处理,那么最终传递的是 Person 的命名空间:https://furion.net/get?json=YourProject.Person,但这并非是我们的预期。

这个时候我们只需要重写 PersonToString 方法即可,如:

public class Person  
{  
    public string Name { get; set; }  

    public override string ToString()  
    {  
        return JsonSerializer.Serialize(this);  // 比如这里做序列化处理  
        // 如果基类中 override,可使用 return JsonSerializer.Serialize<object>(this);  
    }  
}  

19.5.5 设置请求客户端

  • 全局配置方式
services.AddRemoteRequest(options=>  
{  
    // 配置 Github 基本信息  
    options.AddHttpClient("github", c =>  
    {  
        c.BaseAddress = new Uri("https://api.github.com/");  
        c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");  
        c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");  
    });  
});  

[Client("github")]  // 可以在接口中全局使用  
public interface IHttp : IHttpDispatchProxy  
{  
    [Get("get"), Client("github")]  // 也可以在方法中局部使用  
    Task<HttpResponseMessage> GetXXXAsync();  
}  

最终生成请求地址为:https://api.github.com/get

  • 局部配置方式

版本说明以下内容仅限 Furion 4.3.8 + 版本使用。

public interface IHttp : IHttpDispatchProxy  
{  
    // 局部方式  
    [Get("get")]  
    Task<HttpResponseMessage> GetXXXAsync([Interceptor(InterceptorTypes.Initiate)]Func<HttpClient> clientProvider);  

    // 全局静态方式  
    [Interceptor(InterceptorTypes.Initiate)]  
    static HttpClient CreateHttpClient()  
    {  
        return new HttpClient(...);  
    }  
}  

19.5.6 设置 Body 参数

public interface IHttp : IHttpDispatchProxy  
{  
    [Post("https://furion.net/post")]  
    Task<HttpResponseMessage> PostXXXAsync([Body]User user);  

    [Post("https://furion.net/post")]  
    Task<HttpResponseMessage> PostXXXAsync([Body("application/x-www-form-urlencoded")]User user);  

    [Post("https://furion.net/post")]  
    Task<HttpResponseMessage> PostXXXAsync([Body("application/x-www-form-urlencoded", "UTF-8")]User user);  
}  

19.5.7 设置 JSON 序列化提供程序

Furion 默认情况下采用 System.Text.Json 进行 JSON 序列化处理,如需设置第三方 JSON 提供器,则可以通过以下配置:

public interface IHttp : IHttpDispatchProxy  
{  
    [Post("https://furion.net/post"), JsonSerialization(typeof(NewtonsoftJsonSerializerProvider))]  
    Task<HttpResponseMessage> PostXXXAsync([Body]User user);  

    [Post("https://furion.net/post"), JsonSerialization(typeof(NewtonsoftJsonSerializerProvider))]  
    Task<HttpResponseMessage> PostXXXAsync([Body]User user, [JsonSerializerOptions]object jsonSerializerOptions = default);  

    /// <summary>  
    /// 缺省序列化配置  
    /// </summary>  
    /// <returns></returns>  
    [JsonSerializerOptions]  
    static object GetJsonSerializerOptions()  
    {  
        // 这里也可以通过 JSON.GetSerializerOptions<JsonSerializerOptions>() 获取 Startup.cs 中的配置  
        return new JsonSerializerOptions  
        {  

        };  
    }  
}  

[JsonSerializerOptions] 可以标记参数是一个 JSON 序列化配置参数。

关于 JSON 序列化提供器如需了解更多 JSON 序列化知识可查阅 23. JSON 序列化 章节

19.5.8 参数验证

public interface IHttp : IHttpDispatchProxy  
{  
    [Post("https://furion.net/post")]  
    Task<HttpResponseMessage> PostXXXAsync([Range(1,10)]int id, [Required, MaxLength(10)]string name);  

    [Post("https://furion.net/post")]  
    Task<HttpResponseMessage> PostXXXAsync([Required]User user);  // 对象类型支持属性配置特性验证  
}  

19.5.9 请求拦截

Furion 远程请求代理方式提供两种拦截方式:

  • 接口静态方法拦截
  • 参数标记拦截
public interface IHttp : IHttpDispatchProxy  
{  
    // 通过参数拦截  
    [Post("https://furion.net/post")]  
    Task<HttpResponseMessage> PostXXXAsync([Interceptor(InterceptorTypes.Request)] Action<HttpClient, HttpRequestMessage> action = default);  

    // 全局拦截,类中每一个方法都会触发  
    [Interceptor(InterceptorTypes.Request)]  
    static void OnRequesting1(HttpClient client, HttpRequestMessage req)  
    {  
        // 追加更多参数  
        req.AppendQueries(new Dictionary<string, object> {  
            { "access_token", "xxxx"}  
        });  
    }  

    // 全局拦截,类中每一个方法都会触发  
    [Interceptor(InterceptorTypes.Request)]  
    static void OnRequesting2(HttpClient client, HttpRequestMessage req)  
    {  

    }  
}  

支持多次拦截

19.5.10 HttpClient 拦截

Furion 远程请求代理方式提供两种拦截方式:

  • 接口静态方法拦截
  • 参数标记拦截
public interface IHttp : IHttpDispatchProxy  
{  
    // 通过参数拦截  
    [Post("https://furion.net/post")]  
    Task<HttpResponseMessage> PostXXXAsync([Interceptor(InterceptorTypes.Client)] Action<HttpClient> action = default);  

    // 全局拦截,类中每一个方法都会触发  
    [Interceptor(InterceptorTypes.Client)]  
    static void onClientCreating1(HttpClient client)  
    {  

    }  

    // 全局拦截,类中每一个方法都会触发  
    [Interceptor(InterceptorTypes.Client)]  
    static void onClientCreating2(HttpClient client)  
    {  

    }  
}  

支持多次拦截

19.5.11 请求之前拦截

Furion 远程请求代理方式提供两种拦截方式:

  • 接口静态方法拦截
  • 参数标记拦截
public interface IHttp : IHttpDispatchProxy  
{  
    // 通过参数拦截  
    [Post("https://furion.net/post")]  
    Task<HttpResponseMessage> PostXXXAsync([Interceptor(InterceptorTypes.Request)] Action<HttpClient, HttpRequestMessage> action = default);  

    // 全局拦截,类中每一个方法都会触发  
    [Interceptor(InterceptorTypes.Request)]  
    static void OnRequest1(HttpClient client, HttpRequestMessage req)  
    {  

    }  

    // 全局拦截,类中每一个方法都会触发  
    [Interceptor(InterceptorTypes.Request)]  
    static void OnRequest2(HttpClient client, HttpRequestMessage req)  
    {  

    }  
}  

支持多次拦截

19.5.12 成功请求拦截

Furion 远程请求代理方式提供两种拦截方式:

  • 接口静态方法拦截
  • 参数标记拦截
public interface IHttp : IHttpDispatchProxy  
{  
    // 通过参数拦截  
    [Post("https://furion.net/post")]  
    Task<HttpResponseMessage> PostXXXAsync([Interceptor(InterceptorTypes.Response)] Action<HttpClient, HttpResponseMessage> action = default);  

    // 全局拦截,类中每一个方法都会触发  
    [Interceptor(InterceptorTypes.Response)]  
    static void OnResponsing1(HttpClient client, HttpResponseMessage res)  
    {  

    }  

    // 全局拦截,类中每一个方法都会触发  
    [Interceptor(InterceptorTypes.Response)]  
    static void OnResponsing2(HttpClient client, HttpResponseMessage res)  
    {  

    }  
}  

支持多次拦截

19.5.13 请求异常拦截

Furion 远程请求代理方式提供两种拦截方式:

  • 接口静态方法拦截
  • 参数标记拦截
public interface IHttp : IHttpDispatchProxy  
{  
    // 通过参数拦截  
    [Post("https://furion.net/post")]  
    Task<HttpResponseMessage> PostXXXAsync([Interceptor(InterceptorTypes.Exception)] Action<HttpClient, HttpResponseMessage, string> action = default);  

    // 全局拦截,类中每一个方法都会触发  
    [Interceptor(InterceptorTypes.Exception)]  
    static void OnException1(HttpClient client, HttpResponseMessage res, string errors)  
    {  

    }  

    // 全局拦截,类中每一个方法都会触发  
    [Interceptor(InterceptorTypes.Exception)]  
    static void OnException2(HttpClient client, HttpResponseMessage res, string errors)  
    {  

    }  
}  

支持多次拦截

19.5.14 各种返回值处理

Furion 远程请求默认提供五种返回值类型:

  • HttpResponseMessage:请求响应消息类型
  • Stream:流类型,可用来下载文件
  • T:泛型 T 类型
  • String:字符串类型,也就是直接将网络请求结果内容字符串化
  • Byte[]:字节数组类型
  • HttpResponseModel<T>:请求响应模型,Furion 4.9.2.34+ 版本支持,T类型仅支持 Stream、String、Byte[]和自定义类型,不支持传入自身类型和 HttpResponseMessage

如:

public interface IHttp : IHttpDispatchProxy  
{  
    // HttpResponseMessage  
    [Post("https://furion.net/post")]  
    Task<HttpResponseMessage> PostXXXAsync();  

    // Stream,可用来下载文件  
    [Post("https://furion.net/post")]  
    Task<Stream> PostXXXAsync();  

    // T  
    [Post("https://furion.net/post")]  
    Task<User> PostXXXAsync();  

    // String  
    [Post("https://furion.net/post")]  
    Task<string> PostXXXAsync();  

    // Byte[]  
    [Post("https://furion.net/post")]  
    Task<byte[]> PostXXXAsync();  

    // HttpResponseModel<T> 类型,Furion 4.9.2.34+ 版本支持  
    [Post("https://furion.net/post")]  
    Task<HttpResponseModel<string>> PostXXXAsync();  
    Task<HttpResponseModel<Stream>> PostXXXAsync();  
    Task<HttpResponseModel<byte[]>> PostXXXAsync();  
    Task<HttpResponseModel<User>> PostXXXAsync();  
}  

19.5.15 设置 Byte[]/Stream 类型/上传文件

Furion 4.4.0 以下版本在 Furion 4.4.0+ 版本移除了 [BodyBytes] 方式,原因是拓展性太差,新版本请使用 HttpFile 方式

有时候我们需要上传文件,需要设置 Content-Typemultipart/form-data 类型,如:

public interface IHttp : IHttpDispatchProxy  
{  
    [Post("https://furion.net/upload", ContentType = "multipart/form-data")] // bytes 可以通过 File.ReadAllBytes(文件路径) 获取  
    Task<HttpResponseMessage> PostXXXAsync([BodyBytes("键","文件名")]Byte[] bytes);  

    // 支持多个文件  
    [Post("https://furion.net/upload", ContentType = "multipart/form-data")] // bytes 可以通过 File.ReadAllBytes(文件路径) 获取  
    Task<HttpResponseMessage> PostXXXAsync([BodyBytes("键","文件名")]Byte[] bytes,[BodyBytes("键","文件名")]Byte[] bytes2);  
}  

Furion 4.4.0+ 版本如果使用 Furion 4.4.0+ 版本,请使用以下的 HttpFile 替代 [BodyBytes] 操作。请求有额外参数时 HttpFile 必须设置 fileName 值。

public interface IHttp : IHttpDispatchProxy  
{  
    [Post("https://furion.net/upload", ContentType = "multipart/form-data")]  
    Task<HttpResponseMessage> PostXXXAsync(HttpFile file);  


    [Post("https://furion.net/upload", ContentType = "multipart/form-data")]  
    Task<HttpResponseMessage> PostXXXAsync(HttpFile file, [Body("multipart/form-data")]User user);  

    // 支持多个文件  
    [Post("https://furion.net/upload", ContentType = "multipart/form-data")]  
    Task<HttpResponseMessage> PostXXXAsync(HttpFile[] files);  

    // 支持多个文件  
    [Post("https://furion.net/upload", ContentType = "multipart/form-data")]  
    Task<HttpResponseMessage> PostXXXAsync(IList<HttpFile> files);  
}  

19.5.16 支持模板配置

模板格式为:#(配置路径)

public interface IHttp : IHttpDispatchProxy  
{  
    [Post("#(Furion:Address)/upload")]  
    Task<HttpResponseMessage> PostXXXAsync([Body]User user);  
}  

{  
  "Furion": {  
    "Address": "https://furion.net"  
  }  
}  

方法的优先级高于接口定义的优先级。

19.5.17 重试策略

Furion v2.18+ 版本支持配置重试策略,如:

[RetryPolicy(3, 1000)] // 支持全局  
public interface IHttp : IHttpDispatchProxy  
{  
    [Post("https://furion.net"), RetryPolicy(3, 1000)]    // 支持局部  
    Task<HttpResponseMessage> PostXXXAsync([Body]User user);  
}  

以上代码表示请求失败重试 3 次,每次延迟 1000ms

19.5.18 支持 GZip

Furion v3.2.0+ 版本支持 GZip,如:

public interface IHttp : IHttpDispatchProxy  
{  
    [Post("https://furion.net", WithGZip = true)]  
    Task<HttpResponseMessage> PostXXXAsync([Body]User user);  
}  

19.5.19 设置 Url 转码

过去版本会对所有的 Url 进行 Uri.EscapeDataString 转码,在 Furion v3.8.0+ 版本支持 Url 转码设置,如:

public interface IHttp : IHttpDispatchProxy  
{  
    [Post("https://furion.net", WithEncodeUrl = false)]  
    Task<HttpResponseMessage> PostXXXAsync([Body]User user);  
}  

19.5.20 设置 HTTP 版本

可解决一些 HTTPHTTPS 请求问题。

public interface IHttp : IHttpDispatchProxy  
{  
    [Post("https://furion.net", HttpVersion = "1.1")]  
    Task<HttpResponseMessage> PostXXXAsync([Body]User user);  
}  

19.5.21 设置 Cookies

  • 局部请求拦截器方式
public interface IHttp : IHttpDispatchProxy  
{  
    // 通过参数拦截  
    [Get("https://furion.net/getuser")]  
    Task<HttpResponseMessage> GetUser([Interceptor(InterceptorTypes.Request)] Action<HttpClient, HttpRequestMessage> action = default);  
}  

设置 Cookies

// 方式一  
await _http.GetUser((client, request) =>  
{  
      request.Headers.Add("Cookie", "username=monksoul");   // 多个使用分号 ; 追加  
});  

// 方式二(推荐)!!!  
await _http.GetUser((client, request) =>  
{  
      var cookieContainer = new CookieContainer();  
      cookieContainer.Add(request.RequestUri, new Cookie("username", "monksoul"));  // 重复 .Add 操作可设置多个  
      request.Headers.Add("Cookie", cookieContainer.GetCookieHeader(request.RequestUri));  
});  

  • 全局请求拦截器方式
public interface IHttp : IHttpDispatchProxy  
{  
    [Get("https://furion.net/getuser")]  
    Task<HttpResponseMessage> GetUser();  

    // 全局拦截,类中每一个方法都会触发  
    [Interceptor(InterceptorTypes.Request)]  
    static void OnRequest1(HttpClient client, HttpRequestMessage request)  
    {  
      // 方式一  
          request.Headers.Add("Cookie", "username=monksoul");   // 多个使用分号 ; 追加  

         // 方式二(推荐)!!!  
          var cookieContainer = new CookieContainer();  
          cookieContainer.Add(request.RequestUri, new Cookie("username", "monksoul"));  // 重复 .Add 操作可设置多个  
          request.Headers.Add("Cookie", cookieContainer.GetCookieHeader(request.RequestUri));  
    }  
}  

设置 Cookies

// 请求后全局拦截器自动设置  
await _http.GetUser();  

19.5.21 设置客户端 BaseAddress

版本说明以下内容仅限 Furion 4.9.2.25 + 版本使用。

在最新版本中,Furion 框架提供了便捷设置客户端 BaseAddress 的特性 [BaseAddress],如:

[BaseAddress("https://weixin.qq.com/")] // 为整个接口方法设置基础地址  
public interface IHttp : IBase  
{  
    [Get("api/theapi")]  
    Task<HttpResponseMessage> TheApi();  

    [Get("api/otherapi"), BaseAddress("https://v2.weixin.qq.com/")] // 为特定方法设置不同的基础地址  
    Task<HttpResponseMessage> OtherApi();  
}  

19.6 请求客户端配置

Furion 框架也提供了多个请求客户端配置,可以为多个客户端请求配置默认请求信息,目前支持四种模式进行配置。

19.6.1 Startup.cs 统一配置

services.AddRemoteRequest(options=>  
{  
    // 配置默认 HttpClient  
    options.AddHttpClient(string.Empty, c => {  
        // 其他配置  
    });  

    // 配置特定客户端  
    options.AddHttpClient("github", c =>  
    {  
        c.BaseAddress = new Uri("https://api.github.com/");  
        c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");  
        c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");  
    });  
})  

配置了命名客户端后,每次请求都会自动加上这些配置。

  • 代理请求 使用
// 在接口定义中使用  
[Client("github")]  
public interface IHttp: IHttpDispatchProxy  
{  
}  

// 在方法中使用  
[Get("api/getdata"), Client("github")]  
Task<User> GetData();  

[Put("api/getdata"), Client("facebook")]  
Task<User> GetData();  

  • 字符串拓展 使用
// 设置请求拦截  
using var response = await "https://furion.net/api/sysdata/categories".SetClient("github").PostAsync();  

  • IHttpClientFactory 中使用
public class ValuesController : Controller  
{  
    private readonly IHttpClientFactory _httpClientFactory;  

    public ValuesController(IHttpClientFactory httpClientFactory)  
    {  
        _httpClientFactory = httpClientFactory;  
    }  

    [HttpGet]  
    public async Task<ActionResult> Get()  
    {  
        var client = _httpClientFactory.CreateClient("github");  
        string result = await client.GetStringAsync("/");  
        return Ok(result);  
    }  
}  

19.6.2 配置客户端 Timeout

默认情况下,HttpClient 请求超时时间为 100秒,可根据实际情况进行设置:

// 配置默认 HttpClient  
options.AddHttpClient(string.Empty, c =>  
{  
    c.Timeout = TimeSpan.FromMinutes(2);  
});  

// 配置特定客户端  
options.AddHttpClient("github", c =>  
{  
    c.Timeout = TimeSpan.FromMinutes(2);  
});  

19.6.3 配置客户端生存期

每次对 IHttpClientFactory 调用 CreateClient 都会返回一个新 HttpClient 实例。 每个命名客户端都创建一个 HttpMessageHandler。 工厂管理 HttpMessageHandler 实例的生存期。

IHttpClientFactory 将工厂创建的 HttpMessageHandler 实例汇集到池中,以减少资源消耗。 新建 HttpClient 实例时,可能会重用池中的 HttpMessageHandler 实例(如果生存期尚未到期的话)。

处理程序的默认生存期为两分钟。 可在每个命名客户端上重写默认值:

// 配置默认 HttpClient  
options.AddHttpClient(string.Empty, c => { ... })  
       .SetHandlerLifetime(TimeSpan.FromMinutes(5));  

// 配置特定客户端  
options.AddHttpClient("github", c => { ... })  
       .SetHandlerLifetime(TimeSpan.FromMinutes(5));  

19.6.4 自定义 Client 类方式

我们可以按照一定的规则编写特定服务的请求客户端,如:

public class GitHubClient  
{  
    public HttpClient Client { get; private set; }  

    public GitHubClient(HttpClient httpClient)  
    {  
        httpClient.BaseAddress = new Uri("https://api.github.com/");  
        httpClient.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");  
        httpClient.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");  
        Client = httpClient;  
    }  
}  

然后在 Startup.cs 中注册:

services.AddHttpClient<GitHubClient>();  

使用如下:

public class ValuesController : Controller  
{  
    private readonly GitHubClient _gitHubClient;;  

    public ValuesController(GitHubClient gitHubClient)  
    {  
        _gitHubClient = gitHubClient;  
    }  

    [HttpGet]  
    public async Task<ActionResult> Get()  
    {  
        string result = await _gitHubClient.Client.GetStringAsync("/");  
        return Ok(result);  
    }  
}  

19.6.5 自定义 Client 类 + 接口方式

我们也可以定义接口,通过接口的提供具体的服务 API 操作,无需手动配置 Url,如上面的 GetStringAsync("/")

public interface IGitHubClient  
{  
    Task<string> GetData();  
}  

public class GitHubClient : IGitHubClient  
{  
    private readonly HttpClient _client;  

    public GitHubClient(HttpClient httpClient)  
    {  
        httpClient.BaseAddress = new Uri("https://api.github.com/");  
        httpClient.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");  
        httpClient.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");  
        _client = httpClient;  
    }  

    public async Task<string> GetData()  
    {  
        return await _client.GetStringAsync("/");  
    }  
}  

然后在 Startup.cs 中注册:

services.AddHttpClient<IGitHubClient, GitHubClient>();  

使用:

public class ValuesController : Controller  
{  
    private readonly IGitHubClient _gitHubClient;;  

    public ValuesController(IGitHubClient gitHubClient)  
    {  
        _gitHubClient = gitHubClient;  
    }  

    [HttpGet]  
    public async Task<ActionResult> Get()  
    {  
        string result = await _gitHubClient.GetData();  
        return Ok(result);  
    }  
}  

19.6.6 HttpClient 超时问题

有时候会遇到 HttpClient 超时问题可尝试在 Startup.cs 中添加以下代码:

AppContext.SetSwitch("System.Net.DisableIPv6", true);  

19.7 SSL/https 证书配置

有时候我们请求远程接口时会遇到 The SSL connection could not be established, see inner exception. 这样的错误,原因是证书配置不正确问题,下面有几种解决方法。

19.7.1 使用默认 SSL 证书

在一些情况下,可直接使用默认证书即可解决问题,如:

services.AddRemoteRequest(options=>  
{  
    // 默认 HttpClient 在 Furion 框架内部已经配置了该操作  
    options.AddHttpClient(string.Empty)  
            .ConfigurePrimaryHttpMessageHandler(u => new HttpClientHandler  
            {  
                AllowAutoRedirect = true,  
                UseDefaultCredentials = true  
            });  

    // 配置特定客户端  
    options.AddHttpClient("github", c => { /*其他配置*/ })  
           .ConfigurePrimaryHttpMessageHandler(u => new HttpClientHandler  
            {  
                AllowAutoRedirect = true,  
                UseDefaultCredentials = true  
            });  
});  

19.7.2 忽略特定客户端 SSL 证书检查

services.AddRemoteRequest(options=>  
{  
    // 默认 HttpClient 在 Furion 框架内部已经配置了该操作  
    options.AddHttpClient(string.Empty)  
            .ConfigurePrimaryHttpMessageHandler(u => new HttpClientHandler  
            {  
                ServerCertificateCustomValidationCallback = (_, _, _, _) => true,  
            });  

    // 配置特定客户端  
    options.AddHttpClient("github", c => { /*其他配置*/ })  
           .ConfigurePrimaryHttpMessageHandler(u => new HttpClientHandler  
            {  
                ServerCertificateCustomValidationCallback = (_, _, _, _) => true,  
            });  
});  

关于 HttpClientHandlerSocketsHttpHandler.NET6 之后默认使用 SocketsHttpHandler 作为默认底层网络通信,但比 HttpClientHandler 提供了更多平台无差异的功能,对 HttpClientHandler 的任何设置都会转发到 SocketsHttpHandler 中,如需使用 SocketsHttpHandler 配置可参考:

// 忽略 SSL 不安全检查,或 https 不安全或 https 证书有误  
options.AddHttpClient(string.Empty)  
       .ConfigurePrimaryHttpMessageHandler(u => new SocketsHttpHandler  
        {  
            SslOptions = new SslClientAuthenticationOptions  
            {  
                RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true  
            }  
    });  

19.7.3 手动指定 SSL 证书

services.AddRemoteRequest(options=>  
{  
    // 配置特定客户端  
    options.AddHttpClient("github", c => { /*其他配置*/ })  
           .ConfigurePrimaryHttpMessageHandler(u => new HttpClientHandler  
           {  
                // 手动配置证书  
                ClientCertificateOptions = ClientCertificateOption.Manual,  
                ClientCertificates = {  
                    new X509Certificate2("...","..."),  
                    new X509Certificate2("...","..."),  
                    new X509Certificate2("...","...")  
                }  
            });  
});  

19.7.4 忽略所有客户端证书检查

版本说明以下内容仅限 Furion v3.6.6+ 版本使用。

services.AddRemoteRequest(options=>  
{  
    // 需在所有客户端注册之前注册  
    options.ApproveAllCerts();  
});  

19.8 远程请求拦截器 🌟

有时候,我们需要在发送请求前后进行拦截,比如添加授权 Token、记录异常日志、实现重试策略、超时策略等,这时候可以通过继承并实现 DelegatingHandler 抽象类即可,支持多个处理

  1. 创建拦截处理程序(支持多个)
using System.Net;  

namespace Furion.Application;  

public class RemoteFilterHandler : DelegatingHandler, ITransient  
{  
    // 这里支持构造函数依赖注入  

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)  
    {  
        // 自定义逻辑,比如判断是否携带 X-API-KEY 头,也可以动态添加 Token 等等  
        if (!request.Headers.Contains("X-API-KEY"))  
        {  
            return new HttpResponseMessage(HttpStatusCode.BadRequest)  
            {  
                Content = new StringContent(  
                    "The API key header X-API-KEY is required.")  
            };  
        }  

        return await base.SendAsync(request, cancellationToken);  
    }  
}  

  1. 注册拦截器

支持为多个客户端添加。默认客户端名称为 string.Empty

services.AddRemoteRequest(options =>  
{  
    options.AddHttpClient(string.Empty) // 默认客户端  
        .AddHttpMessageHandler<RemoteFilterHandler>();  

//    options.AddHttpClient("github") // 为特定客户端,如 github  
//        .AddHttpMessageHandler<RemoteFilterHandler>();  
});  

这样就能实现请求前后拦截了。

如需多个拦截,只需要重复 .AddHttpMessageHandler<>() 即可,如:

services.AddRemoteRequest(options =>  
{  
    options.AddHttpClient(string.Empty) // 默认客户端  
        .AddHttpMessageHandler<RemoteFilterHandler1>()  //支持多个  
        .AddHttpMessageHandler<RemoteFilterHandler2>()  // 支持多个  
        .AddHttpMessageHandler<RemoteFilterHandler3>(); // 支持多个  
});  

在前面的代码中,RemoteFilterHandler1 先运行,再运行 RemoteFilterHandler2,最后运行 RemoteFilterHandler3

19.9 配置客户端请求代理

services.AddRemoteRequest(options =>  
{  
    // 创建 Web 代理对象  
    var webProxy = new WebProxy(new Uri("http://192.168.31.11:8080"), BypassOnLocal: false);  

    // 默认客户端配置  
    options.AddHttpClient(string.Empty)  
            .ConfigurePrimaryHttpMessageHandler(u => new HttpClientHandler  
            {  
                Proxy = webProxy,  
                UseProxy = true  
            });  

    // 特定客户端配置  
    options.AddHttpClient("github", c => { /*其他配置*/ })  
           .ConfigurePrimaryHttpMessageHandler(u => new HttpClientHandler  
           {  
               Proxy = webProxy,  
               UseProxy = true  
           });  
});  

19.10 关于返回值非 200 时忽略 Http状态

Furion 提供了非常方便的请求并且序列化请求结果 PostAsAsync<T>2.8.8 及以下版本,当返回结果的 Http 状态为非 200 时,会直接截断。考虑到请求接口的多样性,在 2.8.9 及以上版本增加忽略返回 Http 状态,直接序列化结果的方式。

// 请求并且序列化请求结果  
var result = await "https://api.facebook.com/"  
    // 如果不加 OnException,则会直接截断  
    .OnException((client, res, errors)=> {  
        // 激活异步拦截 此处可以做记录日志操作 也可保持现状  
    })  
    .PostAsAsync<T>();  

PostAsStringAsync() 也使用同样的 OnException 操作使得忽略返回 Http 状态,原样返回 Http 请求结果

特别说明如果不加 OnException,则会直接截断。 如果需要复杂的 Http Post 请求,建议直接使用 PostAsync,返回值为 HttpResponseMessage,可以更灵活的控制结果。

19.11 关于同步请求

Furion 框架内部默认不提供同步请求操作,建议总是使用异步的方式请求。如在不能使用异步的情况下,可自行转换为同步执行。如:

  • 字符串拓展方式:
var result = "https://api.facebook.com".GetAsync().GetAwaiter().GetResult();  

// 如果不考虑 Task 异常捕获,可以直接 .Result  
var result = "https://api.facebook.com".GetAsync().Result;  

  • 代理方式
public interface IHttp : IHttpDispatchProxy  
{  
    [Get("https://api.facebook.com")]  
    Task<HttpResponseMessage> GetAsync();  
}  

// 同步调用  
var result = _http.GetAsync().GetAwaiter().GetResult();  

// 如果不考虑 Task 异常捕获,可以直接 .Result  
var result = _http.GetAsync().Result;  

19.12 静态 Default 方式构建

这种方式比字符串拓展好,避免了直接在字符串上拓展。

await HttpRequestPart.Default().SetRequestUrl("https://www.baidu.com").GetAsStringAsync();  

19.13 关闭 Http 请求日志

Furion 框架底层中,HttpClient 对象默认通过 IHttpClientFactory 创建的,只要发送请求就会自动打印日志,如:

info: 2022-10-26 11:38:16(+08:00) 星期三 L System.Logging.EventBusService[0] #1  
      EventBus Hosted Service is running.  
info: 2022-10-26 11:38:17(+08:00) 星期三 L Microsoft.Hosting.Lifetime[14] #1  
      Now listening on: https://localhost:5001  
info: 2022-10-26 11:38:17(+08:00) 星期三 L Microsoft.Hosting.Lifetime[14] #1  
      Now listening on: http://localhost:5000  
info: 2022-10-26 11:38:17(+08:00) 星期三 L Microsoft.Hosting.Lifetime[0] #1  
      Application started. Press Ctrl+C to shut down.  
info: 2022-10-26 11:38:17(+08:00) 星期三 L Microsoft.Hosting.Lifetime[0] #1  
      Hosting environment: Development  
info: 2022-10-26 11:38:17(+08:00) 星期三 L Microsoft.Hosting.Lifetime[0] #1  
      Content root path: D:\Workplaces\OpenSources\Furion\samples\Furion.Web.Entry\  
info: 2022-10-26 11:39:00(+08:00) 星期三 L System.Net.Http.HttpClient.Default.LogicalHandler[100] #8  
      Start processing HTTP request GET https://www.baidu.com/  
info: 2022-10-26 11:39:00(+08:00) 星期三 L System.Net.Http.HttpClient.Default.ClientHandler[100] #8  
      Sending HTTP request GET https://www.baidu.com/  
info: 2022-10-26 11:39:00(+08:00) 星期三 L System.Net.Http.HttpClient.Default.ClientHandler[101] #6  
      Received HTTP response headers after 288.0665ms - 200  
info: 2022-10-26 11:39:00(+08:00) 星期三 L System.Net.Http.HttpClient.Default.LogicalHandler[101] #6  
      End processing HTTP request after 326.1497ms - 200  
info: 2022-10-26 11:39:04(+08:00) 星期三 L System.Net.Http.HttpClient.Default.LogicalHandler[100] #3  
      Start processing HTTP request GET https://www.baidu.com/  

如需关闭只需在 appsettings.jsonappsettings.Development.json 中添加 System.Net.Http.HttpClient 日志类别过滤即可,如:

{  
  "Logging": {  
    "LogLevel": {  
      "Default": "Information",  
      "Microsoft.AspNetCore": "Warning",  
      "Microsoft.EntityFrameworkCore": "Information",  
      "System.Net.Http.HttpClient": "Warning"  
    }  
  }  
}  

19.14 获取 Cookies

版本说明以下内容仅限 Furion 4.7.4 + 版本使用。

  • 字符串方式
using var response = await "https://furion.net/".GetAsync();  
var cookies = response.GetCookies();  

  • 代理方式
public interface IHttp : IHttpDispatchProxy  
{  
    [Get("https://furion.net/")]  
    Task<HttpResponseMessage> GetAsync();  
}  

using var response = await _http.GetAsync();  
var cookies = response.GetCookies();  

重复 Key 处理由于 Cookies 是支持重复 Key 的,所以通过索引方式如 cookies[key] 会抛出异常,这时可以通过 .Lookup() 进行分组处理然后返回新的字典集合,如:

var dicCookies = cookies.ToLookup(u => u.Key, u => u.Value)  
                        .ToDictionary(u => u.Key, u => u.First()); // 取重复第一个 .First();  

var cookie1 = dicCookies["key"];  

19.15 在 WinForm/WPF 中使用

远程请求可在 WinForm/WPF 中使用,只需要将方法/事件标记为 async 即可。

private async void button1_Click(object sender, EventArgs e)  
{  
    var result = await "https://furion.net".GetAsStringAsync();  
}  

19.16 反馈与建议

与我们交流给 Furion 提 Issue


了解更多想了解更多 HttpClient 知识可查阅 ASP.NET Core - HTTP 请求 章节