{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    }
   },
   "source": [
    "# HttpClient 处理错误与异常"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "根据客户端的创建方法不周,有不同的错误与异常处理方式。推荐类型化客户端、工厂和Polly库中统一处理!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "source": [
    "## 初始化"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "## 初始化\n",
       "这是全局共用文件,包括Nuget包引用、全局类库引用、全局文件引用、全局命名空间引用、全局变量、全局方法、全局类定义等功能。\n",
       "\n",
       "在业务笔记中引用,执行其它单元格之前先执行一次。"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div><div></div><div></div><div><strong>Installed Packages</strong><ul><li><span>Microsoft.Extensions.DependencyInjection, 9.0.4</span></li><li><span>Microsoft.Extensions.Http, 9.0.4</span></li><li><span>Microsoft.Extensions.Http.Polly, 9.0.4</span></li><li><span>Microsoft.Extensions.Logging, 9.0.4</span></li><li><span>Microsoft.Extensions.Logging.Console, 9.0.4</span></li><li><span>Microsoft.Net.Http.Headers, 9.0.4</span></li><li><span>Polly, 8.5.2</span></li><li><span>Refit, 8.0.0</span></li><li><span>Refit.HttpClientFactory, 8.0.0</span></li><li><span>System.Net.Http.Json, 9.0.4</span></li></ul></div></div>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "配置文件根目录:c:\\Users\\ruyu\\Desktop\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.Core\n",
      "配置文件根目录:c:\\Users\\ruyu\\Desktop\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.Core\n",
      "启动WebApi项目...\n",
      "程序[c:\\Users\\ruyu\\Desktop\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.WebApp\\HttpClientStudy.WebApp.exe]已在新的命令行窗口执行。如果未出现新命令行窗口,可能是程序错误造成窗口闪现!\n",
      "已启动WebApi项目,保持窗口打开状态!\n",
      "初始化完成!\n"
     ]
    }
   ],
   "source": [
    "#!import ./Ini.ipynb"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 常规方法"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "直接实例化客户端时,可以用`Try Catch`或者`EnsureSuccessStatusCode方法`简单处理, 这种是最不推荐的方式!\n",
    "\n",
    "推荐使用后面介绍的 Pipeline 管道方式"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "+ try catch"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "远程请求失败,响应码 InternalServerError\r\n"
     ]
    }
   ],
   "source": [
    "/*\n",
    "    try catch 处理异常\n",
    "*/\n",
    "try\n",
    "{\n",
    "    var requestUri = new Uri(webApiBaseUri, \"/api/ErrorDemo/Error500\");\n",
    "\n",
    "    using(var client = new HttpClient())\n",
    "    {\n",
    "        //发送请求\n",
    "        var response = await client.GetAsync(requestUri);\n",
    "        if(response.IsSuccessStatusCode)\n",
    "        {\n",
    "            Console.WriteLine($\"远程请求成功,响应码 {response.StatusCode}\");\n",
    "        }\n",
    "        else\n",
    "        {\n",
    "            Console.WriteLine($\"远程请求失败,响应码 {response.StatusCode}\");\n",
    "        }\n",
    "    }\n",
    "}\n",
    "catch(Exception ex)\n",
    "{\n",
    "    Console.WriteLine($\"远程调用异常:{ex.Message}\");\n",
    "}\n",
    "finally\n",
    "{\n",
    "    //清理业务\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "+ Response的EnsureSuccessStatusCode方法"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [
    {
     "ename": "Error",
     "evalue": "System.Net.Http.HttpRequestException: Response status code does not indicate success: 500 (Internal Server Error).\r\n   at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()\r\n   at Submission#3.<<Initialize>>d__0.MoveNext()\r\n--- End of stack trace from previous location ---\r\n   at Microsoft.CodeAnalysis.Scripting.ScriptExecutionState.RunSubmissionsAsync[TResult](ImmutableArray`1 precedingExecutors, Func`2 currentExecutor, StrongBox`1 exceptionHolderOpt, Func`2 catchExceptionOpt, CancellationToken cancellationToken)",
     "output_type": "error",
     "traceback": [
      "System.Net.Http.HttpRequestException: Response status code does not indicate success: 500 (Internal Server Error).\r\n",
      "   at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()\r\n",
      "   at Submission#3.<<Initialize>>d__0.MoveNext()\r\n",
      "--- End of stack trace from previous location ---\r\n",
      "   at Microsoft.CodeAnalysis.Scripting.ScriptExecutionState.RunSubmissionsAsync[TResult](ImmutableArray`1 precedingExecutors, Func`2 currentExecutor, StrongBox`1 exceptionHolderOpt, Func`2 catchExceptionOpt, CancellationToken cancellationToken)"
     ]
    }
   ],
   "source": [
    "{ //EnsureSuccessStatusCode方法\n",
    "\n",
    "    var requestUri = new Uri(webApiBaseUri, \"/api/ErrorDemo/Error500\");\n",
    "\n",
    "    using(var client = new HttpClient())\n",
    "    {\n",
    "        //发送请求\n",
    "        var response = await client.GetAsync(requestUri);\n",
    "\n",
    "        //确保响应码正确,否则异常\n",
    "        response.EnsureSuccessStatusCode();\n",
    "\n",
    "        //输出响应码\n",
    "        Console.WriteLine($\"远程请求成功,响应码 {response.StatusCode}\");\n",
    "    }\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 静态或工具类中处理"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "静态类或工具类,可以在内部方法里使用 Try Catch 等常规方法进行异常处理。\n",
    "\n",
    "注意:推荐使用后面介绍的 Pipeline 管道方式"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "+ 内部处理"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "配置文件根目录:c:\\Users\\ruyu\\Desktop\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.Core\n",
      "正常请求结果:{\"data\":{\"host\":\"localhost\",\"port\":5189,\"scheme\":\"http\",\"pathBase\":\"\",\"baseUrl\":\"http://localhost:5189\",\"webAppMutexName\":\"HttpClientStudy.WebApp\"},\"code\":1,\"message\":\"成功\"}\n",
      "异常请求结果:StatusCode: 500, ReasonPhrase: 'Internal Server Error', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:\n",
      "{\n",
      "  Date: Tue, 13 May 2025 04:30:54 GMT\n",
      "  Server: Kestrel\n",
      "  Transfer-Encoding: chunked\n",
      "  X-WebApi-UseTime: 685\n",
      "  Content-Type: text/plain; charset=utf-8\n",
      "}\n"
     ]
    }
   ],
   "source": [
    "//静态类中处理异常\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",
    "            //统一设置: 基础Uri\n",
    "            BaseAddress = new Uri(WebApiConfigManager.GetWebApiConfig().BaseUrl),\n",
    "        };\n",
    "\n",
    "        //统一设置:请求头等\n",
    "        StaticClient.DefaultRequestHeaders.Add(\"x-custom-time\",\"12ms\");\n",
    "\n",
    "        //统一错误处理:可以在Pipline中统一设置,后面有单独章节\n",
    "    } \n",
    "\n",
    "    public static async Task<HttpResponseMessage> GetAsync(string url)\n",
    "    {\n",
    "        //异常处理\n",
    "        try\n",
    "        {\n",
    "            return await StaticClient.GetAsync(url);\n",
    "        }\n",
    "        catch(Exception ex)\n",
    "        {\n",
    "            Console.WriteLine($\"远程调用发生异常:{ex.Message}\");\n",
    "            return await Task.FromResult(new HttpResponseMessage(HttpStatusCode.ExpectationFailed));\n",
    "        }\n",
    "        finally\n",
    "        {\n",
    "            //清理业务\n",
    "        }\n",
    "    }\n",
    "\n",
    "    public static async Task<string> GetStringAsync(string url)\n",
    "    {\n",
    "        var response = await StaticClient.GetAsync(url);\n",
    "\n",
    "        //异常处理\n",
    "        response.EnsureSuccessStatusCode();\n",
    "        return await response.Content.ReadAsStringAsync();\n",
    "    }\n",
    "\n",
    "    public static async Task<HttpResponseMessage> PostAsync(string url, HttpContent content)\n",
    "    {\n",
    "        //异常处理\n",
    "        try\n",
    "        {\n",
    "            return await StaticClient.PostAsync(url, content);\n",
    "        }\n",
    "        catch(Exception ex)\n",
    "        {\n",
    "            Console.WriteLine($\"远程调用发生异常:{ex.Message}\");\n",
    "            return await Task.FromResult(new HttpResponseMessage(HttpStatusCode.ExpectationFailed));\n",
    "        }\n",
    "        finally\n",
    "        {\n",
    "            //清理业务\n",
    "        }\n",
    "    }\n",
    "}\n",
    "\n",
    "{   //正常请求\n",
    "    var response = await HttpClientHelper.GetAsync(\"/api/Config/GetApiConfig\");\n",
    "    var content = await response.Content.ReadAsStringAsync();\n",
    "    Console.WriteLine($\"正常请求结果:{content}\");\n",
    "\n",
    "    //异常请求\n",
    "    var response2 = await HttpClientHelper.GetAsync(\"/api/ErrorDemo/Error500\");\n",
    "    Console.WriteLine($\"异常请求结果:{response2}\");\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "+ 添加异常Pipeline管道(下面专项介绍)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 类型化客户端中处理"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "类型化客户端有三种方法:一是类内部处理;二是Pipeline,统一处理;三是结合IoC/工厂和Polly库,初始设置时统一处理。\n",
    "\n",
    "推荐第二、第三种,后面有专项介绍。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "+ 内部处理,类似静态或工具类"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "HttpClientServiceA => 构造函数执行一次\n",
      "System.Exception: 服务器异常\n",
      "   at HttpClientStudy.WebApp.Controllers.ErrorDemoController.Error500() in C:\\Users\\ruyu\\Desktop\\HttpClientStudy\\HttpClientStudy.WebApp\\Controllers\\ErrorDemoController.cs:line 37\n",
      "   at lambda_method3(Closure, Object, Object[])\n",
      "   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncActionResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)\n",
      "   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)\n",
      "   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)\n",
      "   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)\n",
      "   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)\n",
      "   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()\n",
      "--- End of stack trace from previous location ---\n",
      "   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)\n",
      "   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)\n",
      "   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)\n",
      "   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|7_0(Endpoint endpoint, Task requestTask, ILogger logger)\n",
      "   at HttpClientStudy.WebApp.Program.<>c.<<Main>b__0_12>d.MoveNext() in C:\\Users\\ruyu\\Desktop\\HttpClientStudy\\HttpClientStudy.WebApp\\Program.cs:line 258\n",
      "--- End of stack trace from previous location ---\n",
      "   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)\n",
      "   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)\n",
      "   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)\n",
      "   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)\n",
      "   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)\n",
      "   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)\n",
      "\n",
      "HEADERS\n",
      "=======\n",
      "Host: 127.0.0.1:5189\n",
      "traceparent: 00-8cccd683184035a261e8996c74c87f82-7440f8a982d79135-00\n",
      "\n"
     ]
    }
   ],
   "source": [
    "// 类型化客户端A 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",
    "    //常规方法:异常可在方法内处理\n",
    "    public async Task<string> GetAsync()\n",
    "    {\n",
    "        var response = await Client.GetAsync(\"/api/ErrorDemo/Error500\");\n",
    "        var content = await response.Content.ReadAsStringAsync();\n",
    "        return content;\n",
    "    }\n",
    "}\n",
    "\n",
    "// 使用类型化客户端\n",
    "{\n",
    "    var services = new ServiceCollection();\n",
    "    services\n",
    "        .AddHttpClient<HttpClientServiceA>()\n",
    "        .ConfigureHttpClient(client=>\n",
    "        {\n",
    "            client.BaseAddress = new Uri(webApiBaseUrl);\n",
    "        });\n",
    "\n",
    "    var builder = services.BuildServiceProvider();\n",
    "    var serverA = builder.GetRequiredService<HttpClientServiceA>();\n",
    "\n",
    "    var dataA = await serverA.GetAsync();\n",
    "    Console.WriteLine(dataA);\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "+ 与工厂和Polly库统合,统一处理(下面专项介绍)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "+ 添加异常Pipeline管道(下面专项介绍)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 在管道中,添加异常中间件"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "不管那种方式创建的客户端,都可以配置Pipeline,专门添加异常管道中间件进行异常统一处理,也很推荐这种方式。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "+ 准备异常处理中间件"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "//异常中间件(管道)类\n",
    "public class ExceptionDelegatingHandler : DelegatingHandler\n",
    "{\n",
    "    protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)\n",
    "    {\n",
    "        Console.WriteLine(\"ExceptionDelegatingHandler -> Send -> Before\");\n",
    "\n",
    "        HttpResponseMessage response;\n",
    "        try \n",
    "        {\n",
    "            response = base.Send(request, cancellationToken);\n",
    "            response.EnsureSuccessStatusCode();\n",
    "        }\n",
    "        catch(Exception ex)\n",
    "        {\n",
    "            Console.WriteLine($\"异常管道中间件,监控到异常:{ex.Message}\");\n",
    "\n",
    "            response = new HttpResponseMessage(HttpStatusCode.ExpectationFailed);\n",
    "        }\n",
    "        finally\n",
    "        {\n",
    "            //清理业务\n",
    "        }\n",
    "        \n",
    "        Console.WriteLine(\"ExceptionDelegatingHandler -> Send -> After\");\n",
    "\n",
    "        return response;\n",
    "    }\n",
    "\n",
    "    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n",
    "    {\n",
    "        Console.WriteLine(\"ExceptionDelegatingHandler -> SendAsync -> Before\");\n",
    "\n",
    "        HttpResponseMessage response;\n",
    "        try \n",
    "        {\n",
    "            response = response = await base.SendAsync(request, cancellationToken);\n",
    "            response.EnsureSuccessStatusCode();\n",
    "        }\n",
    "        catch(Exception ex)\n",
    "        {\n",
    "            Console.WriteLine($\"异常管道中间件,监控到异常:{ex.Message}\");\n",
    "\n",
    "            response = new HttpResponseMessage(HttpStatusCode.ExpectationFailed);\n",
    "        }\n",
    "        finally\n",
    "        {\n",
    "            //清理业务\n",
    "        }\n",
    "\n",
    "        Console.WriteLine(\"ExceptionDelegatingHandler -> SendAsync -> After\");\n",
    "\n",
    "        return response;\n",
    "    }\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "+ 直接创建客户端时,加入异常管道"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "ExceptionDelegatingHandler -> SendAsync -> Before\n",
      "异常管道中间件,监控到异常:Response status code does not indicate success: 500 (Internal Server Error).\n",
      "ExceptionDelegatingHandler -> SendAsync -> After\n",
      "远程请求失败,响应码 ExpectationFailed\n"
     ]
    }
   ],
   "source": [
    "{\n",
    "\n",
    "    //构建管道(加入异常中间件)\n",
    "    var handler = new ExceptionDelegatingHandler()\n",
    "    {\n",
    "        //相当于下一个中间件(管道)\n",
    "        //最后中间件必须是SocketsHttpHandler\n",
    "        InnerHandler = new SocketsHttpHandler() \n",
    "        {\n",
    "            AllowAutoRedirect = true\n",
    "        }\n",
    "    };\n",
    "\n",
    "    //构造中传入管道对象\n",
    "    using(var client = new HttpClient(handler){BaseAddress = new Uri(webApiBaseUrl)})\n",
    "    {\n",
    "        //发送请求\n",
    "        var response = await client.GetAsync(\"/api/ErrorDemo/Error500\");\n",
    "\n",
    "        if(response.IsSuccessStatusCode)\n",
    "        {\n",
    "            Console.WriteLine($\"远程请求成功,响应码 {response.StatusCode}\");\n",
    "        }\n",
    "        else\n",
    "        {\n",
    "            Console.WriteLine($\"远程请求失败,响应码 {response.StatusCode}\");\n",
    "        }\n",
    "    }\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "+ 静态或工具类,加入异常管道"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "配置文件根目录:c:\\Users\\ruyu\\Desktop\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.Core\n",
      "ExceptionDelegatingHandler -> SendAsync -> Before\n",
      "ExceptionDelegatingHandler -> SendAsync -> After\n",
      "正常请求结果:{\"data\":{\"host\":\"localhost\",\"port\":5189,\"scheme\":\"http\",\"pathBase\":\"\",\"baseUrl\":\"http://localhost:5189\",\"webAppMutexName\":\"HttpClientStudy.WebApp\"},\"code\":1,\"message\":\"成功\"}\n",
      "ExceptionDelegatingHandler -> SendAsync -> Before\n",
      "异常管道中间件,监控到异常:Response status code does not indicate success: 500 (Internal Server Error).\n",
      "ExceptionDelegatingHandler -> SendAsync -> After\n",
      "异常请求结果:StatusCode: 417, ReasonPhrase: 'Expectation Failed', Version: 1.1, Content: System.Net.Http.EmptyContent, Headers:\n",
      "{\n",
      "  Content-Length: 0\n",
      "}\n"
     ]
    }
   ],
   "source": [
    "//静态类中处理异常\n",
    "public class HttpClientHelper2\n",
    "{\n",
    "    public readonly static HttpClient StaticClient;\n",
    "\n",
    "    static HttpClientHelper2()\n",
    "    {\n",
    "        //构建管道(加入异常中间件)\n",
    "        var handler = new ExceptionDelegatingHandler()\n",
    "        {\n",
    "            //相当于下一个中间件(管道)\n",
    "            //最后中间件必须是SocketsHttpHandler\n",
    "            InnerHandler = new SocketsHttpHandler() \n",
    "            {\n",
    "                AllowAutoRedirect = true,\n",
    "                PooledConnectionLifetime = TimeSpan.FromSeconds(30),\n",
    "            }\n",
    "        };\n",
    "\n",
    "        StaticClient = new HttpClient(handler)\n",
    "        {\n",
    "            //统一设置: 基础Uri\n",
    "            BaseAddress = new Uri(WebApiConfigManager.GetWebApiConfig().BaseUrl),\n",
    "        };\n",
    "\n",
    "        //统一设置:请求头等\n",
    "        StaticClient.DefaultRequestHeaders.Add(\"x-custom-time\",\"12ms\");\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",
    "\n",
    "        //无异常处理\n",
    "\n",
    "        return await response.Content.ReadAsStringAsync();\n",
    "    }\n",
    "\n",
    "    public static async Task<HttpResponseMessage> PostAsync(string url, HttpContent content)\n",
    "    {\n",
    "        //无异常处理\n",
    "        return await StaticClient.PostAsync(url,content);\n",
    "    }\n",
    "}\n",
    "\n",
    "{   //正常请求\n",
    "    var response = await HttpClientHelper2.GetAsync(\"/api/Config/GetApiConfig\");\n",
    "    var content = await response.Content.ReadAsStringAsync();\n",
    "    Console.WriteLine($\"正常请求结果:{content}\");\n",
    "\n",
    "    //异常请求\n",
    "    var response2 = await HttpClientHelper2.GetAsync(\"/api/ErrorDemo/Error500\");\n",
    "    Console.WriteLine($\"异常请求结果:{response2}\");\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "+ 类型化客户端,加入异常管道"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "HttpClientServiceC => 构造函数执行一次\n",
      "ExceptionDelegatingHandler -> SendAsync -> Before\n",
      "异常管道中间件,监控到异常:Response status code does not indicate success: 500 (Internal Server Error).\n",
      "ExceptionDelegatingHandler -> SendAsync -> After\n",
      "响应数据为:\n"
     ]
    }
   ],
   "source": [
    "// 类型化客户端A HttpClient\n",
    "public class HttpClientServiceC\n",
    "{\n",
    "    public HttpClient Client { get; }\n",
    "    public HttpClientServiceC(HttpClient client)\n",
    "    {\n",
    "        Client = client;\n",
    "        Console.WriteLine(\"HttpClientServiceC => 构造函数执行一次\");\n",
    "    }\n",
    "\n",
    "    //常规方法:异常可在方法内处理\n",
    "    public async Task<string> GetAsync()\n",
    "    {\n",
    "        var response = await Client.GetAsync(\"/api/ErrorDemo/Error500\");\n",
    "        var content = await response.Content.ReadAsStringAsync();\n",
    "        return content;\n",
    "    }\n",
    "}\n",
    "\n",
    "// 使用类型化客户端\n",
    "{\n",
    "    var services = new ServiceCollection();\n",
    "    services\n",
    "        //1、注入异常管道到服务\n",
    "        .AddTransient<ExceptionDelegatingHandler>()\n",
    "        .AddHttpClient<HttpClientServiceC>()\n",
    "        .ConfigureHttpClient(client=>\n",
    "        {\n",
    "            client.BaseAddress = new Uri(webApiBaseUrl);\n",
    "        })\n",
    "        //2、添加异常管道至客户端\n",
    "        .AddHttpMessageHandler<ExceptionDelegatingHandler>();\n",
    "\n",
    "    var builder = services.BuildServiceProvider();\n",
    "    var serverC = builder.GetRequiredService<HttpClientServiceC>();\n",
    "\n",
    "    var dataC = await serverC.GetAsync();\n",
    "    Console.WriteLine($\"响应数据为:{dataC}\");\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "+ 工厂,加入异常管道"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "ExceptionDelegatingHandler -> SendAsync -> Before\n",
      "ExceptionDelegatingHandler -> SendAsync -> After\n",
      "Pong\n",
      "ExceptionDelegatingHandler -> SendAsync -> Before\n",
      "异常管道中间件,监控到异常:Response status code does not indicate success: 500 (Internal Server Error).\n",
      "ExceptionDelegatingHandler -> SendAsync -> After\n",
      "响应码:ExpectationFailed\n"
     ]
    }
   ],
   "source": [
    "//使用\n",
    "{\n",
    "    var services = new ServiceCollection();\n",
    "\n",
    "    //基础配置\n",
    "    services\n",
    "        //1、注入异常管道到服务\n",
    "        .AddTransient<ExceptionDelegatingHandler>()\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<HttpClient>(string.Empty, config => \n",
    "        {\n",
    "            config.DefaultRequestHeaders.Add(\"X-Custom-Demo\", \"true\");\n",
    "        })\n",
    "\n",
    "        //配置客户端\n",
    "        .ConfigureHttpClient(client => \n",
    "        {\n",
    "            client.BaseAddress = new Uri(webApiBaseUrl);\n",
    "            client.Timeout = TimeSpan.FromSeconds(10);\n",
    "        })\n",
    "\n",
    "        //添加类型化客户端\n",
    "        //.AddTypedClient<HttpClientServiceC>()\n",
    "\n",
    "        //2、添加异常管道至客户端\n",
    "        .AddHttpMessageHandler<ExceptionDelegatingHandler>()\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<HttpResponseMessage>(11, TimeSpan.FromSeconds(30)))\n",
    "        ;\n",
    "\n",
    "    \n",
    "    var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();\n",
    "\n",
    "    //正常请求\n",
    "    var defaultClient = factory.CreateClient();\n",
    "    var defaultContent = await defaultClient.GetStringAsync(\"api/hello/ping\");\n",
    "    Console.WriteLine(defaultContent);\n",
    "\n",
    "    //异常请求\n",
    "    var clientA = factory.CreateClient();\n",
    "    var responseA = await clientA.GetAsync(\"/api/ErrorDemo/Error500\");\n",
    "    Console.WriteLine($\"响应码:{responseA.StatusCode}\");\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 工厂 + Polly库处理"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Polly库提供了多种多样的超时、异常等功能,非常推荐使用(可以配合IoC/工厂)。\n",
    "\n",
    "下面是简单例子:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "正常请求, 响应内容: Pong\n",
      "运程调用发性异常,Polly进行了回退!\n",
      "异常请求,响应码:InternalServerError\n"
     ]
    }
   ],
   "source": [
    "//polly策略\n",
    "var policy = Policy\n",
    "    .Handle<HttpRequestException>()\n",
    "    .OrResult<HttpResponseMessage>(message => message.StatusCode != System.Net.HttpStatusCode.OK)\n",
    "    //后备策略,代替异常\n",
    "    .FallbackAsync(async fallbackAction =>\n",
    "    {\n",
    "       Console.WriteLine(\"运程调用发性异常,Polly进行了回退!\");\n",
    "\n",
    "       return await Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError));\n",
    "    });\n",
    "\n",
    "//工厂中使用 Polly异常回退策略\n",
    "{\n",
    "   var services = new ServiceCollection();\n",
    "\n",
    "   //默认命名客户端\n",
    "   services\n",
    "      .AddHttpClient<HttpClient>(string.Empty)\n",
    "      .ConfigureHttpClient(client => \n",
    "      {\n",
    "         client.BaseAddress = new Uri(webApiBaseUrl);\n",
    "         client.Timeout = TimeSpan.FromSeconds(10);\n",
    "      })\n",
    "\n",
    "      //Polly策略配置\n",
    "      .AddPolicyHandler(policy);\n",
    "\n",
    "    \n",
    "    var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();\n",
    "\n",
    "    //正常请求\n",
    "    var defaultClient = factory.CreateClient();\n",
    "    var defaultContent = await defaultClient.GetStringAsync(\"api/hello/ping\");\n",
    "    Console.WriteLine($\"正常请求, 响应内容: {defaultContent}\");\n",
    "\n",
    "    //异常请求\n",
    "    var clientA = factory.CreateClient();\n",
    "    var responseA = await clientA.GetAsync(\"/api/ErrorDemo/Error500\");\n",
    "    Console.WriteLine($\"异常请求,响应码:{responseA.StatusCode}\");\n",
    "}\n"
   ]
  }
 ],
 "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
}