❤️ 关注 Furion 微信公众号有惊喜哦!
Skip to main content

19. 远程请求

📝 模块更新日志
  • 新特性

    •   远程请求 [HttpMethod]ToSaveAsync 下载远程文件并保存到磁盘方法 4.8.7.32 ⏱️2023.04.02 bfd02c1
    •   远程请求支持 Content-Typetext/htmltext/plain 处理 4.8.7.22 ⏱️2023.03.27 #I6QMLR
    •   远程请求 HttpRequestMessage 拓展方法 AppendHeaders 4.8.7.10 ⏱️2023.03.14 #I6MVHT
    •   远程请求配置 SetHttpVersion(version) 配置,可配置 HTTP 请求版本,默认为 1.1 4.8.5.8 ⏱️2023.02.06 #I6D64H
    •   远程请求 [QueryString] 特性添加时间格式化 Format 属性 4.8.1.2 ⏱️2022.11.24 !670
  • 问题修复

    •   远程请求获取响应 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
  • 其他更改

    •   取消远程请求 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 接口

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

[Post("http://furion.baiqian.ltd/post")]
Task<Result> PostXXXAsync();

[Put("http://furion.baiqian.ltd/put")]
Task<Result> PutXXXAsync();

[Delete("http://furion.baiqian.ltd/delete")]
Task<Result> DeleteXXXAsync();

[Patch("http://furion.baiqian.ltd/patch")]
Task<Result> PatchXXXAsync();

[Head("http://furion.baiqian.ltd/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();
}
}
}

19.4 字符串方式使用示例

温馨提示

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

19.4.1 内置请求方式

// 发送 Get 请求
var response = await "http://furion.baiqian.ltd/get".GetAsync();

// 发送 Post 请求
var response = await "http://furion.baiqian.ltd/post".PostAsync();

// 发送 Put 请求
var response = await "http://furion.baiqian.ltd/put".PutAsync();

// 发送 Delete 请求
var response = await "http://furion.baiqian.ltd/delete".DeleteAsync();

// 发送 Patch 请求
var response = await "http://furion.baiqian.ltd/patch".PatchAsync();

// 发送 Head 请求
var response = await "http://furion.baiqian.ltd/head".HeadAsync();

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

19.4.2 设置请求地址

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

19.4.3 设置请求方式

await "http://furion.baiqian.ltd/post".SetHttpMethod(HttpMethod.Get);

19.4.4 设置地址模板

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

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

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

19.4.5 设置请求报文头

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

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

19.4.6 设置 URL 地址参数

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

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

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

最终输出格式为:http://furion.baiqian.ltd/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 "http://furion.baiqian.ltd".SetClient(() => new HttpClient());

19.4.8 设置 Body 参数

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

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

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

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

// 处理 application/xml、text/xml
await "http://furion.baiqian.ltd/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 "http://furion.baiqian.ltd/post".SetContentType("application/json");

19.4.10 设置内容编码

await "http://furion.baiqian.ltd/post".SetContentEncoding(Encoding.UTF8);

19.4.11 设置 JSON 序列化提供程序

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

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

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

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

});

// 比如配置缺省的序列化选项
await "http://furion.baiqian.ltd".SetJsonSerialization(default, new JsonSerializerOptions
{
// 中文乱码
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
})
.GetAsAsync();
关于 JSON 序列化提供器

如需了解更多 JSON 序列化知识可查阅 23. JSON 序列化 章节

19.4.12 启用 Body 参数验证

await "http://furion.baiqian.ltd/api/user/add".SetValidationState();

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

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

19.4.13 请求拦截

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

支持多次拦截

19.4.14 HttpClient 拦截

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

支持多次拦截

19.4.15 请求之前拦截

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

支持多次拦截

19.4.16 成功请求拦截

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

支持多次拦截

19.4.17 请求异常拦截

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

支持多次拦截

19.4.18 各种返回值处理

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

  • HttpResponseMessage:请求响应消息类型
  • Stream:流类型,可用来下载文件
  • T:泛型 T 类型
  • String:字符串类型,也就是直接将网络请求结果内容字符串化
  • Byte[]:字节数组类型

如:

// HttpResponseMessage
var res = await "http://furion.baiqian.ltd/".GetAsync();

// Stream,可用来下载文件
var (stream, encoding) = await "http://furion.baiqian.ltd/".GetAsStreamAsync();

// T
var user = await "http://furion.baiqian.ltd/".GetAsAsync<User>();

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

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 "http://furion.baiqian.ltd/upload".SetContentType("multipart/form-data")
.SetBodyBytes(("键", bytes, "文件名")).PostAsync();

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

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

// 支持多个文件,Furion 4.5.8 版本支持 Stream 方式更新
var res = await "http://furion.baiqian.ltd/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 "http://furion.baiqian.ltd/upload".SetContentType("multipart/form-data")
.SetFiles(HttpFile.Create("file", bytes, "image.png")).PostAsync();

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

19.4.20 设置 IServiceProvider

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

var res = await "http://furion.baiqian.ltd/upload".SetRequestScoped(services);

19.4.21 支持模板配置

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

var res = await "#(Furion:Address)/upload".GetAsync();
{
"Furion": {
"Address": "http://furion.baiqian.ltd"
}
}

19.4.22 重试策略

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

var res = await "http://furion.baiqian.ltd".SetRetryPolicy(3, 1000).GetAsync();

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

19.4.23 支持 GZip 压缩

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

var res = await "http://furion.baiqian.ltd".WithGZip().GetAsync();

19.4.24 设置 Url 转码

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

var res = await "http://furion.baiqian.ltd".WithEncodeUrl(false).GetAsync();

19.4.25 设置 HTTP 版本

可解决一些 HTTPHTTPS 请求问题。

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

19.4.26 下载远程文件并保存

版本说明

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

await "http://furion.baiqian.ltd/img/rm1.png".GetToSaveAsync("D:/rm3.png");
await "http://furion.baiqian.ltd/img/rm1.png".PostToSaveAsync("D:/rm3.png");
...

19.5 IHttpDispatchProxy 代理方式

19.5.1 支持多种代理方式

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

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

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

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

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

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

19.5.2 设置地址模板

public interface IHttp : IHttpDispatchProxy
{
[Get("http://furion.baiqian.ltd/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("http://furion.baiqian.ltd/get/{id}?name={name}"), Headers("key2","value2")]
Task<HttpResponseMessage> GetXXXAsync(int id, string name);

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

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

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

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

调用:

_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("http://furion.baiqian.ltd/get/{id}?name={name}")]
Task<HttpResponseMessage> GetXXXAsync(int id, string name);

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

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

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

// Furion 4.8.1.4 新增 [QueryString(Format)] 配置时间类型格式化
[Get("http://furion.baiqian.ltd/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("http://furion.baiqian.ltd/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("http://furion.baiqian.ltd/get", IgnoreNullValueQueries = true)]
Task<HttpResponseMessage> GetXXXAsync([QueryString]int id, [QueryString]string name, [QueryString]string nullValue);
}

最终输出格式为:http://furion.baiqian.ltd/get?id=1&name=Furion

关于对象类型直接作为模板参数

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

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

如果 Person 类型不做任何处理,那么最终传递的是 Person 的命名空间:http://furion.baiqian.ltd/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("http://furion.baiqian.ltd/post")]
Task<HttpResponseMessage> PostXXXAsync([Body]User user);

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

[Post("http://furion.baiqian.ltd/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("http://furion.baiqian.ltd/post"), JsonSerialization(typeof(NewtonsoftJsonSerializerProvider))]
Task<HttpResponseMessage> PostXXXAsync([Body]User user);

[Post("http://furion.baiqian.ltd/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("http://furion.baiqian.ltd/post")]
Task<HttpResponseMessage> PostXXXAsync([Range(1,10)]int id, [Required, MaxLength(10)]string name);

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

19.5.9 请求拦截

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

  • 接口静态方法拦截
  • 参数标记拦截
public interface IHttp : IHttpDispatchProxy
{
// 通过参数拦截
[Post("http://furion.baiqian.ltd/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("http://furion.baiqian.ltd/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("http://furion.baiqian.ltd/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("http://furion.baiqian.ltd/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("http://furion.baiqian.ltd/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:字符串类型,也就是直接将网络请求结果内容字符串化

如:

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

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

// T
[Post("http://furion.baiqian.ltd/post")]
Task<User> PostXXXAsync();

// String
[Post("http://furion.baiqian.ltd/post")]
Task<string> 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("http://furion.baiqian.ltd/upload", ContentType = "multipart/form-data")] // bytes 可以通过 File.ReadAllBytes(文件路径) 获取
Task<HttpResponseMessage> PostXXXAsync([BodyBytes("键","文件名")]Byte[] bytes);

// 支持多个文件
[Post("http://furion.baiqian.ltd/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("http://furion.baiqian.ltd/upload", ContentType = "multipart/form-data")]
Task<HttpResponseMessage> PostXXXAsync(HttpFile file);


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

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

// 支持多个文件
[Post("http://furion.baiqian.ltd/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": "http://furion.baiqian.ltd"
}
}

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

19.5.17 重试策略

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

[RetryPolicy(3, 1000)] // 支持全局
public interface IHttp : IHttpDispatchProxy
{
[Post("http://furion.baiqian.ltd"), 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("http://furion.baiqian.ltd", 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("http://furion.baiqian.ltd", WithEncodeUrl = false)]
Task<HttpResponseMessage> PostXXXAsync([Body]User user);
}

19.5.20 设置 HTTP 版本

可解决一些 HTTPHTTPS 请求问题。

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

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();
  • 字符串拓展 使用
// 设置请求拦截
var response = await "http://47.100.247.61/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 配置客户端请求代理

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.9 关于返回值非 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.10 关于同步请求

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.11 静态 Default 方式构建

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

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

19.12 关闭 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.13 获取 Cookies

版本说明

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

  • 字符串方式
var response = await "http://furion.baiqian.ltd/".GetAsync();
var cookies = response.GetCookies();
  • 代理方式
public interface IHttp : IHttpDispatchProxy
{
[Get("http://furion.baiqian.ltd/")]
Task<HttpResponseMessage> GetAsync();
}

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.14 在 WinForm/WPF 中使用

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

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

19.15 反馈与建议

与我们交流

给 Furion 提 Issue


了解更多

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