main
bicijinlian 8 months ago
parent e2f3245d8f
commit 0c990e96ac

@ -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<LoggerDelegatingHandler>()\n",
" .AddHttpMessageHandler<LoggerDelegatingHandler>();\n",
" .AddHttpMessageHandler<LoggerDelegatingHandler>()\n",
" ;\n",
"\n",
" var factory = services.BuildServiceProvider().GetService<IHttpClientFactory>();\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<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": {},
@ -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<string>());\n",
" \n",
" var factory = services.BuildServiceProvider().GetService<IHttpClientFactory>();\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<Exception>()\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<IHttpClientFactory>();\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": {},

@ -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<string,bool> ToggleExceptionCache = new ConcurrentDictionary<string,bool>();
private readonly ILogger<Polly8Controller> _logger;
/// <summary>
@ -26,11 +30,49 @@ namespace HttpClientStudy.WebApp.Controllers
/// </summary>
/// <returns></returns>
[HttpGet]
public ActionResult Retry_Exception()
public ActionResult RetryException()
{
//return BadRequest("服务器异常");
throw new HttpRequestException("服务器异常");
}
/// <summary>
/// 随机异常
/// </summary>
/// <returns></returns>
[HttpGet]
public ActionResult RandomException()
{
var num = Random.Shared.Next(1,100);
if (num >= 50)
{
throw new HttpRequestException("服务器随机异常");
}
else
{
return Ok(num);
}
}
/// <summary>
/// 切换异常
/// </summary>
/// <returns></returns>
[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}");
}
}
}
}

Loading…
Cancel
Save