You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
HttpClientStudy/Docs/1.3.0.基础使用.管理客户端.ipynb

1983 lines
68 KiB
Plaintext

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
}
},
"source": [
"# HttpClient 初始化与生命周期管理"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"HttpClient 旨在实例化一次,并在应用程序的整个生命周期内重复使用。\n",
"\n",
"为实现复用HttpClient类库默认使用连接池和请求管道可以手动管理(连接池、配置管道、使用Polly); 结合IoC容器、工厂模式(提供了IHttpClientFactory类库)、复原库Polly,可以更加方便、完善的使用,这也是推荐的方法。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 0、初始化与全局设置"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"//全局设置,行运行一次,为后续准备\n",
"#r \"nuget:System.Net.Http.Json\"\n",
"#r \"nuget:Microsoft.Extensions.Http\"\n",
"#r \"nuget:Microsoft.Extensions.DependencyInjection\"\n",
"#r \"nuget:Microsoft.Extensions.Logging\" \n",
"#r \"nuget:Microsoft.Extensions.Logging.Console\"\n",
"#r \"nuget:Polly\"\n",
"#r \"nuget:Microsoft.Extensions.Http.Polly\"\n",
"#r \"nuget:Refit\" \n",
"#r \"nuget:Refit.HttpClientFactory\"\n",
"#r \"./Publish/HttpClientStudy.Core/HttpClientStudy.Core.dll\"\n",
"\n",
"global using System;\n",
"global using System.IO;\n",
"global using System.IO.Enumeration;\n",
"global using System.Buffers;\n",
"\n",
"global using System.Collections;\n",
"global using System.Collections.Concurrent;\n",
"global using System.Linq;\n",
"global using System.Linq.Expressions;\n",
"global using System.Threading;\n",
"global using System.Threading.Tasks;\n",
"\n",
"global using System.Net.Http;\n",
"global using System.Net.Http.Json;\n",
"\n",
"global using Microsoft.Extensions.DependencyInjection;\n",
"global using Microsoft.Extensions.DependencyInjection.Extensions;\n",
"global using Microsoft.Extensions.Logging;\n",
"global using Microsoft.Extensions.Logging.Console;\n",
"global using Microsoft.Extensions.Http.Logging;\n",
"\n",
"\n",
"global using Polly;\n",
"global using Polly.NoOp;\n",
"global using Polly.Simmy;\n",
"global using Polly.Retry;\n",
"global using Polly.Hedging;\n",
"global using Polly.Timeout;\n",
"global using Polly.Bulkhead;\n",
"global using Polly.Fallback;\n",
"global using Polly.RateLimit;\n",
"global using Polly.CircuitBreaker;\n",
"global using Polly.Utilities;\n",
"global using Polly.Extensions;\n",
"global using Polly.Wrap;\n",
"global using Polly.Registry;\n",
"global using Polly.Telemetry;\n",
"\n",
"global using Refit;\n",
"//global using Refit.HttpClientFactory;\n",
"\n",
"global using HttpClientStudy.Config;\n",
"global using HttpClientStudy.Core;\n",
"global using HttpClientStudy.Core.Utilities;\n",
"\n",
"//全局变量\n",
"var webApiBaseUrl = WebApiConfigManager.GetWebApiConfig().BaseUrl;\n",
"var workDir = Environment.CurrentDirectory;\n",
"var fullPath = System.IO.Path.GetFullPath(\"./Publish/HttpClientStudy.WebApp/HttpClientStudy.WebApp.exe\", workDir);\n",
"//fullPath.Display();\n",
"\n",
"//启动已发布的WebApi项目\n",
"Console.WriteLine(\"启动WebApi项目\");\n",
"var startMessage = AppUtility.RunWebApiExeFile(fullPath);\n",
"Console.WriteLine(startMessage);\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"source": [
"## 1、手动管理直接实例化-强烈不推荐"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"下面这种每次使用都实例化的用法是最常见、最`不推荐的`\n",
"\n",
"因为HttpClient刚推出时不成熟及微软官方文档的示例代码是这种用法再加上这种是最简单方便的使用方法就造成很多人使用这种用法。\n",
"这种方法有如下缺点:\n",
" 1. 每次使用都实例化,造成性能开销大、容易内存泄露;\n",
" 2. 并发量大、请求频繁时:网络端口会被耗尽 `Using包HttpClient,也只是在应用进程中释放了HttpClient实例但http请求/响应是跨操作系统和网络的,而系统及网络问题在进程之上,不是进程所能处理的。`\n",
" \n",
"优点:\n",
" 1. 使用简单,好学易用;\n",
" 2. 并发量小且请求不频繁时,问题不大;"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"/*\n",
" 每次请求都实例化:并发量大、请求频繁进会耗尽套接字端口\n",
"*/\n",
"{ \n",
" var baseUrl = WebApiConfigManager.GetWebApiConfig().BaseUrl;\n",
"\n",
" using(var client = new HttpClient())\n",
" {\n",
" //发送请求\n",
" var response = await client.GetAsync(baseUrl);\n",
" response.EnsureSuccessStatusCode();\n",
" }\n",
"\n",
" //显示句柄\n",
" var displayValue = display($\"第 1 次请求,成功!\");\n",
"\n",
" for(int i=0;i<10;i++)\n",
" {\n",
" using(var client = new HttpClient())\n",
" {\n",
" var response = await client.GetAsync(baseUrl);\n",
" response.EnsureSuccessStatusCode();\n",
" displayValue.Update($\"第 {i+1} 次/ 共 10 次请求,成功!\");\n",
" }\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2、手动管理静态类或单例"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"相比于直接new,实现了HttpClient的重用`不推荐的`\n",
"\n",
"缺点:\n",
" 1. 不够灵活、优雅:特别是有多个系列的请求时;\n",
" \n",
"优点:\n",
" 1. 复用 HttpClient\n",
" 2. 实现了HttpClient的重用减少创建和销毁的开销"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"/*\n",
" 静态类/属性\n",
"*/\n",
"\n",
"public class HttpClientHelper\n",
"{\n",
" public readonly static HttpClient StaticClient;\n",
"\n",
" static HttpClientHelper()\n",
" {\n",
" SocketsHttpHandler handler = new SocketsHttpHandler()\n",
" {\n",
" PooledConnectionLifetime = TimeSpan.FromSeconds(30),\n",
" };\n",
"\n",
" StaticClient = new HttpClient(handler);\n",
"\n",
" //统一设置:请求头等\n",
"\n",
" //统一错误处理\n",
"\n",
" //当然这里也可以设置Pipline不过这里就不演示了\n",
" } \n",
"\n",
" public static async Task<HttpResponseMessage> GetAsync(string url)\n",
" {\n",
" return await StaticClient.GetAsync(url);\n",
" }\n",
"\n",
" public static async Task<string> GetStringAsync(string url)\n",
" {\n",
" var response = await StaticClient.GetAsync(url);\n",
" response.EnsureSuccessStatusCode();\n",
" return await response.Content.ReadAsStringAsync();\n",
" }\n",
"\n",
" public static async Task<HttpResponseMessage> PostAsync(string url, HttpContent content)\n",
" {\n",
" return await StaticClient.PostAsync(url, content);\n",
" }\n",
"}\n",
"\n",
"{ //调用静态类\n",
" var baseUrl = WebApiConfigManager.GetWebApiConfig().BaseUrl;\n",
" var response = await HttpClientHelper.GetAsync(baseUrl+\"/api/Config/GetApiConfig\");\n",
" var content = await response.Content.ReadAsStringAsync();\n",
" Console.WriteLine(content);\n",
"\n",
" var response2 = await HttpClientHelper.GetStringAsync(baseUrl+\"/api/Normal/GetAllAccounts\");\n",
" Console.WriteLine(response2);\n",
"}"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"/*\n",
" 单例实现1:\n",
" 1. 私有构造函数,防止外部实例化\n",
" 2. 使用静态只读变量存储类的实例,由.Net框架保证实例不变且线程安全\n",
" 3. 密封类,拒绝继承,保证不被子类破坏\n",
"*/\n",
"\n",
"// 使用Lazy<T>实现单例\n",
"public sealed class HttpClientSingleton \n",
"{\n",
" // 私有静态变量,用于存储类的实例\n",
" private static readonly HttpClientSingleton instance = new HttpClientSingleton();\n",
" \n",
" //公共静态属性,用于获取类的实例\n",
" public static HttpClientSingleton Instance\n",
" {\n",
" get\n",
" {\n",
" return instance;\n",
" }\n",
" }\n",
"\n",
" private readonly HttpClient Client;\n",
"\n",
" //私有构造函数,防止外部实例化\n",
" private HttpClientSingleton() \n",
" {\n",
" SocketsHttpHandler handler = new SocketsHttpHandler()\n",
" {\n",
" PooledConnectionLifetime = TimeSpan.FromSeconds(30),\n",
" };\n",
"\n",
" Client = new HttpClient(handler);\n",
"\n",
" //统一设置:请求头等\n",
"\n",
" //统一错误处理\n",
"\n",
" //可以使用IoC容器来管理\n",
"\n",
" //当然这里也可以设置Pipline不过这里就不演示了\n",
" Console.WriteLine(\"HttpClientSingleton 初始化一次\");\n",
" }\n",
"\n",
" public async Task<HttpResponseMessage> GetAsync(string url)\n",
" {\n",
" return await Client.GetAsync(url);\n",
" }\n",
"\n",
" public async Task<string> GetStringAsync(string url)\n",
" {\n",
" var response = await Client.GetAsync(url);\n",
" response.EnsureSuccessStatusCode();\n",
" return await response.Content.ReadAsStringAsync();\n",
" }\n",
"}\n",
"\n",
"{ //调用示例\n",
"\n",
" var baseUrl = WebApiConfigManager.GetWebApiConfig().BaseUrl;\n",
" var response = await HttpClientSingleton.Instance.GetAsync(baseUrl+\"/api/Config/GetApiConfig\");\n",
" var content = await response.Content.ReadAsStringAsync();\n",
" Console.WriteLine(content);\n",
"\n",
" var response2 = await HttpClientSingleton.Instance.GetStringAsync(baseUrl+\"/api/Normal/GetAllAccounts\");\n",
" Console.WriteLine(response2);\n",
"}"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"/*\n",
" 单例实现2:\n",
" 1. 私有构造函数,防止外部实例化\n",
" 2. 使用Lazy<T>, 延迟实例化, 由.Net 框架保证线程安全\n",
" 3. 密封类,拒绝继承,保证不被子类破坏\n",
"*/\n",
"\n",
"// 由于静态初始化器是由 .NET 运行时在后台处理的,因此它是线程安全的,不需要额外的锁定机制。\n",
"public sealed class HttpClientSingleton2\n",
"{\n",
" private static readonly Lazy<HttpClient> _httpClientLazy = new Lazy<HttpClient>(() =>\n",
" {\n",
" SocketsHttpHandler handler = new SocketsHttpHandler()\n",
" {\n",
" PooledConnectionLifetime = TimeSpan.FromSeconds(30)\n",
" };\n",
"\n",
" var client = new HttpClient(handler)\n",
" {\n",
" // 可以在这里配置HttpClient的实例例如设置超时时间、基地址等\n",
" //Timeout = TimeSpan.FromSeconds(30),\n",
" //BaseAddress = new Uri(\"https://api.example.com/\"),\n",
" };\n",
"\n",
"\n",
" //统一设置:请求头等\n",
"\n",
" //统一错误处理\n",
"\n",
" //可以使用IoC容器来管理\n",
"\n",
" //当然这里也可以设置Pipline不过这里就不演示了\n",
" Console.WriteLine(\"HttpClientSingleton2 初始化一次\");\n",
"\n",
" return client;\n",
" });\n",
"\n",
" public static HttpClient Instance => _httpClientLazy.Value;\n",
"\n",
" // 私有构造函数,防止外部实例化\n",
" private HttpClientSingleton2() { } \n",
"\n",
" public async Task<HttpResponseMessage> GetAsync(string url)\n",
" {\n",
" return await _httpClientLazy.Value.GetAsync(url);\n",
" }\n",
"\n",
" public async Task<string> GetStringAsync(string url)\n",
" {\n",
" var response = await _httpClientLazy.Value.GetAsync(url);\n",
" response.EnsureSuccessStatusCode();\n",
" return await response.Content.ReadAsStringAsync();\n",
" }\n",
"}\n",
"\n",
"{ //调用示例\n",
"\n",
" var baseUrl = WebApiConfigManager.GetWebApiConfig().BaseUrl;\n",
" var response = await HttpClientSingleton2.Instance.GetAsync(baseUrl+\"/api/Config/GetApiConfig\");\n",
" var content = await response.Content.ReadAsStringAsync();\n",
" Console.WriteLine(content);\n",
"\n",
" var response2 = await HttpClientSingleton2.Instance.GetStringAsync(baseUrl+\"/api/Normal/GetAllAccounts\");\n",
" Console.WriteLine(response2);\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3、手动管理多工具类(每类请求对应一种工具类或单例类)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"把不同类别的请求分成不同的工具类,业务类直接封装成工具类的方法。类似类型化的客户端。 简单使用的话,`比较推荐`\n",
"\n",
"优点:\n",
" 1. 复用HttpClient\n",
" 2. 可以灵活的进行统一配置\n",
" 3. 不同类别不同工具类,方便定制\n",
" 4. 业务直接封装成工具类方法,调用方便、快捷\n",
"\n",
"缺点:\n",
" 1. 工具类比较多,需要手动维护\n",
" 2. 工具类方法比较多且和业务直接相关,需要手动维护\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"// 百度服务类\n",
"public sealed class BaiduService \n",
"{\n",
" private readonly HttpClient _httpClient;\n",
" public BaiduService()\n",
" {\n",
" //初始化httpClient\n",
" var baseHander = new SocketsHttpHandler() \n",
" { \n",
" MaxConnectionsPerServer = 1000 \n",
" };\n",
"\n",
" _httpClient = new HttpClient(baseHander)\n",
" {\n",
" Timeout = TimeSpan.FromSeconds(10),\n",
" BaseAddress = new Uri(\"http://www.baidu.com\"),\n",
" };\n",
" }\n",
"\n",
" ///// <summary>\n",
" /// 获取百度首页长度\n",
" /// </summary>\n",
" public async Task<int> GetIndexLengthAsync(string url)\n",
" {\n",
" var response = await _httpClient.GetAsync(url);\n",
" response.EnsureSuccessStatusCode();\n",
" var result = await response.Content.ReadAsStringAsync();\n",
" return result.Length;\n",
" }\n",
"}\n",
"//调用示例\n",
"{\n",
" var service = new BaiduService();\n",
" var result = await service.GetIndexLengthAsync(\"/\");\n",
" Console.WriteLine(result);\n",
"}"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"// 本机服务类\n",
"// 百度服务类\n",
"public sealed class LocalService \n",
"{\n",
" private readonly HttpClient _httpClient;\n",
" public LocalService()\n",
" {\n",
" //初始化httpClient\n",
" var baseHander = new SocketsHttpHandler() \n",
" { \n",
" MaxConnectionsPerServer = 1000 \n",
" };\n",
"\n",
" _httpClient = new HttpClient(baseHander)\n",
" {\n",
" Timeout = TimeSpan.FromSeconds(10),\n",
" BaseAddress = new Uri(WebApiConfigManager.GetWebApiConfig().BaseUrl),\n",
" };\n",
" }\n",
"\n",
" ///// <summary>\n",
" /// 获取百度首页长度\n",
" /// </summary>\n",
" public async Task<string> GetIndexAsync(string url)\n",
" {\n",
" var response = await _httpClient.GetAsync(url);\n",
" response.EnsureSuccessStatusCode();\n",
" var result = await response.Content.ReadAsStringAsync();\n",
" return result;\n",
" }\n",
"}\n",
"//调用示例\n",
"{\n",
" var service2 = new LocalService();\n",
" var result = await service2.GetIndexAsync(\"/api/Simple/GetAccount\");\n",
" Console.WriteLine(result);\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4、手动管理可复原(Polly)请求"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"#r \"nuget:Polly\"\n",
"#r \"nuget:Microsoft.Extensions.Http.Polly\"\n",
"\n",
"using Polly;\n",
"using Polly.Simmy;\n",
"using Polly.Retry;\n",
"using Polly.Extensions;\n",
"{\n",
" var pipleLine = new ResiliencePipelineBuilder()\n",
" .AddRetry(new RetryStrategyOptions()\n",
" {\n",
" ShouldHandle = new PredicateBuilder().Handle<Exception>(),\n",
" MaxRetryAttempts = 3, // Retry up to 3 times\n",
" OnRetry = args =>\n",
" {\n",
" // Due to how we have defined ShouldHandle, this delegate is called only if an exception occurred.\n",
" // Note the ! sign (null-forgiving operator) at the end of the command.\n",
" var exception = args.Outcome.Exception!; // The Exception property is nullable\n",
" Console.WriteLine(\"内部重试\");\n",
" return default;\n",
" }\n",
" })\n",
" .Build();\n",
"\n",
" var BaseUrl = WebApiConfigManager.GetWebApiConfig().BaseUrl;\n",
" HttpClient client = new HttpClient(new SocketsHttpHandler(){})\n",
" {\n",
" BaseAddress = new Uri(BaseUrl),\n",
" };\n",
"\n",
" try\n",
" {\n",
" await pipleLine.ExecuteAsync(async (inneerToken)=>\n",
" {\n",
" var response = await client.GetAsync(\"api/Polly8/RetryException\",inneerToken);\n",
" response.EnsureSuccessStatusCode();\n",
" });\n",
" }\n",
" catch(Exception ex)\n",
" {\n",
" Console.WriteLine(ex.Message);\n",
" }\n",
" finally\n",
" {\n",
"\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 5、IoC容器管理"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 直接注册IoC"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"/*\n",
" 注意:\n",
" 1、直接IoC管理只能一个不太方便\n",
" 2、可使用.NET 8+ 的 KeyedService, 可以管理多个。 老版只能用服务集合,勉强能用;\n",
" 3、把HttpClient 放在多个类中,分别注册使用;不过这样,不如直接使用类型化客户端;\n",
"*/\n",
"{ // 直接使用\n",
" var services = new ServiceCollection();\n",
" services.AddSingleton<HttpClient>(new HttpClient()\n",
" {\n",
" //BaseAddress = new Uri(\"https://localhost:5001/\"),\n",
" Timeout = TimeSpan.FromSeconds(10),\n",
" });\n",
"\n",
" var client = services.BuildServiceProvider().GetRequiredService<HttpClient>();\n",
"\n",
" var resp = await client.GetAsync(\"https://www.baidu.com\");\n",
" resp.EnsureSuccessStatusCode();\n",
"\n",
" var content = await resp.Content.ReadAsStringAsync();\n",
" Console.WriteLine(content.Length);\n",
"}\n",
"\n",
"{ // KeyService: .Net 8+ 才支持的功能\n",
" var services = new ServiceCollection();\n",
" services\n",
" .AddKeyedSingleton<HttpClient>(\"HttpClientA\",new HttpClient()\n",
" {\n",
" BaseAddress = new Uri(\"https://www.baidu.com/\"),\n",
" Timeout = TimeSpan.FromSeconds(10),\n",
" })\n",
" .AddKeyedSingleton<HttpClient>(\"HttpClientB\", new HttpClient()\n",
" {\n",
" BaseAddress = new Uri(\"https://www.qq.com/\"),\n",
" Timeout = TimeSpan.FromSeconds(2),\n",
" });\n",
"\n",
" var clientA = services.BuildServiceProvider().GetRequiredKeyedService<HttpClient>(\"HttpClientA\");\n",
" var responseA = await clientA.GetAsync(\"/\");\n",
" responseA.EnsureSuccessStatusCode();\n",
" var contentA = await responseA.Content.ReadAsStringAsync();\n",
" Console.WriteLine(contentA.Length);\n",
"\n",
" var clientB = services.BuildServiceProvider().GetRequiredKeyedService<HttpClient>(\"HttpClientB\");\n",
"\n",
" var responseB = await clientB.GetAsync(\"/\");\n",
" responseB.EnsureSuccessStatusCode();\n",
"\n",
" var contentB= await responseB.Content.ReadAsStringAsync();\n",
" Console.WriteLine(contentB.Length);\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### HttpClient 多服务类"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"// IoC 多个HttpClient服务类\n",
"\n",
"public class HttpClientServerA\n",
"{\n",
" public static HttpClient Client = new HttpClient()\n",
" {\n",
" BaseAddress = new Uri(\"https://www.baidu.com/\"),\n",
" Timeout = TimeSpan.FromSeconds(2),\n",
" };\n",
"\n",
" public int GetBaiduIndexLength()\n",
" {\n",
" var requestMessage = new HttpRequestMessage(HttpMethod.Get, \"/\");\n",
"\n",
" var response = Client.Send(requestMessage);\n",
"\n",
" response.EnsureSuccessStatusCode();\n",
"\n",
" var s = response.Content.ReadAsStream();\n",
" return (int)s.Length;\n",
" }\n",
"}\n",
"\n",
"public class HttpClientServerB\n",
"{\n",
" public static HttpClient Client = new HttpClient()\n",
" {\n",
" BaseAddress = new Uri(\"https://www.qq.com/\"),\n",
" Timeout = TimeSpan.FromSeconds(2),\n",
" };\n",
"\n",
" public int GetBaiduIndexLength()\n",
" {\n",
" var requestMessage = new HttpRequestMessage(HttpMethod.Get, \"/\");\n",
"\n",
" var response = Client.Send(requestMessage);\n",
"\n",
" response.EnsureSuccessStatusCode();\n",
"\n",
" var s = response.Content.ReadAsStream();\n",
" return (int)s.Length;\n",
" }\n",
"}\n",
"\n",
"{\n",
" var services = new ServiceCollection();\n",
" services.AddScoped<HttpClientServerA>();\n",
" services.AddScoped<HttpClientServerB>();\n",
"\n",
" var provider = services.BuildServiceProvider();\n",
"\n",
" var clientA = provider.GetService<HttpClientServerA>();\n",
" var sumA = clientA.GetBaiduIndexLength();\n",
" Console.WriteLine($\"A: {sumA}\");\n",
"\n",
" var clientB = provider.GetService<HttpClientServerB>();\n",
" var sumB = clientB.GetBaiduIndexLength();\n",
" Console.WriteLine($\"A: {sumB}\");\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 6、客户端工厂管理IHttpClientFactory(需要结合IoC) `强力推荐`"
]
},
{
"cell_type": "markdown",
"metadata": {
"vscode": {
"languageId": "polyglot-notebook"
}
},
"source": [
"使用 IHttpClientFactory 创建和管理 `短期HttpClient` 是官方强力推荐的方式。特别是使用IoC或是 ASP.NET中后台调用其它接口的情况。\n",
"\n",
"IHttpClientFactory 综合使用了 HttpClient的多种特性HttpClient的生命周期、HttpClient的配置、HttpClient的拦截器、HttpClient的缓存、HttpClient的依赖注入、Polly等等。\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 默认客户端"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"从使用推测,设计 IHttpClientFactory 时,重点应该是使用 “命名客户端” 或 “类型化客户端” 而不是默认客户端。 \n",
"\n",
"只有 AddHttpClient() 扩展方法返回 IServiceCollection其它相关扩展方法 AddHttpClient<HttpClient>())均返回 IHttpClientBuilder明显针对命名客户端。\n",
"AddHttpClient() 相当于注册了基本框架;而命名客户端中,名称为空(\"\"或string.Empty)的,相当于默认客户端。\n",
"\n",
"有一个 名为 `ConfigureHttpClientDefaults` 的 ServiceCollection 对象的扩展方法用于配置所有HttpClient实例并且只在初始化时执行一次。如果只使用一个默认客户端的话可以使用 ConfigureHttpClientDefaults 和 AddHttpClient() 配合使用,也能达到默认客户端的配置效果。 "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"//方式1默认客户端\n",
"{ \n",
" var services = new ServiceCollection();\n",
" /*\n",
" AddHttpClient() 返回 ServiceCollection可以继续添加其他客户端。\n",
" 其它方法则返回IHttpClientBuilder后结配置的扩展方法只能针对当前前端那个命名命令端。\n",
" */\n",
" services.AddHttpClient();\n",
"\n",
" var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();\n",
"\n",
" var client = factory.CreateClient();\n",
" //或者\n",
" var client2 = factory.CreateClient(\"\");\n",
" //或者 内部都是使用CreateClient(string.Empty),表示默认客户端。\n",
" var client3 = factory.CreateClient(string.Empty);\n",
"\n",
" var response = await client.GetAsync(webApiBaseUrl + \"/api/hello/index\");\n",
" response.EnsureSuccessStatusCode();\n",
" var data = await response.Content.ReadAsStringAsync();\n",
" data.Display();\n",
"}\n",
"\n",
"//方式2默认客户端 + 默认配置\n",
"{ \n",
" var services = new ServiceCollection();\n",
"\n",
" //默认客户端\n",
" services.AddHttpClient();\n",
"\n",
" //配置所有客户端\n",
" services.ConfigureHttpClientDefaults(builder => \n",
" {\n",
" //配置构建器\n",
" //builder.AddDefaultLogger();\n",
"\n",
" //配置客户端\n",
" builder.ConfigureHttpClient(c=>\n",
" {\n",
" c.BaseAddress = new Uri(webApiBaseUrl);\n",
" });\n",
" });\n",
"\n",
" var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();\n",
" var client = factory.CreateClient();\n",
" var response = await client.GetAsync(\"/api/hello/ping\");\n",
" response.EnsureSuccessStatusCode();\n",
" var data = await response.Content.ReadAsStringAsync();\n",
" data.Display();\n",
"}\n",
"\n",
"//方式3(推荐):默认客户端:直接使用名称为 string.empty 的命名客户端\n",
"{\n",
" var services = new ServiceCollection();\n",
"\n",
" //默认客户端\n",
" services\n",
" .AddHttpClient<HttpClient>(string.Empty)\n",
" //这样后续的配置,都是针对 string.empty 的客户端,可以使用全部配置功能\n",
" .ConfigureHttpClient(c=>c.BaseAddress = new Uri(webApiBaseUrl))\n",
" .AddDefaultLogger();\n",
"\n",
" var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();\n",
" var client = factory.CreateClient();\n",
" var response = await client.GetAsync(\"/api/hello/ping\");\n",
" response.EnsureSuccessStatusCode();\n",
" var data = await response.Content.ReadAsStringAsync();\n",
" data.Display();\n",
"}\n",
"\n",
"//错误用法\n",
"{\n",
" var services = new ServiceCollection();\n",
"\n",
" //默认客户端\n",
" services\n",
" //没有参数时,导致后面配置不起使用;\n",
" //参数必须为 空字符串或string.Empty后续的配置才能起使用\n",
"\n",
" .AddHttpClient<HttpClient>()\n",
" //没有参数时,导致后面配置不起使用\n",
" .ConfigureHttpClient(c=>c.BaseAddress = new Uri(webApiBaseUrl))\n",
" .AddDefaultLogger();\n",
"\n",
" var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();\n",
" var client = factory.CreateClient();\n",
"\n",
" try\n",
" {\n",
" var response = await client.GetAsync(\"/api/hello/ping\");\n",
" response.EnsureSuccessStatusCode();\n",
" var data = await response.Content.ReadAsStringAsync();\n",
" data.Display();\n",
" }\n",
" catch(InvalidOperationException ex)\n",
" {\n",
" Console.WriteLine($\"没有参数的配置AddHttpClient<HttpClient>(),因后续配置中,赋值 BaseAddress 不起使用,出现异常:{Environment.NewLine}{ex.Message}\");\n",
" }\n",
" catch(Exception ex)\n",
" {\n",
"\n",
" Console.WriteLine(ex.Message);\n",
" }\n",
" finally\n",
" {\n",
" client.Dispose();\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 默认全局配置"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"ConfigureHttpClientDefaults 扩展方法添加一个委托用于配置所有HttpClient实例。只执行一次。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"//全局配置所有HttpClient配置\n",
"{\n",
" var services = new ServiceCollection();\n",
" //添加一个委托用于配置所有HttpClient实例。\n",
" //只执行一次而非每次CreateClient都会执行一次。\n",
" services.ConfigureHttpClientDefaults(builder => \n",
" {\n",
" //builder.UseSocketsHttpHandler();\n",
" //builder.SetHandlerLifetime(TimeSpan.FromMinutes(5));\n",
" \n",
" builder.ConfigureHttpClient(hc =>\n",
" {\n",
" hc.BaseAddress = new Uri(webApiBaseUrl);\n",
" });\n",
"\n",
" Console.WriteLine(\"ConfigureHttpClientDefaults 只执行一次!\");\n",
" });\n",
"\n",
" //配置命名客户端\n",
" services\n",
" .AddHttpClient<HttpClient>(\"client_a\")\n",
" .ConfigureHttpClient(hc => \n",
" {\n",
" hc.DefaultRequestHeaders.Add(\"client_a\", \"client_a\");\n",
"\n",
" //可以覆盖默认配置\n",
" //hc.BaseAddress = new Uri(\"http://www.qq.com\");\n",
"\n",
" Console.WriteLine(\"ConfigureHttpClient 每次 CreateClient 执行一次!\");\n",
" });\n",
"\n",
" \n",
" var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();\n",
"\n",
" //默认客户端\n",
" var defaultClient = factory.CreateClient();\n",
" var defaultResponse = await defaultClient.GetAsync(\"/api/hello/ping\");\n",
" var defaultData = await defaultResponse.Content.ReadAsStringAsync();\n",
" Console.WriteLine(defaultData);\n",
"\n",
" //命名客户端\n",
" var namedClient = factory.CreateClient(\"client_a\");\n",
" var namedResponse = await namedClient.GetAsync(\"/api/hello/get\");\n",
" var namedData = await namedResponse.Content.ReadAsStringAsync();\n",
" Console.WriteLine(namedData);\n",
"\n",
" _ = factory.CreateClient(\"client_a\");\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 命名客户端(推荐用法)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"命名客户端应该是官方推荐的方法。名称为空字符串或string.Empty时可以为是默认命名客户端factory.CreateClient()创建的就是这个默认客户端(或者factory.CreateClient(\"\"))。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"//命名客户端\n",
"{\n",
" var clientA =\"httpClientA\";\n",
" var clientB =\"httpClientB\";\n",
"\n",
" var services = new ServiceCollection();\n",
"\n",
" services.AddHttpClient<HttpClient>(string.Empty, (provider, client) => \n",
" {\n",
" client.BaseAddress = new Uri(webApiBaseUrl);\n",
" });\n",
"\n",
" services.AddHttpClient<HttpClient>(clientA, (provider, client) => \n",
" {\n",
" client.BaseAddress = new Uri(webApiBaseUrl);\n",
" });\n",
"\n",
" services.AddHttpClient<HttpClient>(clientB, (provider, client) => \n",
" {\n",
" client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;\n",
" client.BaseAddress = new Uri(webApiBaseUrl);\n",
" })\n",
" .ConfigureHttpClient(client=>\n",
" {\n",
" client.Timeout = TimeSpan.FromSeconds(1);\n",
" client.DefaultRequestVersion = new Version(1, 1);\n",
" });\n",
"\n",
" var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();\n",
"\n",
" //name=string.Empty\n",
" var defaultClient = factory.CreateClient();\n",
" var defaultResponse = await defaultClient.GetAsync(\"/api/hello/ping\");\n",
" var defaultData = await defaultResponse.Content.ReadAsStringAsync();\n",
" Console.WriteLine(defaultData);\n",
"\n",
" //name=clientA\n",
" var httpClient_a = factory.CreateClient(clientA);\n",
" var responseA = await httpClient_a.GetAsync(\"/api/hello/ping\");\n",
" var dataA = await responseA.Content.ReadAsStringAsync();\n",
" dataA.Display();\n",
"\n",
" //name=clientB\n",
" var httpClient_B = factory.CreateClient(clientB);\n",
" var responseB = await httpClient_B.GetAsync(\"/api/hello/ping\");\n",
" var dataB = await responseB.Content.ReadAsStringAsync();\n",
" dataB.Display();\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 类型化客户端 (推荐)"
]
},
{
"cell_type": "markdown",
"metadata": {
"vscode": {
"languageId": "polyglot-notebook"
}
},
"source": [
"类型化的客户端,两种基本使用方式:\n",
"1、可以单独使用(直接IoC容器)\n",
"2、与IFactoryHttpClient配合使用(依赖注入),目的是:从统一的工厂配置中获取客户端,作为 HttpClient 类型的实参,传给类型化客户端的构造函数。\n",
" 换名话说从工厂获取HttpClient实例设置为 类型化客户端类的 HttpClient在其内部使用。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"// 类型化客户端 HttpClient\n",
"public class HttpClientServiceA\n",
"{\n",
" public HttpClient Client { get; }\n",
" public HttpClientServiceA(HttpClient client)\n",
" {\n",
" Client = client;\n",
" Console.WriteLine(\"HttpClientServiceA => 构造函数执行一次\");\n",
" }\n",
"\n",
" public async Task<string> GetIndexAsync()\n",
" {\n",
" var response = await Client.GetAsync(\"/api/hello/index\");\n",
" var content = await response.Content.ReadAsStringAsync();\n",
" return content;\n",
" }\n",
"}\n",
"\n",
"public class HttpClientServiceB\n",
"{\n",
" public HttpClient Client { get; }\n",
" public HttpClientServiceB(HttpClient client)\n",
" {\n",
" Client = client;\n",
" Console.WriteLine(\"HttpClientServiceB => 构造函数执行一次\");\n",
" }\n",
"\n",
" public async Task<string> PingAsync()\n",
" {\n",
" var response = await Client.GetAsync(\"/api/hello/Ping\");\n",
" var content = await response.Content.ReadAsStringAsync();\n",
" return content;\n",
" }\n",
"}\n",
"\n",
"// 方式1(不推荐)类型化客户端直接注入IoC,并从中获取实例。优点是范围可以自己选择。\n",
"{\n",
" Console.WriteLine(\"方式1 -------------------------------------------------------------------\");\n",
" var services = new ServiceCollection();\n",
" services.AddSingleton<HttpClientServiceA>(b => \n",
" { \n",
" return new HttpClientServiceA(new HttpClient(){BaseAddress = new Uri(webApiBaseUrl)});\n",
" });\n",
" services.AddScoped<HttpClientServiceB>(b=> \n",
" {\n",
" return new HttpClientServiceB(new HttpClient(){BaseAddress = new Uri(webApiBaseUrl)});\n",
" });\n",
"\n",
" var builder = services.BuildServiceProvider();\n",
" var serverA = builder.GetRequiredService<HttpClientServiceA>();\n",
" var serverB = builder.GetRequiredService<HttpClientServiceB>();\n",
"\n",
" var dataA = await serverA.GetIndexAsync();\n",
" Console.WriteLine(dataA);\n",
"\n",
" var dataB = await serverB.PingAsync();\n",
" Console.WriteLine(dataB);\n",
"\n",
" Console.WriteLine(\"========================================================================\");\n",
"}\n",
"\n",
"// 方式2类型化客户端AddHttpClient<>() 设置\n",
"{\n",
" Console.WriteLine(\"方式2 -------------------------------------------------------------------\");\n",
" var services = new ServiceCollection();\n",
" services\n",
" .AddHttpClient<HttpClientServiceA>()\n",
" .ConfigureHttpClient(client=>\n",
" {\n",
" client.BaseAddress = new Uri(webApiBaseUrl);\n",
" });\n",
"\n",
" services\n",
" .AddHttpClient<HttpClientServiceB>()\n",
" .ConfigureHttpClient(client=>\n",
" {\n",
" client.BaseAddress = new Uri(webApiBaseUrl);\n",
" });\n",
"\n",
" var builder = services.BuildServiceProvider();\n",
" var serverA = builder.GetRequiredService<HttpClientServiceA>();\n",
" var serverB = builder.GetRequiredService<HttpClientServiceB>();\n",
"\n",
" var dataA = await serverA.GetIndexAsync();\n",
" Console.WriteLine(dataA);\n",
"\n",
" var dataB = await serverB.PingAsync();\n",
" Console.WriteLine(dataB);\n",
"\n",
" Console.WriteLine(\"========================================================================\");\n",
"}\n",
"\n",
"// 方式3类型化客户端结合工厂由工厂从统一配置中提供类型化客户端中使用的HttpClient实例。\n",
"{\n",
" Console.WriteLine(\"方式3 -------------------------------------------------------------------\");\n",
" var services = new ServiceCollection();\n",
" services.AddHttpClient<HttpClientServiceA>(client => \n",
" {\n",
" client.BaseAddress = new Uri(webApiBaseUrl);\n",
" Console.WriteLine(\"HttpClientServiceA => AddHttpClient 执行一次\");\n",
" })\n",
" .AddTypedClient<HttpClientServiceA>()\n",
" .ConfigureHttpClient(client=>\n",
" {\n",
" client.Timeout = TimeSpan.FromSeconds(1);\n",
" Console.WriteLine(\"HttpClientServiceA => ConfigureHttpClient 执行一次\");\n",
" });\n",
"\n",
" services.AddHttpClient<HttpClientServiceB>(client => \n",
" {\n",
" client.BaseAddress = new Uri(webApiBaseUrl);\n",
" Console.WriteLine(\"HttpClientServiceB => AddHttpClient 执行一次\");\n",
" })\n",
" .AddTypedClient<HttpClientServiceB>()\n",
" .ConfigureHttpClient(client=>\n",
" {\n",
" client.Timeout = TimeSpan.FromSeconds(2);\n",
" Console.WriteLine(\"HttpClientServiceB => ConfigureHttpClient 执行一次\");\n",
" });\n",
"\n",
" var builder = services.BuildServiceProvider();\n",
"\n",
" var serviceA = builder.GetRequiredService<HttpClientServiceA>();\n",
" var serviceB = builder.GetRequiredService<HttpClientServiceB>();\n",
" //每获取一次类型化客户端,都会执行一交。\n",
" var serviceB2 = builder.GetRequiredService<HttpClientServiceB>();\n",
"\n",
" var dataA = await serviceA.GetIndexAsync();\n",
" Console.WriteLine(dataA);\n",
"\n",
" var dataB = await serviceB.PingAsync();\n",
" Console.WriteLine(dataB);\n",
"\n",
" var dataB2 = await serviceB2.PingAsync();\n",
" Console.WriteLine(dataB2);\n",
"\n",
" Console.WriteLine(\"========================================================================\");\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 管道配置"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"//管道配置\n",
"\n",
"//日志中间件(管道类)\n",
"public class LoggerDelegatingHandler : DelegatingHandler\n",
"{\n",
" protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)\n",
" {\n",
" Console.WriteLine(\"LoggerDelegatingHandler -> Send -> Before\");\n",
"\n",
" HttpResponseMessage response = base.Send(request, cancellationToken);\n",
"\n",
" Console.WriteLine(\"LoggerDelegatingHandler -> Send -> After\");\n",
"\n",
" return response;\n",
" }\n",
"\n",
" protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n",
" {\n",
" Console.WriteLine(\"LoggerDelegatingHandler -> SendAsync -> Before\");\n",
"\n",
" HttpResponseMessage response = await base.SendAsync(request, cancellationToken);\n",
"\n",
" Console.WriteLine(\"LoggerDelegatingHandler -> SendAsync -> After\");\n",
"\n",
" return response;\n",
" }\n",
"}\n",
"\n",
"//使用日志中间件\n",
"{\n",
" var services = new ServiceCollection();\n",
"\n",
" //先注册\n",
" services.AddTransient<LoggerDelegatingHandler>();\n",
"\n",
" services.AddHttpClient<HttpClient>(string.Empty).ConfigureHttpClient(client =>\n",
" {\n",
" client.BaseAddress = new Uri(webApiBaseUrl);\n",
" })\n",
" //配置SocketsHttpHandler\n",
" .UseSocketsHttpHandler((handler,provider) =>\n",
" {\n",
" handler.ConnectTimeout = TimeSpan.FromSeconds(10);\n",
" handler.MaxConnectionsPerServer = 100;\n",
" handler.UseProxy = false;\n",
" handler.UseCookies = true;\n",
" handler.EnableMultipleHttp2Connections = true;\n",
" handler.SslOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;\n",
" })\n",
" //使用前先在AddTransient范围注册\n",
" .AddHttpMessageHandler<LoggerDelegatingHandler>()\n",
" ;\n",
"\n",
" var factory = services.BuildServiceProvider().GetService<IHttpClientFactory>();\n",
"\n",
" var client = factory.CreateClient();\n",
"\n",
" var response = await client.GetAsync(\"/api/hello/ping\");\n",
" response.EnsureSuccessStatusCode();\n",
"\n",
" var responseString = await response.Content.ReadAsStringAsync();\n",
" Console.WriteLine(responseString);\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 日志配置"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"默认日志配置,需要先引用 `Microsoft.Extensions.Logging` 和 `Microsoft.Extensions.Logging.Console` 包,进行通用日志配置!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"//通用日志\n",
"{\n",
" ILoggerFactory loggerFactory = LoggerFactory.Create(buider =>\n",
" {\n",
" buider.AddConsole();\n",
" });\n",
"\n",
" ILogger logger = loggerFactory.CreateLogger(\"logger\");\n",
" logger.LogInformation(\"直接使用的通用日志!\");\n",
"}\n",
"\n",
"//IoC中使用\n",
"{\n",
" var services = new ServiceCollection();\n",
" services.AddLogging(config =>\n",
" {\n",
" config.SetMinimumLevel(LogLevel.Information);\n",
"\n",
" config.AddConsole();\n",
" //config.AddSimpleConsole();\n",
" //config.AddSystemdConsole();\n",
" });\n",
"\n",
" var serviceProvider = services.BuildServiceProvider();\n",
" var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();\n",
" var logger = loggerFactory.CreateLogger(\"logger\");\n",
" logger.LogInformation(\"IoC中使用日志!\");\n",
" logger.LogError(\"IoC中的错误日志!\");\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### 配置默认日志"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"//配置默认日志(必须有常规日志及级别设置,否则不起使用)\n",
"{\n",
" var services = new ServiceCollection();\n",
"\n",
" // 1、配置通用日志\n",
" services.AddLogging(config =>\n",
" {\n",
" //日志级别\n",
" config.SetMinimumLevel(LogLevel.Trace);\n",
" //config.SetMinimumLevel(LogLevel.Information);\n",
"\n",
" //日志载体\n",
" config.AddConsole();\n",
" //config.AddDebug();\n",
" //config.AddJsonConsole();\n",
" //config.AddSimpleConsole();\n",
" //config.AddSystemdConsole();\n",
"\n",
" });\n",
" services\n",
" .ConfigureHttpClientDefaults(options =>\n",
" {\n",
" //2、配置通用日志\n",
" options.AddDefaultLogger();\n",
" })\n",
" .AddHttpClient<HttpClient>(String.Empty,c =>\n",
" {\n",
" c.BaseAddress = new Uri(webApiBaseUrl);\n",
" c.DefaultRequestHeaders.Add(\"Authorization\", \"Bearer a.b.c\");\n",
" })\n",
" //2、或者单独配置此命名客户端日志\n",
" .AddDefaultLogger()\n",
" ;\n",
"\n",
" var factory = services.BuildServiceProvider().GetService<IHttpClientFactory>();\n",
" var client = factory.CreateClient(String.Empty);\n",
" var response = await client.GetAsync(\"api/hello/index\");\n",
"\n",
" response.EnsureSuccessStatusCode();\n",
"\n",
" var content = await response.Content.ReadAsStringAsync();\n",
"\n",
" Console.WriteLine(content);\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### 配置自定义日志"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[博客](https://www.cnblogs.com/MingsonZheng/p/18013332) 可以参考"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"/* 添加自定义日志记录\n",
" 1、可以指定当 HttpClient 启动请求、接收响应或引发异常时记录的内容和方式。可以同时添加多个自定义记录器(控制台、ETW 记录器),或“包装”和“不包装”记录器。由于其附加性质,可能需要事先显式删除默认的“旧”日志记录。\n",
" 要添加自定义日志记录,您需要实现 IHttpClientLogger 接口,然后使用 AddLogger 将自定义记录器添加到客户端。请注意,日志记录实现不应引发任何异常,否则可能会中断请求执行\n",
" 2、请求上下文对象\n",
" 上下文对象可用于将 LogRequestStart 调用与相应的 LogRequestStop 调用相匹配,以将数据从一个调用传递到另一个调用。 Context 对象由 LogRequestStart 生成,然后传递回 LogRequestStop。这可以是属性包或保存必要数据的任何其他对象。\n",
" 如果不需要上下文对象,实现可以从 LogRequestStart 返回 null。\n",
" 3、避免从内容流中读取\n",
" 例如,如果您打算阅读和记录请求和响应内容,请注意,它可能会对最终用户体验产生不利的副作用并导致错误。例如,请求内容可能在发送之前被消耗,或者巨大的响应内容可能最终被缓冲在内存中。此外,在 .NET 7 之前,访问标头不是线程安全的,可能会导致错误和意外行为。\n",
" 4、谨慎使用异步日志记录\n",
" 我们期望同步 IHttpClientLogger 接口适用于绝大多数自定义日志记录用例。出于性能原因,建议不要在日志记录中使用异步。但是,如果严格要求日志记录中的异步访问,您可以实现异步版本 IHttpClientAsyncLogger。它派生自 IHttpClientLogger因此可以使用相同的 AddLogger API 进行注册。\n",
" 请注意,在这种情况下,还应该实现日志记录方法的同步对应项,特别是如果该实现是面向 .NET Standard 或 .NET 5+ 的库的一部分。同步对应项是从同步 HttpClient.Send 方法调用的;即使 .NET Standard 表面不包含它们,.NET Standard 库也可以在 .NET 5+ 应用程序中使用,因此最终用户可以访问同步 HttpClient.Send 方法。\n",
" 5、包装和不包装记录仪\n",
" 当您添加记录器时您可以显式设置wrapHandlersPipeline参数来指定记录器是否将被包装。默认不包装。\n",
" 在将重试处理程序添加到管道的情况下(例如 Polly 或某些重试的自定义实现),包装和不包装管道之间的区别最为显着。\n",
"*/\n",
"\n",
"// 创建一个简单的控制台日志类\n",
"public class SimpleConsoleLogger : IHttpClientLogger\n",
"{\n",
" public object? LogRequestStart(HttpRequestMessage request)\n",
" {\n",
" return null;\n",
" }\n",
"\n",
" public void LogRequestStop(object? ctx, HttpRequestMessage request, HttpResponseMessage response, TimeSpan elapsed)\n",
" {\n",
" Console.WriteLine($\"自定义日志:{request.Method} {request.RequestUri?.AbsoluteUri} - {(int)response.StatusCode} {response.StatusCode} in {elapsed.TotalMilliseconds}ms\");\n",
" }\n",
"\n",
" public void LogRequestFailed(object? ctx, HttpRequestMessage request, HttpResponseMessage? response, Exception e, TimeSpan elapsed)\n",
" {\n",
" Console.WriteLine($\"自定义日志:{request.Method} {request.RequestUri?.AbsoluteUri} - Exception {e.GetType().FullName}: {e.Message}\");\n",
" }\n",
"}\n",
"\n",
"//使用\n",
"{\n",
" var services = new ServiceCollection();\n",
" //1、先注册日志类\n",
" services.AddSingleton<SimpleConsoleLogger>();\n",
"\n",
" services\n",
" // 全局配置\n",
" .ConfigureHttpClientDefaults(options =>\n",
" {\n",
" })\n",
" // 配置到HttpClient\n",
" .AddHttpClient<HttpClient>(String.Empty,c =>\n",
" {\n",
" c.BaseAddress = new Uri(webApiBaseUrl);\n",
" })\n",
" //可选:取消默认日志记录\n",
" .RemoveAllLoggers()\n",
" //2、配置到HttpClient\n",
" .AddLogger<SimpleConsoleLogger>()\n",
" ;\n",
"\n",
" var factory = services.BuildServiceProvider().GetService<IHttpClientFactory>();\n",
" var client = factory.CreateClient(String.Empty);\n",
" var response = await client.GetAsync(\"api/hello/index\");\n",
"\n",
" response.EnsureSuccessStatusCode();\n",
"\n",
" var content = await response.Content.ReadAsStringAsync();\n",
"\n",
" Console.WriteLine($\"API 影响内容:{content}\");\n",
"}\n",
"\n",
"// 使用上下文的日志类\n",
"public class RequestIdLogger : IHttpClientLogger\n",
"{\n",
" private readonly ILogger _log;\n",
"\n",
" public RequestIdLogger(ILogger<RequestIdLogger> log)\n",
" {\n",
" _log = log;\n",
" }\n",
"\n",
" private static readonly Action<ILogger, Guid, string?, Exception?> _requestStart = LoggerMessage.Define<Guid, string?>\n",
" (\n",
" LogLevel.Information,\n",
" EventIds.RequestStart,\n",
" \"Request Id={RequestId} ({Host}) started\"\n",
" );\n",
"\n",
" private static readonly Action<ILogger, Guid, double, Exception?> _requestStop = LoggerMessage.Define<Guid, double>\n",
" (\n",
" LogLevel.Information,\n",
" EventIds.RequestStop,\n",
" \"Request Id={RequestId} succeeded in {elapsed}ms\"\n",
" );\n",
"\n",
" private static readonly Action<ILogger, Guid, Exception?> _requestFailed = LoggerMessage.Define<Guid>\n",
" (\n",
" LogLevel.Error,\n",
" EventIds.RequestFailed,\n",
" \"Request Id={RequestId} FAILED\"\n",
" );\n",
"\n",
" public object? LogRequestStart(HttpRequestMessage request)\n",
" {\n",
" var ctx = new Context(Guid.NewGuid());\n",
" _requestStart(_log, ctx.RequestId, request.RequestUri?.Host, null);\n",
" return ctx;\n",
" }\n",
"\n",
" public void LogRequestStop(object? ctx, HttpRequestMessage request, HttpResponseMessage response, TimeSpan elapsed)\n",
" {\n",
" _requestStop(_log, ((Context)ctx!).RequestId, elapsed.TotalMilliseconds, null);\n",
" }\n",
"\n",
" public void LogRequestFailed(object? ctx, HttpRequestMessage request, HttpResponseMessage? response, Exception e, TimeSpan elapsed)\n",
" {\n",
" _requestFailed(_log, ((Context)ctx!).RequestId, null);\n",
" }\n",
"\n",
" public static class EventIds\n",
" {\n",
" public static readonly EventId RequestStart = new(1, \"RequestStart\");\n",
" public static readonly EventId RequestStop = new(2, \"RequestStop\");\n",
" public static readonly EventId RequestFailed = new(3, \"RequestFailed\");\n",
" }\n",
"\n",
" record Context(Guid RequestId);\n",
"}\n",
"\n",
"//使用\n",
"{\n",
" var services = new ServiceCollection();\n",
"\n",
" services.AddLogging(config =>\n",
" {\n",
" config.SetMinimumLevel(LogLevel.Trace);\n",
" config.AddConsole();\n",
" });\n",
"\n",
" //1、先注册日志类\n",
" services.AddSingleton<RequestIdLogger>();\n",
"\n",
" services\n",
" // 全局配置\n",
" .ConfigureHttpClientDefaults(options =>\n",
" {\n",
" })\n",
" // 配置到HttpClient\n",
" .AddHttpClient<HttpClient>(String.Empty,c =>\n",
" {\n",
" c.BaseAddress = new Uri(webApiBaseUrl);\n",
" })\n",
" //可选:取消默认日志记录\n",
" .RemoveAllLoggers()\n",
" //2、配置到HttpClient\n",
" .AddLogger<RequestIdLogger>()\n",
" ;\n",
"\n",
" var factory = services.BuildServiceProvider().GetService<IHttpClientFactory>();\n",
" var client = factory.CreateClient(String.Empty);\n",
" var response = await client.GetAsync(\"api/hello/get\");\n",
"\n",
" response.EnsureSuccessStatusCode();\n",
"\n",
" var content = await response.Content.ReadAsStringAsync();\n",
"\n",
" Console.WriteLine($\"API 影响内容:{content}\");\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 7 工厂 + Polly V8"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"IFactoryHttpClient 与 Polly配合可轻松实现重试、熔断、降级、限流等功能本文只是简略的给出常用的使用方法详情会写在 Polly学习项目中。[Polly 官方参考](https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory)\n",
"使用步骤:\n",
"1. 引用 Polly v8 和 Microsoft.Extensions.Http.Polly 包\n",
"2. 配置命名客户端\n",
"3. 使用 AddTransientHttpErrorPolicy 快捷方法,配置策略\n",
"4. 使用其它方式配置,并且可以使用多策略、注册策略、上下文等功能"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 基础应用"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"使用快捷方法AddTransientHttpErrorPolicy进行常用功能使用。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"/*\n",
" 便捷应用AddTransientHttpErrorPolicy() 方法,添加常用瞬时错误重试策略\n",
"*/\n",
"{\n",
" var services = new ServiceCollection();\n",
"\n",
" services.AddHttpClient(string.Empty)\n",
" //配置默认命名客户端\n",
" .ConfigureHttpClient(client => \n",
" {\n",
" client.BaseAddress = new Uri(webApiBaseUrl);\n",
" })\n",
" //设置Policy错误处理快捷扩展方法\n",
" .AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync\n",
" (\n",
" new[]\n",
" {\n",
" TimeSpan.FromSeconds(1),\n",
" TimeSpan.FromSeconds(2),\n",
" TimeSpan.FromSeconds(4),\n",
" }\n",
" ))\n",
" //可以多次调用:设置多个策略\n",
" .AddTransientHttpErrorPolicy(builder => builder.RetryAsync(1));\n",
" \n",
" var factory = services.BuildServiceProvider().GetService<IHttpClientFactory>();\n",
" var content = await factory.CreateClient().GetStringAsync(\"/api/polly8/RandomException\");\n",
"\n",
" Console.WriteLine($\"响应内容:{content}\");\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 使用通过传统 Polly 语法配置的任何策略"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"使用 AddPolicyHandler 方法及其重载也可用于接受任何 IAsyncPolicy<HttpResponseMessage> ,因此可以定义和应用任何类型的策略:可以指定要处理的内容和处理方式。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "csharp"
},
"polyglot_notebook": {
"kernelName": "csharp"
},
"vscode": {
"languageId": "polyglot-notebook"
}
},
"outputs": [],
"source": [
"/*\n",
" 传统方式配置Polly策略\n",
"*/\n",
"//创建策略\n",
"{\n",
" var services = new ServiceCollection();\n",
"\n",
" //重试策略\n",
" var retryePolicy = Policy\n",
" .Handle<HttpRequestException>()\n",
" .OrResult<HttpResponseMessage>(response => \n",
" {\n",
" return response.StatusCode == System.Net.HttpStatusCode.Created;\n",
" })\n",
" .WaitAndRetryAsync(new TimeSpan[]{TimeSpan.FromSeconds(1), TimeSpan.FromMilliseconds(2)});\n",
" //调用\n",
" services\n",
" .AddHttpClient(string.Empty)\n",
" .AddPolicyHandler(retryePolicy);\n",
"\n",
" //超时策略\n",
" var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(10);\n",
" services\n",
" .AddHttpClient(\"timeoutPolicy\")\n",
" .AddPolicyHandler(timeoutPolicy);\n",
" \n",
" /* 普通策略转换\n",
" 所有通过 HttpClient 的调用都返回 HttpResponseMessage 因此配置的策略必须是 IAsyncPolicy<HttpResponseMessage> \n",
" 通过简单、便捷的 AsAsyncPolicy<HttpResponseMessage>()方法,将非通用策略 IAsyncPolicy 转换为 IAsyncPolicy<HttpResponseMessage> \n",
" */\n",
" var timeoutPolicy2 = Policy.TimeoutAsync(2);\n",
"\n",
" services\n",
" .AddHttpClient(\"timeoutPolicy2\")\n",
" //转换通用策略\n",
" .AddPolicyHandler(timeoutPolicy2.AsAsyncPolicy<HttpResponseMessage>());\n",
"}\n",
"\n",
"//示例\n",
"{\n",
" //创建策略\n",
" var policy = Policy.RateLimitAsync<HttpResponseMessage>(3,TimeSpan.FromSeconds(10));\n",
"\n",
" //使用\n",
" var services = new ServiceCollection();\n",
"\n",
" services.AddHttpClient(string.Empty)\n",
" .ConfigureHttpClient(client => \n",
" {\n",
" client.BaseAddress = new Uri(webApiBaseUrl);\n",
" })\n",
" .AddTransientHttpErrorPolicy\n",
" (\n",
" builder => builder.WaitAndRetryAsync\n",
" (\n",
" new[]\n",
" {\n",
" TimeSpan.FromSeconds(1),\n",
" TimeSpan.FromSeconds(2),\n",
" TimeSpan.FromSeconds(4)\n",
" }\n",
" )\n",
" )\n",
" .AddPolicyHandler(policy);\n",
" try\n",
" {\n",
" var factory = services.BuildServiceProvider().GetService<IHttpClientFactory>();\n",
" var content = await factory.CreateClient().GetStringAsync(\"/api/polly8/RandomException\");\n",
" Console.WriteLine($\"响应内容:{content}\");\n",
" }\n",
" catch(Exception ex)\n",
" {\n",
" Console.WriteLine($\"未处理的异常:{ex.Message}\");\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 应用多个策略"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 动态选择策略"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 从注册表中选择策略"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 8、综合管理工厂 + 类型化客户端 请求管道 + Polly(默认使用 连接池和IoC容器)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".NET (C#)",
"language": "C#",
"name": ".net-csharp"
},
"language_info": {
"name": "python"
},
"polyglot_notebook": {
"kernelInfo": {
"defaultKernelName": "csharp",
"items": [
{
"aliases": [],
"name": "csharp"
}
]
}
}
},
"nbformat": 4,
"nbformat_minor": 2
}