diff --git a/Docs/1.3.0.基础使用.管理客户端.ipynb b/Docs/1.3.0.基础使用.管理客户端.ipynb index 25e9830..d3bb494 100644 --- a/Docs/1.3.0.基础使用.管理客户端.ipynb +++ b/Docs/1.3.0.基础使用.管理客户端.ipynb @@ -48,6 +48,7 @@ "source": [ "//全局设置,行运行一次,为后续准备\n", "#r \"nuget:System.Net.Http.Json\"\n", + "#r \"nuget:Microsoft.Net.Http.Headers\"\n", "#r \"nuget:Microsoft.Extensions.Http\"\n", "#r \"nuget:Microsoft.Extensions.DependencyInjection\"\n", "#r \"nuget:Microsoft.Extensions.Logging\" \n", @@ -1999,6 +2000,12 @@ "cell_type": "code", "execution_count": null, "metadata": { + "dotnet_interactive": { + "language": "csharp" + }, + "polyglot_notebook": { + "kernelName": "csharp" + }, "vscode": { "languageId": "polyglot-notebook" } @@ -2048,6 +2055,12 @@ "cell_type": "code", "execution_count": null, "metadata": { + "dotnet_interactive": { + "language": "csharp" + }, + "polyglot_notebook": { + "kernelName": "csharp" + }, "vscode": { "languageId": "polyglot-notebook" } @@ -2097,6 +2110,320 @@ "source": [ "## 8、综合管理:工厂 + 类型化客户端 + 请求管道 + Polly(默认使用 连接池和IoC容器)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 综合示例1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "dotnet_interactive": { + "language": "csharp" + }, + "polyglot_notebook": { + "kernelName": "csharp" + }, + "vscode": { + "languageId": "polyglot-notebook" + } + }, + "outputs": [], + "source": [ + "/* 综合示例1\n", + " 工厂 + 类型化客户端 + 管道 + Polly + 日志(自定义) \n", + "*/\n", + "\n", + "//类型化客户端\n", + "public class HelloApiService \n", + "{\n", + " public HttpClient Client { get; set; }\n", + "\n", + " public HelloApiService(HttpClient httpClient)\n", + " {\n", + " Client = httpClient;\n", + " }\n", + "\n", + " public async Task Ping()\n", + " {\n", + " var content = await Client.GetStringAsync(\"/api/Hello/Ping\");\n", + " return content;\n", + " }\n", + "\n", + " public async Task Index()\n", + " {\n", + " var content = await Client.GetStringAsync(\"/api/Hello/Index\");\n", + " return content;\n", + " }\n", + "\n", + " public async Task Get()\n", + " {\n", + " var content = await Client.GetStringAsync(\"/api/Hello/Get\");\n", + " return content;\n", + " }\n", + "\n", + " public async Task Post()\n", + " {\n", + " var response = await Client.PostAsync(\"/api/Hello/Post\", null);\n", + " var content = await response.Content.ReadAsStringAsync();\n", + " return content;\n", + " }\n", + "}\n", + "\n", + "//类开型客户端\n", + "public class Polly8ApiService \n", + "{\n", + "\n", + " public HttpClient Client { get; set; }\n", + "\n", + " public Polly8ApiService(HttpClient httpClient)\n", + " {\n", + " Client = httpClient;\n", + " } \n", + "\n", + " public async Task Hello()\n", + " {\n", + " var content = await Client.GetStringAsync(\"/api/Polly8/Hello\");\n", + " return content;\n", + " }\n", + "\n", + " public async Task Exception()\n", + " {\n", + " var response = await Client.GetAsync(\"/api/Polly8/Exception\");\n", + " response.EnsureSuccessStatusCode();\n", + " var content = await response.Content.ReadAsStringAsync();\n", + " return content;\n", + " }\n", + "\n", + " public async Task RetryException()\n", + " {\n", + " var response = await Client.GetAsync(\"/api/Polly8/RetryException\");\n", + " response.EnsureSuccessStatusCode();\n", + " var content = await response.Content.ReadAsStringAsync();\n", + " return content;\n", + " }\n", + "\n", + " public async Task RandomException()\n", + " {\n", + " var response = await Client.GetAsync(\"/api/Polly8/RandomException\");\n", + " response.EnsureSuccessStatusCode();\n", + " var content = await response.Content.ReadAsStringAsync();\n", + " return content;\n", + " }\n", + "\n", + " public async Task ToggleException()\n", + " {\n", + " var response = await Client.GetAsync(\"/api/Polly8/ToggleException?toggleId=\"+Guid.NewGuid().ToString());\n", + " response.EnsureSuccessStatusCode();\n", + " var content = await response.Content.ReadAsStringAsync();\n", + " return content;\n", + " }\n", + "}\n", + "\n", + "//Token管理中间件\n", + "public class TokenDelegatingHandler : DelegatingHandler \n", + "{\n", + " protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)\n", + " {\n", + " Console.WriteLine(\"TokenDelegatingHandler -> Send -> Added Token\");\n", + "\n", + " if (!request.Headers.Contains(Microsoft.Net.Http.Headers.HeaderNames.Authorization)) \n", + " {\n", + " Console.WriteLine(\"没有 Token, TokenDelegatingHandler 添加之\");\n", + " request.Headers.Add(Microsoft.Net.Http.Headers.HeaderNames.Authorization, \"Bearer \" + \"a.b.c\");\n", + " }\n", + " else\n", + " {\n", + " Console.WriteLine($\"已有Token, {request.Headers.Authorization}\");\n", + " }\n", + "\n", + " HttpResponseMessage response = base.Send(request, cancellationToken);\n", + "\n", + " Console.WriteLine(\"TokenDelegatingHandler -> Send -> After\");\n", + "\n", + " return response;\n", + " }\n", + "\n", + " protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n", + " {\n", + " Console.WriteLine(\"TokenDelegatingHandler -> SendAsync -> Before\");\n", + "\n", + " HttpResponseMessage response = await base.SendAsync(request, cancellationToken);\n", + "\n", + " Console.WriteLine(\"TokenDelegatingHandler -> SendAsync -> After\");\n", + "\n", + " return response;\n", + " }\n", + "}\n", + "\n", + "//自定义日志\n", + "public class CustomLogger : 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", + "//polly策略\n", + "var policy = Policy\n", + " .Handle()\n", + " .OrResult(message => message.StatusCode != System.Net.HttpStatusCode.OK)\n", + " .WaitAndRetryAsync(new TimeSpan[]{TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2),TimeSpan.FromSeconds(4),});\n", + "\n", + "//使用\n", + "{\n", + " var services = new ServiceCollection();\n", + "\n", + " //注册基础类型\n", + " services\n", + " //注册日志类\n", + " .AddTransient()\n", + " .AddScoped()\n", + " ;\n", + "\n", + " //基础配置\n", + " services\n", + " // 基础日志配置(默认日志)\n", + " .AddLogging(builder => \n", + " {\n", + " //日志级别\n", + " builder.SetMinimumLevel(LogLevel.Trace);\n", + "\n", + " //控制台日志\n", + " builder.AddConsole();\n", + " })\n", + " //全局配置\n", + " .ConfigureHttpClientDefaults(clientBuilder =>\n", + " {\n", + " clientBuilder.AddDefaultLogger();\n", + " clientBuilder.ConfigureHttpClient(client => \n", + " {\n", + " client.BaseAddress = new Uri(webApiBaseUrl);\n", + " });\n", + " });\n", + "\n", + " //默认命名客户端\n", + " services.AddHttpClient(string.Empty, config => \n", + " {\n", + " config.DefaultRequestHeaders.Add(\"X-Custom-Demo\", \"true\");\n", + " })\n", + " //配置客户端\n", + " .ConfigureHttpClient(client => \n", + " {\n", + " //client.BaseAddress = new Uri(webApiBaseUrl);\n", + " client.Timeout = TimeSpan.FromSeconds(10);\n", + " })\n", + " //添加类型化客户端\n", + " .AddTypedClient()\n", + " //添加自定义管道\n", + " .AddHttpMessageHandler()\n", + " //添加默认日志:全局配置已添加\n", + " //.AddDefaultLogger()\n", + " //添加自定义日志\n", + " .AddLogger()\n", + " //日志转发头(所有请求头)\n", + " .RedactLoggedHeaders( headerName => true)\n", + "\n", + " //配置SocketsHttpHandler\n", + " .UseSocketsHttpHandler(config =>\n", + " {\n", + " //配置连接池等\n", + " config.Configure((handler,provider) => \n", + " {\n", + " handler.AllowAutoRedirect = true;\n", + " handler.PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30);\n", + " handler.PooledConnectionLifetime = TimeSpan.FromSeconds(30);\n", + " handler.UseProxy = false;\n", + " handler.UseCookies = true;\n", + " });\n", + " })\n", + " //设置生命周期\n", + " .SetHandlerLifetime(TimeSpan.FromSeconds(30))\n", + " //Polly策略配置\n", + " .AddPolicyHandler(policy)\n", + " //便捷配置\n", + " .AddTransientHttpErrorPolicy(builder => builder.CircuitBreakerAsync(11, TimeSpan.FromSeconds(30)))\n", + " ;\n", + "\n", + " //自定义\n", + " services.AddHttpClient(\"ClientA\", config => \n", + " {\n", + " config.DefaultRequestHeaders.Add(\"X-Custom-Demo\", \"ClientA\");\n", + " })\n", + " //配置客户端\n", + " .ConfigureHttpClient(client => \n", + " {\n", + " //client.BaseAddress = new Uri(webApiBaseUrl);\n", + " client.Timeout = TimeSpan.FromSeconds(10);\n", + " })\n", + " //添加类型化客户端\n", + " .AddTypedClient()\n", + " //添加自定义管道\n", + " .AddHttpMessageHandler()\n", + " //添加默认日志:全局配置已添加\n", + " //.AddDefaultLogger()\n", + " //添加自定义日志\n", + " .AddLogger()\n", + " //日志转发头(所有请求头)\n", + " .RedactLoggedHeaders( headerName => true)\n", + " //配置SocketsHttpHandler\n", + " .UseSocketsHttpHandler(config =>\n", + " {\n", + " //配置连接池等\n", + " config.Configure((handler,provider) => \n", + " {\n", + " handler.AllowAutoRedirect = true;\n", + " handler.PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30);\n", + " handler.PooledConnectionLifetime = TimeSpan.FromSeconds(30);\n", + " handler.UseProxy = false;\n", + " handler.UseCookies = true;\n", + " });\n", + " })\n", + " //设置生命周期\n", + " .SetHandlerLifetime(TimeSpan.FromSeconds(30))\n", + " //Polly策略配置\n", + " .AddPolicyHandler(policy)\n", + " //便捷配置\n", + " .AddTransientHttpErrorPolicy(builder => builder.CircuitBreakerAsync(11, TimeSpan.FromSeconds(30)))\n", + " ;\n", + "\n", + " var factory = services.BuildServiceProvider().GetRequiredService();\n", + "\n", + " var defaultClient = factory.CreateClient();\n", + " var defaultContent = await defaultClient.GetStringAsync(\"api/hello/ping\");\n", + " Console.WriteLine(defaultContent);\n", + "\n", + " var clientA = factory.CreateClient();\n", + " var contentA = await clientA.GetStringAsync(\"api/polly8/hello\");\n", + " Console.WriteLine(contentA);\n", + "\n", + " //类型化客户端\n", + " HelloApiService helloApiService = services.BuildServiceProvider().GetRequiredService();\n", + " Console.WriteLine(await helloApiService.Ping());\n", + " Console.WriteLine(await helloApiService.Index());\n", + " Console.WriteLine(await helloApiService.Get());\n", + " Console.WriteLine(await helloApiService.Post());\n", + "\n", + " Polly8ApiService polly8ApiService = services.BuildServiceProvider().GetRequiredService();\n", + " Console.WriteLine(await polly8ApiService.Hello());\n", + "\n", + "}\n" + ] } ], "metadata": { diff --git a/HttpClientStudy.Core/CustomHttpClient/CustomLogger.cs b/HttpClientStudy.Core/CustomHttpClient/CustomLogger.cs new file mode 100644 index 0000000..08cf7be --- /dev/null +++ b/HttpClientStudy.Core/CustomHttpClient/CustomLogger.cs @@ -0,0 +1,23 @@ +namespace HttpClientStudy.Core.CustomHttpClient +{ + /// + /// 自定义日志 + /// + public class CustomLogger : IHttpClientLogger + { + public object? LogRequestStart(HttpRequestMessage request) + { + return null; + } + + public void LogRequestStop(object? ctx, HttpRequestMessage request, HttpResponseMessage response, TimeSpan elapsed) + { + Console.WriteLine($"自定义日志:{request.Method} {request.RequestUri?.AbsoluteUri} - {(int)response.StatusCode} {response.StatusCode} in {elapsed.TotalMilliseconds}ms"); + } + + public void LogRequestFailed(object? ctx, HttpRequestMessage request, HttpResponseMessage? response, Exception e, TimeSpan elapsed) + { + Console.WriteLine($"自定义日志:{request.Method} {request.RequestUri?.AbsoluteUri} - Exception {e.GetType().FullName}: {e.Message}"); + } + } +} diff --git a/HttpClientStudy.Core/CustomHttpClient/HelloApiService.cs b/HttpClientStudy.Core/CustomHttpClient/HelloApiService.cs new file mode 100644 index 0000000..93aa6a8 --- /dev/null +++ b/HttpClientStudy.Core/CustomHttpClient/HelloApiService.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HttpClientStudy.Core.CustomHttpClient +{ + /// + /// HelloApi + /// 类型化客户端 + /// + public class HelloApiService + { + public HttpClient Client { get; set; } + + public HelloApiService(HttpClient httpClient) + { + Client = httpClient; + } + + public async Task Ping() + { + var content = await Client.GetStringAsync("/api/Hello/Ping"); + return content; + } + + public async Task Index() + { + var content = await Client.GetStringAsync("/api/Hello/Index"); + return content; + } + + public async Task Get() + { + var content = await Client.GetStringAsync("/api/Hello/Get"); + return content; + } + + public async Task Post() + { + var response = await Client.PostAsync("/api/Hello/Post", null); + var content = await response.Content.ReadAsStringAsync(); + return content; + } + } +} diff --git a/HttpClientStudy.Core/CustomHttpClient/Polly8ApiService.cs b/HttpClientStudy.Core/CustomHttpClient/Polly8ApiService.cs new file mode 100644 index 0000000..a87a11d --- /dev/null +++ b/HttpClientStudy.Core/CustomHttpClient/Polly8ApiService.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HttpClientStudy.Core.CustomHttpClient +{ + /// + /// Polly V8 + /// 类型化客户端 + /// + public class Polly8ApiService + { + public HttpClient Client { get; set; } + + public Polly8ApiService(HttpClient httpClient) + { + Client = httpClient; + } + + public async Task Hello() + { + var content = await Client.GetStringAsync("/api/Polly8/Hello"); + return content; + } + + public async Task Exception() + { + var response = await Client.GetAsync("/api/Polly8/Exception"); + response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync(); + return content; + } + + public async Task RetryException() + { + var response = await Client.GetAsync("/api/Polly8/RetryException"); + response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync(); + return content; + } + + public async Task RandomException() + { + var response = await Client.GetAsync("/api/Polly8/RandomException"); + response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync(); + return content; + } + + public async Task ToggleException() + { + var response = await Client.GetAsync("/api/Polly8/ToggleException?toggleId=" + Guid.NewGuid().ToString()); + response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync(); + return content; + } + } +} diff --git a/HttpClientStudy.Core/CustomHttpClient/TokenDelegatingHandler.cs b/HttpClientStudy.Core/CustomHttpClient/TokenDelegatingHandler.cs new file mode 100644 index 0000000..d38242e --- /dev/null +++ b/HttpClientStudy.Core/CustomHttpClient/TokenDelegatingHandler.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.Net.Http.Headers; + +namespace HttpClientStudy.Core.CustomHttpClient +{ + /// + /// oken管理中间件 + /// + public class TokenDelegatingHandler : DelegatingHandler + { + protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) + { + Console.WriteLine("TokenDelegatingHandler -> Send -> Added Token"); + + if (request.Headers.Authorization is null) + { + request.Headers.Add(HeaderNames.Accept, "Bearer " + "a.b.c"); + } + + HttpResponseMessage response = base.Send(request, cancellationToken); + + Console.WriteLine("TokenDelegatingHandler -> Send -> After"); + + return response; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Console.WriteLine("TokenDelegatingHandler -> SendAsync -> Before"); + + HttpResponseMessage response = await base.SendAsync(request, cancellationToken); + + Console.WriteLine("TokenDelegatingHandler -> SendAsync -> After"); + + return response; + } + } +} diff --git a/HttpClientStudy.Core/GlobalUsings.cs b/HttpClientStudy.Core/GlobalUsings.cs index 25ab24c..04f9c70 100644 --- a/HttpClientStudy.Core/GlobalUsings.cs +++ b/HttpClientStudy.Core/GlobalUsings.cs @@ -3,14 +3,46 @@ global using System.Linq; global using System.Text; global using System.Threading; global using System.Threading.Tasks; -global using System.Collections.Generic; - -global using System.Net; global using System.Net.Mime; global using System.Net.Http; global using System.Net.Http.Json; global using System.Net.Http.Headers; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.Configuration.Memory; +global using Microsoft.Extensions.Configuration.CommandLine; +global using Microsoft.Extensions.Configuration.EnvironmentVariables; +global using Microsoft.Extensions.Configuration.UserSecrets; +global using Microsoft.Extensions.Configuration.KeyPerFile; +global using Microsoft.Extensions.Configuration.Ini; +global using Microsoft.Extensions.Configuration.Json; +global using Microsoft.Extensions.Configuration.Xml; +global using Microsoft.Extensions.Options; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; + +global using Microsoft.Net.Http; +global using Microsoft.Net.Http.Headers; +global using Microsoft.Extensions.Http; +global using Microsoft.Extensions.Http.Logging; + +global using Polly; +global using Polly.NoOp; +global using Polly.Retry; +global using Polly.Bulkhead; +global using Polly.Fallback; +global using Polly.Hedging; +global using Polly.RateLimit; +global using Polly.Caching; +global using Polly.Timeout; +global using Polly.CircuitBreaker; +global using Polly.Wrap; +global using Polly.Registry; +global using Polly.Utilities; +global using Polly.Telemetry; +global using Polly.Extensions; +global using Polly.Extensions.Http; + global using HttpClientStudy.Config; global using HttpClientStudy.Model;