diff --git a/Docs/1.3.0.基础使用.管理客户端.ipynb b/Docs/1.3.0.基础使用.管理客户端.ipynb index 5554b89..10eaee0 100644 --- a/Docs/1.3.0.基础使用.管理客户端.ipynb +++ b/Docs/1.3.0.基础使用.管理客户端.ipynb @@ -50,6 +50,8 @@ "#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", @@ -69,11 +71,17 @@ "global using System.Threading.Tasks;\n", "\n", "global using System.Net.Http;\n", - "global using System.Net.Http.Headers;\n", + "global using System.Net.Http.Json;\n", "\n", "global using Microsoft.Extensions.DependencyInjection;\n", "global using Microsoft.Extensions.DependencyInjection.Extensions;\n", "\n", + "global using Microsoft.Extensions.Logging;\n", + "global using Microsoft.Extensions.Logging.Console;\n", + "\n", + "global using Microsoft.Extensions.Http.Logging;\n", + "\n", + "\n", "global using Polly;\n", "global using Polly.NoOp;\n", "global using Polly.Simmy;\n", @@ -634,7 +642,7 @@ " {\n", " await pipleLine.ExecuteAsync(async (inneerToken)=>\n", " {\n", - " var response = await client.GetAsync(\"api/Polly8/Retry_Exception\",inneerToken);\n", + " var response = await client.GetAsync(\"api/Polly8/RetryException\",inneerToken);\n", " response.EnsureSuccessStatusCode();\n", " });\n", " }\n", @@ -859,6 +867,12 @@ "cell_type": "code", "execution_count": null, "metadata": { + "dotnet_interactive": { + "language": "csharp" + }, + "polyglot_notebook": { + "kernelName": "csharp" + }, "vscode": { "languageId": "polyglot-notebook" } @@ -1073,6 +1087,12 @@ "cell_type": "code", "execution_count": null, "metadata": { + "dotnet_interactive": { + "language": "csharp" + }, + "polyglot_notebook": { + "kernelName": "csharp" + }, "vscode": { "languageId": "polyglot-notebook" } @@ -1154,6 +1174,12 @@ "cell_type": "code", "execution_count": null, "metadata": { + "dotnet_interactive": { + "language": "csharp" + }, + "polyglot_notebook": { + "kernelName": "csharp" + }, "vscode": { "languageId": "polyglot-notebook" } @@ -1309,23 +1335,19 @@ }, { "cell_type": "code", - "execution_count": 150, + "execution_count": null, "metadata": { + "dotnet_interactive": { + "language": "csharp" + }, + "polyglot_notebook": { + "kernelName": "csharp" + }, "vscode": { "languageId": "polyglot-notebook" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "LoggerDelegatingHandler -> SendAsync -> Before\n", - "LoggerDelegatingHandler -> SendAsync -> After\n", - "Pong\n" - ] - } - ], + "outputs": [], "source": [ "//管道配置\n", "\n", @@ -1377,8 +1399,8 @@ " handler.SslOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;\n", " })\n", " //使用前先在AddTransient范围注册\n", - " //.AddHttpMessageHandler()\n", - " .AddHttpMessageHandler();\n", + " .AddHttpMessageHandler()\n", + " ;\n", "\n", " var factory = services.BuildServiceProvider().GetService();\n", "\n", @@ -1399,6 +1421,326 @@ "### 日志配置" ] }, + { + "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();\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(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();\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();\n", + "\n", + " services\n", + " // 全局配置\n", + " .ConfigureHttpClientDefaults(options =>\n", + " {\n", + " })\n", + " // 配置到HttpClient\n", + " .AddHttpClient(String.Empty,c =>\n", + " {\n", + " c.BaseAddress = new Uri(webApiBaseUrl);\n", + " })\n", + " //可选:取消默认日志记录\n", + " .RemoveAllLoggers()\n", + " //2、配置到HttpClient\n", + " .AddLogger()\n", + " ;\n", + "\n", + " var factory = services.BuildServiceProvider().GetService();\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 log)\n", + " {\n", + " _log = log;\n", + " }\n", + "\n", + " private static readonly Action _requestStart = LoggerMessage.Define\n", + " (\n", + " LogLevel.Information,\n", + " EventIds.RequestStart,\n", + " \"Request Id={RequestId} ({Host}) started\"\n", + " );\n", + "\n", + " private static readonly Action _requestStop = LoggerMessage.Define\n", + " (\n", + " LogLevel.Information,\n", + " EventIds.RequestStop,\n", + " \"Request Id={RequestId} succeeded in {elapsed}ms\"\n", + " );\n", + "\n", + " private static readonly Action _requestFailed = LoggerMessage.Define\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();\n", + "\n", + " services\n", + " // 全局配置\n", + " .ConfigureHttpClientDefaults(options =>\n", + " {\n", + " })\n", + " // 配置到HttpClient\n", + " .AddHttpClient(String.Empty,c =>\n", + " {\n", + " c.BaseAddress = new Uri(webApiBaseUrl);\n", + " })\n", + " //可选:取消默认日志记录\n", + " .RemoveAllLoggers()\n", + " //2、配置到HttpClient\n", + " .AddLogger()\n", + " ;\n", + "\n", + " var factory = services.BuildServiceProvider().GetService();\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": {}, @@ -1406,6 +1748,149 @@ "## 7 工厂 + Polly V8" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 基础应用" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "工厂可以和Polly(v8)一起使用:\n", + "1. 引用 Polly v8 和 Microsoft.Extensions.Http.Polly 包\n", + "2. 配置命名客户端\n", + "3. 使用 AddTransientHttpErrorPolicy 配置策略" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "dotnet_interactive": { + "language": "csharp" + }, + "polyglot_notebook": { + "kernelName": "csharp" + }, + "vscode": { + "languageId": "polyglot-notebook" + } + }, + "outputs": [], + "source": [ + "//便捷应用:AddTransientHttpErrorPolicy() 方法,添加常用瞬时错误重试策略\n", + "{\n", + " var services = new ServiceCollection();\n", + "\n", + " services.AddHttpClient(\"ClientA\")\n", + " .ConfigureHttpClient(client => \n", + " {\n", + " client.BaseAddress = new Uri(webApiBaseUrl);\n", + " })\n", + " .AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync\n", + " (\n", + " new[]\n", + " {\n", + " TimeSpan.FromSeconds(1),\n", + " TimeSpan.FromSeconds(2),\n", + " TimeSpan.FromSeconds(4)\n", + " }\n", + " ))\n", + " .AddTransientHttpErrorPolicy(builder => builder.Fallback());\n", + " \n", + " var factory = services.BuildServiceProvider().GetService();\n", + " var clientA = factory.CreateClient(\"ClientA\");\n", + " var response = await clientA.GetAsync(\"/api/polly8/RandomException\");\n", + " response.EnsureSuccessStatusCode();\n", + " var content = await response.Content.ReadAsStringAsync();\n", + "\n", + " Console.WriteLine($\"响应内容:{content}\");\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 使用通过传统 Polly 语法配置的任何策略" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "dotnet_interactive": { + "language": "csharp" + }, + "polyglot_notebook": { + "kernelName": "csharp" + }, + "vscode": { + "languageId": "polyglot-notebook" + } + }, + "outputs": [], + "source": [ + "{\n", + "\n", + " var policy = Policy.Handle()\n", + " .WaitAndRetryAsync\n", + " (\n", + " new[]\n", + " {\n", + " TimeSpan.FromSeconds(1),\n", + " TimeSpan.FromSeconds(2),\n", + " TimeSpan.FromSeconds(4)\n", + " }\n", + " );\n", + " var services = new ServiceCollection();\n", + "\n", + " services.AddHttpClient(\"ClientA\")\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", + " \n", + " var factory = services.BuildServiceProvider().GetService();\n", + " var clientA = factory.CreateClient(\"ClientA\");\n", + " var response = await clientA.GetAsync(\"/api/polly8/RandomException\");\n", + " response.EnsureSuccessStatusCode();\n", + " var content = await response.Content.ReadAsStringAsync();\n", + "\n", + " Console.WriteLine($\"响应内容:{content}\");\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 应用多个策略" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 动态选择策略" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/HttpClientStudy.WebApp/Controllers/Polly8Controller.cs b/HttpClientStudy.WebApp/Controllers/Polly8Controller.cs index 109a272..350323c 100644 --- a/HttpClientStudy.WebApp/Controllers/Polly8Controller.cs +++ b/HttpClientStudy.WebApp/Controllers/Polly8Controller.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Http; +using System.Collections.Concurrent; + +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace HttpClientStudy.WebApp.Controllers @@ -10,6 +12,8 @@ namespace HttpClientStudy.WebApp.Controllers [ApiController] public class Polly8Controller : ControllerBase { + private static ConcurrentDictionary ToggleExceptionCache = new ConcurrentDictionary(); + private readonly ILogger _logger; /// @@ -26,11 +30,49 @@ namespace HttpClientStudy.WebApp.Controllers /// /// [HttpGet] - public ActionResult Retry_Exception() + public ActionResult RetryException() + { + //return BadRequest("服务器异常"); + throw new HttpRequestException("服务器异常"); + } + + /// + /// 随机异常 + /// + /// + [HttpGet] + public ActionResult RandomException() + { + var num = Random.Shared.Next(1,100); + if (num >= 50) + { + throw new HttpRequestException("服务器随机异常"); + } + else + { + return Ok(num); + } + } + + /// + /// 切换异常 + /// + /// + [HttpGet] + public ActionResult ToggleException(string toggleId="") { + var toggle = ToggleExceptionCache.GetOrAdd(toggleId, true); + //保存切换 + ToggleExceptionCache[toggleId] = !toggle; - return BadRequest("服务器错误"); - //throw new HttpRequestException(); + if (toggle) + { + throw new HttpRequestException("服务器随机异常"); + } + else + { + return Ok($"toggleId={toggleId}"); + } } } }