diff --git a/Docs/1.3.0.基础使用.管理客户端.ipynb b/Docs/1.3.0.基础使用.管理客户端.ipynb index c2694db..5554b89 100644 --- a/Docs/1.3.0.基础使用.管理客户端.ipynb +++ b/Docs/1.3.0.基础使用.管理客户端.ipynb @@ -32,7 +32,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": { "dotnet_interactive": { "language": "csharp" @@ -44,26 +44,7 @@ "languageId": "polyglot-notebook" } }, - "outputs": [ - { - "data": { - "text/html": [ - "
Installed Packages
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "配置文件根目录:e:\\王高峰\\我的项目\\学习项目\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.Core\n", - "启动WebApi项目\n", - "程序[e:\\王高峰\\我的项目\\学习项目\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.WebApp\\HttpClientStudy.WebApp.exe]已在新的命令行窗口执行。如果未出现新命令行窗口,可能是程序错误造成窗口闪现!\n" - ] - } - ], + "outputs": [], "source": [ "//全局设置,行运行一次,为后续准备\n", "#r \"nuget:System.Net.Http.Json\"\n", @@ -855,6 +836,159 @@ "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())均返回 IHttpClientBuilder,明显针对命名客户端。\n", + "AddHttpClient() 相当于注册了基本框架;而命名客户端中,名称为空(\"\"或string.Empty)的,相当于默认客户端。\n", + "\n", + "有一个 名为 `ConfigureHttpClientDefaults` 的 ServiceCollection 对象的扩展方法,用于配置所有HttpClient实例,并且只在初始化时执行一次。如果只使用一个默认客户端的话,可以使用 ConfigureHttpClientDefaults 和 AddHttpClient() 配合使用,也能达到默认客户端的配置效果。 " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "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();\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();\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(string.Empty)\n", + " //这样后续的配置,都是针对 string.empty 的客户端,可以使用全部配置功能\n", + " .ConfigureHttpClient(c=>c.BaseAddress = new Uri(webApiBaseUrl))\n", + " .AddDefaultLogger();\n", + "\n", + " var factory = services.BuildServiceProvider().GetRequiredService();\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()\n", + " //没有参数时,导致后面配置不起使用\n", + " .ConfigureHttpClient(c=>c.BaseAddress = new Uri(webApiBaseUrl))\n", + " .AddDefaultLogger();\n", + "\n", + " var factory = services.BuildServiceProvider().GetRequiredService();\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(),因后续配置中,赋值 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, @@ -871,10 +1005,11 @@ }, "outputs": [], "source": [ - "//所有HttpClient配置\n", + "//全局配置:所有HttpClient配置\n", "{\n", " var services = new ServiceCollection();\n", - " //添加一个委托,用于配置所有HttpClient实例。只执行一次。\n", + " //添加一个委托,用于配置所有HttpClient实例。\n", + " //只执行一次,而非每次CreateClient,都会执行一次。\n", " services.ConfigureHttpClientDefaults(builder => \n", " {\n", " //builder.UseSocketsHttpHandler();\n", @@ -884,60 +1019,83 @@ " {\n", " hc.BaseAddress = new Uri(webApiBaseUrl);\n", " });\n", - " });\n", - "\n", - " var factory = services.BuildServiceProvider().GetRequiredService();\n", "\n", - " var client = factory.CreateClient();\n", - " var response = await client.GetAsync(\"/api/hello/ping\");\n", - " var data = await response.Content.ReadAsStringAsync();\n", - " Console.WriteLine(data);\n", - "}\n", + " Console.WriteLine(\"ConfigureHttpClientDefaults 只执行一次!\");\n", + " });\n", "\n", - "/*\n", - " 默认 HttpClient\n", - " 1、配置默认HttpClient时,AddHttpClient(Action)的Action不会被执行,ConfigureHttpClient()也不执行。原因暂时不明\n", - " 2、想配置默认HttpClient,可以使用ConfigureHttpClientDefaults方法,但此方法针对所有HttpClient实例(包括命名HttpClient)\n", - "*/\n", - "{\n", - " var services = new ServiceCollection();\n", + " //配置命名客户端\n", + " services\n", + " .AddHttpClient(\"client_a\")\n", + " .ConfigureHttpClient(hc => \n", + " {\n", + " hc.DefaultRequestHeaders.Add(\"client_a\", \"client_a\");\n", "\n", - " //默认HttpClient\n", - " services.AddHttpClient();\n", + " //可以覆盖默认配置\n", + " //hc.BaseAddress = new Uri(\"http://www.qq.com\");\n", "\n", - " //这种形式,不生效\n", - " // services.AddHttpClient((provider, client) => \n", - " // {\n", - " // client.BaseAddress = new Uri(webApiBaseUrl);\n", - " // });\n", + " Console.WriteLine(\"ConfigureHttpClient 每次 CreateClient 执行一次!\");\n", + " });\n", "\n", - " //这种形式,ConfigureHttpClient 也不生效\n", - " // services.AddHttpClient()\n", - " // .ConfigureHttpClient(config=>\n", - " // {\n", - " // config.BaseAddress = new Uri(webApiBaseUrl);\n", - " // });\n", + " \n", + " var factory = services.BuildServiceProvider().GetRequiredService();\n", "\n", - " var provider = services.BuildServiceProvider();\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", - " var factory = provider.GetRequiredService();\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", - " var response = await factory.CreateClient().GetAsync(webApiBaseUrl+\"/api/hello/index\");\n", - " var content = await response.Content.ReadAsStringAsync();\n", - " \n", - " Console.WriteLine(content);\n", - "}\n", - "\n", - "//命名 HttpClient\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": { + "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(string.Empty, (provider, client) => \n", + " {\n", + " client.BaseAddress = new Uri(webApiBaseUrl);\n", + " });\n", + "\n", " services.AddHttpClient(clientA, (provider, client) => \n", " {\n", " client.BaseAddress = new Uri(webApiBaseUrl);\n", " });\n", + "\n", " services.AddHttpClient(clientB, (provider, client) => \n", " {\n", " client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;\n", @@ -951,15 +1109,57 @@ "\n", " var factory = services.BuildServiceProvider().GetRequiredService();\n", "\n", - " var responseA = await factory.CreateClient(clientA).GetAsync(\"/api/hello/ping\");\n", - " var contentA = await responseA.Content.ReadAsStringAsync();\n", - " Console.WriteLine(contentA);\n", - "\n", - " var responseB = await factory.CreateClient(clientB).PostAsync(\"/api/hello/post\",null);\n", - " var contentB = await responseB.Content.ReadAsStringAsync();\n", - " Console.WriteLine(contentB);\n", - "}\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": { + "vscode": { + "languageId": "polyglot-notebook" + } + }, + "outputs": [], + "source": [ "// 类型化客户端 HttpClient\n", "public class HttpClientServiceA\n", "{\n", @@ -995,13 +1195,73 @@ " }\n", "}\n", "\n", + "// 方式1(不推荐):类型化客户端:直接注入IoC,并从中获取实例。优点是范围可以自己选择。\n", "{\n", + " Console.WriteLine(\"方式1 -------------------------------------------------------------------\");\n", + " var services = new ServiceCollection();\n", + " services.AddSingleton(b => \n", + " { \n", + " return new HttpClientServiceA(new HttpClient(){BaseAddress = new Uri(webApiBaseUrl)});\n", + " });\n", + " services.AddScoped(b=> \n", + " {\n", + " return new HttpClientServiceB(new HttpClient(){BaseAddress = new Uri(webApiBaseUrl)});\n", + " });\n", + "\n", + " var builder = services.BuildServiceProvider();\n", + " var serverA = builder.GetRequiredService();\n", + " var serverB = builder.GetRequiredService();\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()\n", + " .ConfigureHttpClient(client=>\n", + " {\n", + " client.BaseAddress = new Uri(webApiBaseUrl);\n", + " });\n", + "\n", + " services\n", + " .AddHttpClient()\n", + " .ConfigureHttpClient(client=>\n", + " {\n", + " client.BaseAddress = new Uri(webApiBaseUrl);\n", + " });\n", + "\n", + " var builder = services.BuildServiceProvider();\n", + " var serverA = builder.GetRequiredService();\n", + " var serverB = builder.GetRequiredService();\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(client => \n", " {\n", " client.BaseAddress = new Uri(webApiBaseUrl);\n", " Console.WriteLine(\"HttpClientServiceA => AddHttpClient 执行一次\");\n", " })\n", + " .AddTypedClient()\n", " .ConfigureHttpClient(client=>\n", " {\n", " client.Timeout = TimeSpan.FromSeconds(1);\n", @@ -1013,6 +1273,7 @@ " client.BaseAddress = new Uri(webApiBaseUrl);\n", " Console.WriteLine(\"HttpClientServiceB => AddHttpClient 执行一次\");\n", " })\n", + " .AddTypedClient()\n", " .ConfigureHttpClient(client=>\n", " {\n", " client.Timeout = TimeSpan.FromSeconds(2);\n", @@ -1034,39 +1295,122 @@ "\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": 150, + "metadata": { + "vscode": { + "languageId": "polyglot-notebook" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LoggerDelegatingHandler -> SendAsync -> Before\n", + "LoggerDelegatingHandler -> SendAsync -> After\n", + "Pong\n" + ] + } + ], + "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 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", - "// 类型化客户端2\n", + "//使用日志中间件\n", "{\n", " var services = new ServiceCollection();\n", - " services.AddHttpClient(client => \n", + "\n", + " //先注册\n", + " services.AddTransient();\n", + "\n", + " services.AddHttpClient(string.Empty).ConfigureHttpClient(client =>\n", " {\n", " client.BaseAddress = new Uri(webApiBaseUrl);\n", - " Console.WriteLine(\"HttpClientServiceA => AddHttpClient 执行一次\");\n", " })\n", - " .AddTypedClient()\n", - " .ConfigureHttpClient(client=>\n", + " //配置SocketsHttpHandler\n", + " .UseSocketsHttpHandler((handler,provider) =>\n", " {\n", - " client.Timeout = TimeSpan.FromSeconds(1);\n", - " Console.WriteLine(\"HttpClientServiceA => ConfigureHttpClient 执行一次\");\n", - " });\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()\n", + " .AddHttpMessageHandler();\n", "\n", - "//todo: 生成式 HttpClient: Refit库\n", + " var factory = services.BuildServiceProvider().GetService();\n", "\n", - "/*\n", - " IFactoryHttpClient + Polly8\n", - " 1、引用库 Polly 和 Microsoft.Extensions.Http.Polly\n", - "*/\n", + " var client = factory.CreateClient();\n", + "\n", + " var response = await client.GetAsync(\"/api/hello/ping\");\n", + " response.EnsureSuccessStatusCode();\n", "\n", - "\n" + " var responseString = await response.Content.ReadAsStringAsync();\n", + " Console.WriteLine(responseString);\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 日志配置" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7 工厂 + Polly V8" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## 7、综合管理:工厂 + 类型化客户端 + 请求管道 + Polly(默认使用 连接池和IoC容器)" + "## 8、综合管理:工厂 + 类型化客户端 + 请求管道 + Polly(默认使用 连接池和IoC容器)" ] } ],