.Net HttpClient 管理客户端(初始化与生命周期管理)

发布于:2025-05-14 ⋅ 阅读:(14) ⋅ 点赞:(0)

HttpClient 初始化与生命周期管理

HttpClient 旨在实例化一次,并在应用程序的整个生命周期内重复使用。

为实现复用,HttpClient类库默认使用连接池和请求管道,可以手动管理(连接池、配置管道、使用Polly); 结合IoC容器、工厂模式(提供了IHttpClientFactory类库)、复原库Polly,可以更加方便、完善的使用,这也是推荐的方法。

0、初始化与全局设置

//初始化:必须先执行一次
#!import ./ini.ipynb

1、手动管理:直接实例化-强烈不推荐

下面这种每次使用都实例化的用法是最常见、最不推荐的

因为HttpClient刚推出时不成熟及微软官方文档的示例代码是这种用法,再加上这种是最简单方便的使用方法,就造成很多人使用这种用法。
这种方法有如下缺点:

  1. 每次使用都实例化,造成性能开销大、容易内存泄露;
  2. 并发量大、请求频繁时:网络端口会被耗尽 Using包HttpClient,也只是在应用进程中释放了HttpClient实例,但http请求/响应是跨操作系统和网络的,而系统及网络问题在进程之上,不是进程所能处理的。

优点:

  1. 使用简单,好学易用;
  2. 并发量小且请求不频繁时,问题不大;
/*
    每次请求都实例化:并发量大、请求频繁进会耗尽套接字端口
*/
{ 
    var baseUrl = WebApiConfigManager.GetWebApiConfig().BaseUrl;

    using(var client = new HttpClient())
    {
        //发送请求
        var response = await client.GetAsync(baseUrl);
        response.EnsureSuccessStatusCode();
    }

    //显示句柄
    var displayValue = display($"第 1 次请求,成功!");

    for(int i=0;i<10;i++)
    {
        using(var client = new HttpClient())
        {
            var response = await client.GetAsync(baseUrl);
            response.EnsureSuccessStatusCode();
            displayValue.Update($"第 {i+1} 次/ 共 10 次请求,成功!");
        }
    }
}

2、手动管理:静态类或单例

相比于直接new,实现了HttpClient的重用,不推荐的

缺点:

  1. 不够灵活、优雅:特别是有多个系列的请求时;

优点:

  1. 复用 HttpClient
  2. 实现了HttpClient的重用,减少创建和销毁的开销
/*
    静态类/属性
*/

public class HttpClientHelper
{
    public readonly static HttpClient StaticClient;

    static HttpClientHelper()
    {
        SocketsHttpHandler handler = new SocketsHttpHandler()
        {
            PooledConnectionLifetime = TimeSpan.FromSeconds(30),
        };

        StaticClient = new HttpClient(handler);

        //统一设置:请求头等

        //统一错误处理

        //当然这里也可以设置Pipline,不过这里就不演示了
    } 

    public static async Task<HttpResponseMessage> GetAsync(string url)
    {
        return await StaticClient.GetAsync(url);
    }

    public static async Task<string> GetStringAsync(string url)
    {
        var response = await StaticClient.GetAsync(url);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }

    public static async Task<HttpResponseMessage> PostAsync(string url, HttpContent content)
    {
        return await StaticClient.PostAsync(url, content);
    }
}

{   //调用静态类
    var baseUrl = WebApiConfigManager.GetWebApiConfig().BaseUrl;
    var response = await HttpClientHelper.GetAsync(baseUrl+"/api/Config/GetApiConfig");
    var content = await response.Content.ReadAsStringAsync();
    Console.WriteLine(content);

    var response2 = await HttpClientHelper.GetStringAsync(baseUrl+"/api/Normal/GetAllAccounts");
    Console.WriteLine(response2);
}
/*
   单例实现1:
   1. 私有构造函数,防止外部实例化
   2. 使用静态只读变量存储类的实例,由.Net框架保证实例不变且线程安全
   3. 密封类,拒绝继承,保证不被子类破坏
*/

// 使用Lazy<T>实现单例
public sealed class HttpClientSingleton 
{
    // 私有静态变量,用于存储类的实例
    private  static  readonly  HttpClientSingleton  instance  =  new  HttpClientSingleton();
    
    //公共静态属性,用于获取类的实例
    public  static  HttpClientSingleton  Instance
    {
        get
        {
            return  instance;
        }
    }

    private  readonly  HttpClient  Client;

    //私有构造函数,防止外部实例化
    private HttpClientSingleton() 
    {
        SocketsHttpHandler handler = new SocketsHttpHandler()
        {
            PooledConnectionLifetime = TimeSpan.FromSeconds(30),
        };

        Client = new HttpClient(handler);

        //统一设置:请求头等

        //统一错误处理

        //可以使用IoC容器来管理

        //当然这里也可以设置Pipline,不过这里就不演示了
        Console.WriteLine("HttpClientSingleton 初始化一次");
    }

    public async Task<HttpResponseMessage> GetAsync(string url)
    {
        return await Client.GetAsync(url);
    }

    public async Task<string> GetStringAsync(string url)
    {
        var response = await Client.GetAsync(url);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }
}

{ //调用示例

    var baseUrl = WebApiConfigManager.GetWebApiConfig().BaseUrl;
    var response = await HttpClientSingleton.Instance.GetAsync(baseUrl+"/api/Config/GetApiConfig");
    var content = await response.Content.ReadAsStringAsync();
    Console.WriteLine(content);

    var response2 = await HttpClientSingleton.Instance.GetStringAsync(baseUrl+"/api/Normal/GetAllAccounts");
    Console.WriteLine(response2);
}
/*
   单例实现2:
   1. 私有构造函数,防止外部实例化
   2. 使用Lazy<T>, 延迟实例化, 由.Net 框架保证线程安全
   3. 密封类,拒绝继承,保证不被子类破坏
*/

// 由于静态初始化器是由 .NET  运行时在后台处理的,因此它是线程安全的,不需要额外的锁定机制。
public sealed class HttpClientSingleton2
{
    private  static  readonly  Lazy<HttpClient>  _httpClientLazy  =  new  Lazy<HttpClient>(()  =>
    {
         SocketsHttpHandler handler = new SocketsHttpHandler()
        {
            PooledConnectionLifetime = TimeSpan.FromSeconds(30)
        };

        var  client  =  new  HttpClient(handler)
        {
            //  可以在这里配置HttpClient的实例,例如设置超时时间、基地址等
            //Timeout  =  TimeSpan.FromSeconds(30),
            //BaseAddress  =  new  Uri("https://api.example.com/"),
        };


        //统一设置:请求头等

        //统一错误处理

        //可以使用IoC容器来管理

        //当然这里也可以设置Pipline,不过这里就不演示了
        Console.WriteLine("HttpClientSingleton2 初始化一次");

        return  client;
    });

    public  static  HttpClient  Instance  =>  _httpClientLazy.Value;

    //  私有构造函数,防止外部实例化
    private  HttpClientSingleton2()  {  }  

    public async Task<HttpResponseMessage> GetAsync(string url)
    {
        return await _httpClientLazy.Value.GetAsync(url);
    }

    public async Task<string> GetStringAsync(string url)
    {
        var response = await _httpClientLazy.Value.GetAsync(url);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }
}

{ //调用示例

    var baseUrl = WebApiConfigManager.GetWebApiConfig().BaseUrl;
    var response = await HttpClientSingleton2.Instance.GetAsync(baseUrl+"/api/Config/GetApiConfig");
    var content = await response.Content.ReadAsStringAsync();
    Console.WriteLine(content);

    var response2 = await HttpClientSingleton2.Instance.GetStringAsync(baseUrl+"/api/Normal/GetAllAccounts");
    Console.WriteLine(response2);
}

3、手动管理:多工具类(每类请求对应一种工具类或单例类)

把不同类别的请求分成不同的工具类,业务类直接封装成工具类的方法。类似类型化的客户端。 简单使用的话,比较推荐

优点:

  1. 复用HttpClient
  2. 可以灵活的进行统一配置
  3. 不同类别不同工具类,方便定制
  4. 业务直接封装成工具类方法,调用方便、快捷

缺点:

  1. 工具类比较多,需要手动维护
  2. 工具类方法比较多且和业务直接相关,需要手动维护
// 百度服务类
public sealed class BaiduService 
{
    private readonly HttpClient _httpClient;
    public BaiduService()
    {
        //初始化httpClient
        var baseHander = new SocketsHttpHandler() 
        { 
            MaxConnectionsPerServer = 1000 
        };

        _httpClient = new HttpClient(baseHander)
        {
            Timeout = TimeSpan.FromSeconds(10),
            BaseAddress = new Uri("http://www.baidu.com"),
        };
    }

    / <summary>
    /// 获取百度首页长度
    /// </summary>
    public async Task<int> GetIndexLengthAsync(string url)
    {
        var response = await _httpClient.GetAsync(url);
        response.EnsureSuccessStatusCode();
        var result = await response.Content.ReadAsStringAsync();
        return result.Length;
    }
}
//调用示例
{
    var service = new BaiduService();
    var result = await service.GetIndexLengthAsync("/");
    Console.WriteLine(result);
}
// 本机服务类
// 百度服务类
public sealed class LocalService 
{
    private readonly HttpClient _httpClient;
    public LocalService()
    {
        //初始化httpClient
        var baseHander = new SocketsHttpHandler() 
        { 
            MaxConnectionsPerServer = 1000 
        };

        _httpClient = new HttpClient(baseHander)
        {
            Timeout = TimeSpan.FromSeconds(10),
            BaseAddress = new Uri(WebApiConfigManager.GetWebApiConfig().BaseUrl),
        };
    }

    / <summary>
    /// 获取百度首页长度
    /// </summary>
    public async Task<string> GetIndexAsync(string url)
    {
        var response = await _httpClient.GetAsync(url);
        response.EnsureSuccessStatusCode();
        var result = await response.Content.ReadAsStringAsync();
        return result;
    }
}
//调用示例
{
    var service2 = new LocalService();
    var result = await service2.GetIndexAsync("/api/Simple/GetAccount");
    Console.WriteLine(result);
}

4、手动管理:可复原(Polly)请求

#r "nuget:Polly"
#r "nuget:Microsoft.Extensions.Http.Polly"

using Polly;
using Polly.Simmy;
using Polly.Retry;
using Polly.Extensions;
{
    var pipleLine = new ResiliencePipelineBuilder()
        .AddRetry(new RetryStrategyOptions()
        {
            ShouldHandle = new PredicateBuilder().Handle<Exception>(),
            MaxRetryAttempts = 3, // Retry up to 3 times
            OnRetry = args =>
            {
                // Due to how we have defined ShouldHandle, this delegate is called only if an exception occurred.
                // Note the ! sign (null-forgiving operator) at the end of the command.
                var exception = args.Outcome.Exception!; // The Exception property is nullable
                Console.WriteLine("内部重试");
                return default;
            }
        })
        .Build();

    var BaseUrl = WebApiConfigManager.GetWebApiConfig().BaseUrl;
    HttpClient client = new HttpClient(new SocketsHttpHandler(){})
    {
        BaseAddress = new Uri(BaseUrl),
    };

    try
    {
        await pipleLine.ExecuteAsync(async (inneerToken)=>
        {
            var response = await client.GetAsync("api/Polly8/RetryException",inneerToken);
            response.EnsureSuccessStatusCode();
        });
    }
    catch(Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
    finally
    {

    }
}

5、IoC容器管理

直接注册IoC

/*
    注意:
        1、直接IoC管理:只能一个,不太方便;
        2、可使用.NET 8+ 的 KeyedService, 可以管理多个。 老版只能用服务集合,勉强能用;
        3、把HttpClient 放在多个类中,分别注册使用;不过这样,不如直接使用类型化客户端;
*/
{   // 直接使用
    var services = new ServiceCollection();
    services.AddSingleton<HttpClient>(new HttpClient()
    {
        //BaseAddress = new Uri("https://localhost:5001/"),
        Timeout = TimeSpan.FromSeconds(10),
    });

    var client = services.BuildServiceProvider().GetRequiredService<HttpClient>();

    var resp = await client.GetAsync("https://www.baidu.com");
    resp.EnsureSuccessStatusCode();

    var content = await resp.Content.ReadAsStringAsync();
    Console.WriteLine(content.Length);
}

{ // KeyService: .Net 8+ 才支持的功能
    var services = new ServiceCollection();
    services
        .AddKeyedSingleton<HttpClient>("HttpClientA",new HttpClient()
        {
            BaseAddress = new Uri("https://www.baidu.com/"),
            Timeout = TimeSpan.FromSeconds(10),
        })
        .AddKeyedSingleton<HttpClient>("HttpClientB", new HttpClient()
        {
            BaseAddress = new Uri("https://www.qq.com/"),
            Timeout = TimeSpan.FromSeconds(2),
        });

    var clientA = services.BuildServiceProvider().GetRequiredKeyedService<HttpClient>("HttpClientA");
    var responseA = await clientA.GetAsync("/");
    responseA.EnsureSuccessStatusCode();
    var contentA = await responseA.Content.ReadAsStringAsync();
    Console.WriteLine(contentA.Length);

    var clientB = services.BuildServiceProvider().GetRequiredKeyedService<HttpClient>("HttpClientB");

    var responseB = await clientB.GetAsync("/");
    responseB.EnsureSuccessStatusCode();

    var contentB= await responseB.Content.ReadAsStringAsync();
    Console.WriteLine(contentB.Length);
}

HttpClient 多服务类

// IoC 多个HttpClient服务类

public class  HttpClientServerA
{
    public static HttpClient Client = new HttpClient()
    {
        BaseAddress = new Uri("https://www.baidu.com/"),
        Timeout = TimeSpan.FromSeconds(2),
    };

    public int GetBaiduIndexLength()
    {
        var requestMessage = new HttpRequestMessage(HttpMethod.Get, "/");

        var response = Client.Send(requestMessage);

        response.EnsureSuccessStatusCode();

        var s = response.Content.ReadAsStream();
        return (int)s.Length;
    }
}

public class  HttpClientServerB
{
    public static HttpClient Client = new HttpClient()
    {
        BaseAddress = new Uri("https://www.qq.com/"),
        Timeout = TimeSpan.FromSeconds(2),
    };

    public int GetBaiduIndexLength()
    {
        var requestMessage = new HttpRequestMessage(HttpMethod.Get, "/");

        var response = Client.Send(requestMessage);

        response.EnsureSuccessStatusCode();

        var s = response.Content.ReadAsStream();
        return (int)s.Length;
    }
}

{
    var services = new ServiceCollection();
    services.AddScoped<HttpClientServerA>();
    services.AddScoped<HttpClientServerB>();

    var provider = services.BuildServiceProvider();

    var clientA = provider.GetService<HttpClientServerA>();
    var sumA = clientA.GetBaiduIndexLength();
    Console.WriteLine($"A: {sumA}");

    var clientB = provider.GetService<HttpClientServerB>();
    var sumB = clientB.GetBaiduIndexLength();
    Console.WriteLine($"A: {sumB}");
}

6、客户端工厂管理:IHttpClientFactory(需要结合IoC) 强力推荐

使用 IHttpClientFactory 创建和管理 短期HttpClient 是官方强力推荐的方式。特别是使用IoC或是 ASP.NET中后台调用其它接口的情况。

IHttpClientFactory 综合使用了 HttpClient的多种特性:HttpClient的生命周期、HttpClient的配置、HttpClient的拦截器、HttpClient的缓存、HttpClient的依赖注入、Polly等等。

默认客户端

从使用推测,设计 IHttpClientFactory 时,重点应该是使用 “命名客户端” 或 “类型化客户端” 而不是默认客户端。

只有 AddHttpClient() 扩展方法返回 IServiceCollection;其它相关扩展方法( AddHttpClient())均返回 IHttpClientBuilder,明显针对命名客户端。
AddHttpClient() 相当于注册了基本框架;而命名客户端中,名称为空(""或string.Empty)的,相当于默认客户端。

有一个 名为 ConfigureHttpClientDefaults 的 ServiceCollection 对象的扩展方法,用于配置所有HttpClient实例,并且只在初始化时执行一次。如果只使用一个默认客户端的话,可以使用 ConfigureHttpClientDefaults 和 AddHttpClient() 配合使用,也能达到默认客户端的配置效果。

//方式1:默认客户端
{   
    var services = new ServiceCollection();
    /*
        AddHttpClient() 返回 ServiceCollection,可以继续添加其他客户端。
        其它方法则返回IHttpClientBuilder,后结配置的扩展方法,只能针对当前前端那个命名命令端。
    */
    services.AddHttpClient();

    var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();

    var client = factory.CreateClient();
    //或者
    var client2 = factory.CreateClient("");
    //或者  内部都是使用CreateClient(string.Empty),表示默认客户端。
    var client3 = factory.CreateClient(string.Empty);

    var response = await client.GetAsync(webApiBaseUrl + "/api/hello/index");
    response.EnsureSuccessStatusCode();
    var data = await response.Content.ReadAsStringAsync();
    data.Display();
}

//方式2:默认客户端 + 默认配置
{   
    var services = new ServiceCollection();

    //默认客户端
    services.AddHttpClient();

    //配置所有客户端
    services.ConfigureHttpClientDefaults(builder => 
    {
        //配置构建器
        //builder.AddDefaultLogger();

        //配置客户端
        builder.ConfigureHttpClient(c=>
        {
            c.BaseAddress = new Uri(webApiBaseUrl);
        });
    });

    var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();
    var client = factory.CreateClient();
    var response = await client.GetAsync("/api/hello/ping");
    response.EnsureSuccessStatusCode();
    var data = await response.Content.ReadAsStringAsync();
    data.Display();
}

//方式3(推荐):默认客户端:直接使用名称为 string.empty 的命名客户端
{
    var services = new ServiceCollection();

    //默认客户端
    services
        .AddHttpClient<HttpClient>(string.Empty)
        //这样后续的配置,都是针对 string.empty 的客户端,可以使用全部配置功能
        .ConfigureHttpClient(c=>c.BaseAddress = new Uri(webApiBaseUrl))
        .AddDefaultLogger();

    var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();
    var client = factory.CreateClient();
    var response = await client.GetAsync("/api/hello/ping");
    response.EnsureSuccessStatusCode();
    var data = await response.Content.ReadAsStringAsync();
    data.Display();
}

//错误用法
{
    var services = new ServiceCollection();

    //默认客户端
    services
        //没有参数时,导致后面配置不起使用;
        //参数必须为 空字符串或string.Empty,后续的配置才能起使用

        .AddHttpClient<HttpClient>()
        //没有参数时,导致后面配置不起使用
        .ConfigureHttpClient(c=>c.BaseAddress = new Uri(webApiBaseUrl))
        .AddDefaultLogger();

    var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();
    var client = factory.CreateClient();

    try
    {
        var response = await client.GetAsync("/api/hello/ping");
        response.EnsureSuccessStatusCode();
        var data = await response.Content.ReadAsStringAsync();
        data.Display();
    }
    catch(InvalidOperationException ex)
    {
        Console.WriteLine($"没有参数的配置:AddHttpClient<HttpClient>(),因后续配置中,赋值 BaseAddress 不起使用,出现异常:{Environment.NewLine}{ex.Message}");
    }
    catch(Exception ex)
    {

        Console.WriteLine(ex.Message);
    }
    finally
    {
        client.Dispose();
    }
}

默认全局配置

ConfigureHttpClientDefaults 扩展方法,添加一个委托,用于配置所有HttpClient实例。只执行一次。

//全局配置:所有HttpClient配置
{
    var services = new ServiceCollection();
    //添加一个委托,用于配置所有HttpClient实例。
    //只执行一次,而非每次CreateClient,都会执行一次。
    services.ConfigureHttpClientDefaults(builder => 
    {
        //builder.UseSocketsHttpHandler();
        //builder.SetHandlerLifetime(TimeSpan.FromMinutes(5));
        
        builder.ConfigureHttpClient(hc =>
        {
            hc.BaseAddress = new Uri(webApiBaseUrl);
        });

        Console.WriteLine("ConfigureHttpClientDefaults 只执行一次!");
    });

    //配置命名客户端
    services
        .AddHttpClient<HttpClient>("client_a")
        .ConfigureHttpClient(hc => 
        {
            hc.DefaultRequestHeaders.Add("client_a", "client_a");

            //可以覆盖默认配置
            //hc.BaseAddress = new Uri("http://www.qq.com");

            Console.WriteLine("ConfigureHttpClient 每次 CreateClient 执行一次!");
        });

    
    var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();

    //默认客户端
    var defaultClient = factory.CreateClient();
    var defaultResponse = await defaultClient.GetAsync("/api/hello/ping");
    var defaultData = await defaultResponse.Content.ReadAsStringAsync();
    Console.WriteLine(defaultData);

    //命名客户端
    var namedClient = factory.CreateClient("client_a");
    var namedResponse = await namedClient.GetAsync("/api/hello/get");
    var namedData = await namedResponse.Content.ReadAsStringAsync();
    Console.WriteLine(namedData);

    _ = factory.CreateClient("client_a");
}

命名客户端(推荐用法)

命名客户端,应该是官方推荐的方法。名称为空字符串或string.Empty时,可以为是默认命名客户端,factory.CreateClient()创建的就是这个默认客户端(或者factory.CreateClient(“”))。

//命名客户端
{
    var clientA ="httpClientA";
    var clientB ="httpClientB";

    var services = new ServiceCollection();

    services.AddHttpClient<HttpClient>(string.Empty, (provider, client) => 
    {
        client.BaseAddress = new Uri(webApiBaseUrl);
    });

    services.AddHttpClient<HttpClient>(clientA, (provider, client) => 
    {
        client.BaseAddress = new Uri(webApiBaseUrl);
    });

    services.AddHttpClient<HttpClient>(clientB, (provider, client) =>   
    {
        client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
        client.BaseAddress = new Uri(webApiBaseUrl);
    })
    .ConfigureHttpClient(client=>
    {
        client.Timeout = TimeSpan.FromSeconds(1);
        client.DefaultRequestVersion = new Version(1, 1);
    });

    var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();

    //name=string.Empty
    var defaultClient = factory.CreateClient();
    var defaultResponse = await defaultClient.GetAsync("/api/hello/ping");
    var defaultData = await defaultResponse.Content.ReadAsStringAsync();
    Console.WriteLine(defaultData);

    //name=clientA
    var httpClient_a = factory.CreateClient(clientA);
    var responseA = await httpClient_a.GetAsync("/api/hello/ping");
    var dataA     = await responseA.Content.ReadAsStringAsync();
    dataA.Display();

    //name=clientB
    var httpClient_B = factory.CreateClient(clientB);
    var responseB = await httpClient_B.GetAsync("/api/hello/ping");
    var dataB     = await responseB.Content.ReadAsStringAsync();
    dataB.Display();
}

类型化客户端 (推荐)

类型化的客户端,两种基本使用方式:
1、可以单独使用(直接IoC容器)
2、与IFactoryHttpClient配合使用(依赖注入),目的是:从统一的工厂配置中获取客户端,作为 HttpClient 类型的实参,传给类型化客户端的构造函数。
换名话说:从工厂获取HttpClient实例,设置为 类型化客户端类的 HttpClient,在其内部使用。

// 类型化客户端 HttpClient
public class HttpClientServiceA
{
    public HttpClient Client { get; }
    public HttpClientServiceA(HttpClient client)
    {
        Client = client;
        Console.WriteLine("HttpClientServiceA => 构造函数执行一次");
    }

    public async Task<string> GetIndexAsync()
    {
        var response = await Client.GetAsync("/api/hello/index");
        var content = await response.Content.ReadAsStringAsync();
        return content;
    }
}

public class HttpClientServiceB
{
    public HttpClient Client { get; }
    public HttpClientServiceB(HttpClient client)
    {
        Client = client;
        Console.WriteLine("HttpClientServiceB => 构造函数执行一次");
    }

    public async Task<string> PingAsync()
    {
        var response = await Client.GetAsync("/api/hello/Ping");
        var content = await response.Content.ReadAsStringAsync();
        return content;
    }
}

// 方式1(不推荐):类型化客户端:直接注入IoC,并从中获取实例。优点是范围可以自己选择。
{
    Console.WriteLine("方式1 -------------------------------------------------------------------");
    var services = new ServiceCollection();
    services.AddSingleton<HttpClientServiceA>(b => 
    { 
        return new HttpClientServiceA(new HttpClient(){BaseAddress = new Uri(webApiBaseUrl)});
    });
    services.AddScoped<HttpClientServiceB>(b=> 
    {
        return new HttpClientServiceB(new HttpClient(){BaseAddress = new Uri(webApiBaseUrl)});
    });

    var builder = services.BuildServiceProvider();
    var serverA = builder.GetRequiredService<HttpClientServiceA>();
    var serverB = builder.GetRequiredService<HttpClientServiceB>();

    var dataA = await serverA.GetIndexAsync();
    Console.WriteLine(dataA);

    var dataB = await serverB.PingAsync();
    Console.WriteLine(dataB);

    Console.WriteLine("========================================================================");
}

// 方式2:类型化客户端:AddHttpClient<>() 设置
{
    Console.WriteLine("方式2 -------------------------------------------------------------------");
    var services = new ServiceCollection();
    services
        .AddHttpClient<HttpClientServiceA>()
        .ConfigureHttpClient(client=>
        {
            client.BaseAddress = new Uri(webApiBaseUrl);
        });

    services
        .AddHttpClient<HttpClientServiceB>()
        .ConfigureHttpClient(client=>
        {
            client.BaseAddress = new Uri(webApiBaseUrl);
        });

    var builder = services.BuildServiceProvider();
    var serverA = builder.GetRequiredService<HttpClientServiceA>();
    var serverB = builder.GetRequiredService<HttpClientServiceB>();

    var dataA = await serverA.GetIndexAsync();
    Console.WriteLine(dataA);

    var dataB = await serverB.PingAsync();
    Console.WriteLine(dataB);

    Console.WriteLine("========================================================================");
}

// 方式3:类型化客户端:结合工厂,由工厂从统一配置中提供类型化客户端中使用的HttpClient实例。
{
    Console.WriteLine("方式3 -------------------------------------------------------------------");
    var services = new ServiceCollection();
    services.AddHttpClient<HttpClientServiceA>(client => 
    {
        client.BaseAddress = new Uri(webApiBaseUrl);
        Console.WriteLine("HttpClientServiceA => AddHttpClient 执行一次");
    })
    .AddTypedClient<HttpClientServiceA>()
    .ConfigureHttpClient(client=>
    {
        client.Timeout = TimeSpan.FromSeconds(1);
        Console.WriteLine("HttpClientServiceA => ConfigureHttpClient 执行一次");
    });

    services.AddHttpClient<HttpClientServiceB>(client => 
    {
        client.BaseAddress = new Uri(webApiBaseUrl);
        Console.WriteLine("HttpClientServiceB => AddHttpClient 执行一次");
    })
    .AddTypedClient<HttpClientServiceB>()
    .ConfigureHttpClient(client=>
    {
        client.Timeout = TimeSpan.FromSeconds(2);
        Console.WriteLine("HttpClientServiceB => ConfigureHttpClient 执行一次");
    });

    var builder = services.BuildServiceProvider();

    var serviceA = builder.GetRequiredService<HttpClientServiceA>();
    var serviceB = builder.GetRequiredService<HttpClientServiceB>();
    //每获取一次类型化客户端,都会执行一交。
    var serviceB2 = builder.GetRequiredService<HttpClientServiceB>();

    var dataA = await serviceA.GetIndexAsync();
    Console.WriteLine(dataA);

    var dataB = await serviceB.PingAsync();
    Console.WriteLine(dataB);

    var dataB2 = await serviceB2.PingAsync();
    Console.WriteLine(dataB2);

    Console.WriteLine("========================================================================");
}

管道配置

//管道配置

//日志中间件(管道类)
public class LoggerDelegatingHandler : DelegatingHandler
{
    protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        Console.WriteLine("LoggerDelegatingHandler -> Send -> Before");

        HttpResponseMessage response = base.Send(request, cancellationToken);

        Console.WriteLine("LoggerDelegatingHandler -> Send -> After");

        return response;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        Console.WriteLine("LoggerDelegatingHandler -> SendAsync -> Before");

        HttpResponseMessage response = await base.SendAsync(request, cancellationToken);

        Console.WriteLine("LoggerDelegatingHandler -> SendAsync -> After");

        return response;
    }
}

//使用日志中间件
{
    var services = new ServiceCollection();

    //先注册
    services.AddTransient<LoggerDelegatingHandler>();

    services.AddHttpClient<HttpClient>(string.Empty).ConfigureHttpClient(client =>
    {
        client.BaseAddress = new Uri(webApiBaseUrl);
    })
    //配置SocketsHttpHandler
    .UseSocketsHttpHandler((handler,provider) =>
    {
        handler.ConnectTimeout = TimeSpan.FromSeconds(10);
        handler.MaxConnectionsPerServer = 100;
        handler.UseProxy = false;
        handler.UseCookies = true;
        handler.EnableMultipleHttp2Connections = true;
        handler.SslOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
    })
    //使用前先在AddTransient范围注册
    .AddHttpMessageHandler<LoggerDelegatingHandler>()
    ;

    var factory = services.BuildServiceProvider().GetService<IHttpClientFactory>();

    var client = factory.CreateClient();

    var response = await client.GetAsync("/api/hello/ping");
    response.EnsureSuccessStatusCode();

    var responseString = await response.Content.ReadAsStringAsync();
    Console.WriteLine(responseString);
}

日志配置

默认日志配置,需要先引用 Microsoft.Extensions.LoggingMicrosoft.Extensions.Logging.Console 包,进行通用日志配置!

//通用日志
{
    ILoggerFactory loggerFactory = LoggerFactory.Create(buider =>
    {
        buider.AddConsole();
    });

    ILogger logger = loggerFactory.CreateLogger("logger");
    logger.LogInformation("直接使用的通用日志!");
}

//IoC中使用
{
    var services = new ServiceCollection();
    services.AddLogging(config =>
    {
        config.SetMinimumLevel(LogLevel.Information);

        config.AddConsole();
        //config.AddSimpleConsole();
        //config.AddSystemdConsole();
    });

    var serviceProvider = services.BuildServiceProvider();
    var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
    var logger = loggerFactory.CreateLogger("logger");
    logger.LogInformation("IoC中使用日志!");
    logger.LogError("IoC中的错误日志!");
}
配置默认日志
//配置默认日志(必须有常规日志及级别设置,否则不起使用)
{
    var services = new ServiceCollection();

    // 1、配置通用日志
    services.AddLogging(config =>
    {
        //日志级别
        config.SetMinimumLevel(LogLevel.Trace);
        //config.SetMinimumLevel(LogLevel.Information);

        //日志载体
        config.AddConsole();
        //config.AddDebug();
        //config.AddJsonConsole();
        //config.AddSimpleConsole();
        //config.AddSystemdConsole();

    });
    services
        .ConfigureHttpClientDefaults(options =>
        {
            //2、配置通用日志
            options.AddDefaultLogger();
        })
        .AddHttpClient<HttpClient>(String.Empty,c =>
        {
            c.BaseAddress = new Uri(webApiBaseUrl);
            c.DefaultRequestHeaders.Add("Authorization", "Bearer a.b.c");
        })
        //2、或者单独配置此命名客户端日志
        .AddDefaultLogger()
        ;

    var factory = services.BuildServiceProvider().GetService<IHttpClientFactory>();
    var client = factory.CreateClient(String.Empty);
    var response = await client.GetAsync("api/hello/index");

    response.EnsureSuccessStatusCode();

    var content = await response.Content.ReadAsStringAsync();

    Console.WriteLine(content);
}
配置自定义日志

博客 可以参考

/*  添加自定义日志记录
    1、可以指定当 HttpClient 启动请求、接收响应或引发异常时记录的内容和方式。可以同时添加多个自定义记录器(控制台、ETW 记录器),或“包装”和“不包装”记录器。由于其附加性质,可能需要事先显式删除默认的“旧”日志记录。
        要添加自定义日志记录,您需要实现 IHttpClientLogger 接口,然后使用 AddLogger 将自定义记录器添加到客户端。请注意,日志记录实现不应引发任何异常,否则可能会中断请求执行
    2、请求上下文对象
        上下文对象可用于将 LogRequestStart 调用与相应的 LogRequestStop 调用相匹配,以将数据从一个调用传递到另一个调用。 Context 对象由 LogRequestStart 生成,然后传递回 LogRequestStop。这可以是属性包或保存必要数据的任何其他对象。
        如果不需要上下文对象,实现可以从 LogRequestStart 返回 null。
    3、避免从内容流中读取
        例如,如果您打算阅读和记录请求和响应内容,请注意,它可能会对最终用户体验产生不利的副作用并导致错误。例如,请求内容可能在发送之前被消耗,或者巨大的响应内容可能最终被缓冲在内存中。此外,在 .NET 7 之前,访问标头不是线程安全的,可能会导致错误和意外行为。
    4、谨慎使用异步日志记录
        我们期望同步 IHttpClientLogger 接口适用于绝大多数自定义日志记录用例。出于性能原因,建议不要在日志记录中使用异步。但是,如果严格要求日志记录中的异步访问,您可以实现异步版本 IHttpClientAsyncLogger。它派生自 IHttpClientLogger,因此可以使用相同的 AddLogger API 进行注册。
        请注意,在这种情况下,还应该实现日志记录方法的同步对应项,特别是如果该实现是面向 .NET Standard 或 .NET 5+ 的库的一部分。同步对应项是从同步 HttpClient.Send 方法调用的;即使 .NET Standard 表面不包含它们,.NET Standard 库也可以在 .NET 5+ 应用程序中使用,因此最终用户可以访问同步 HttpClient.Send 方法。
    5、包装和不包装记录仪:
        当您添加记录器时,您可以显式设置wrapHandlersPipeline参数来指定记录器是否将被包装。默认不包装。
        在将重试处理程序添加到管道的情况下(例如 Polly 或某些重试的自定义实现),包装和不包装管道之间的区别最为显着。
*/

// 创建一个简单的控制台日志类
public class SimpleConsoleLogger : 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}");
    }
}

//使用
{
    var services = new ServiceCollection();
    //1、先注册日志类
    services.AddSingleton<SimpleConsoleLogger>();

    services
        // 全局配置
        .ConfigureHttpClientDefaults(options =>
        {
        })
        // 配置到HttpClient
        .AddHttpClient<HttpClient>(String.Empty,c =>
        {
            c.BaseAddress = new Uri(webApiBaseUrl);
        })
        //可选:取消默认日志记录
        .RemoveAllLoggers()
        //2、配置到HttpClient
        .AddLogger<SimpleConsoleLogger>()
        ;

    var factory = services.BuildServiceProvider().GetService<IHttpClientFactory>();
    var client = factory.CreateClient(String.Empty);
    var response = await client.GetAsync("api/hello/index");

    response.EnsureSuccessStatusCode();

    var content = await response.Content.ReadAsStringAsync();

    Console.WriteLine($"API 影响内容:{content}");
}

// 使用上下文的日志类
public class RequestIdLogger : IHttpClientLogger
{
    private readonly ILogger _log;

    public RequestIdLogger(ILogger<RequestIdLogger> log)
    {
        _log = log;
    }

    private static readonly Action<ILogger, Guid, string?, Exception?> _requestStart = LoggerMessage.Define<Guid, string?>
    (
        LogLevel.Information,
        EventIds.RequestStart,
        "Request Id={RequestId} ({Host}) started"
    );

    private static readonly Action<ILogger, Guid, double, Exception?> _requestStop = LoggerMessage.Define<Guid, double>
    (
        LogLevel.Information,
        EventIds.RequestStop,
        "Request Id={RequestId} succeeded in {elapsed}ms"
    );

    private static readonly Action<ILogger, Guid, Exception?> _requestFailed = LoggerMessage.Define<Guid>
    (
        LogLevel.Error,
        EventIds.RequestFailed,
        "Request Id={RequestId} FAILED"
    );

    public object? LogRequestStart(HttpRequestMessage request)
    {
        var ctx = new Context(Guid.NewGuid());
        _requestStart(_log, ctx.RequestId, request.RequestUri?.Host, null);
        return ctx;
    }

    public void LogRequestStop(object? ctx, HttpRequestMessage request, HttpResponseMessage response, TimeSpan elapsed)
    {
        _requestStop(_log, ((Context)ctx!).RequestId, elapsed.TotalMilliseconds, null);
    }

    public void LogRequestFailed(object? ctx, HttpRequestMessage request, HttpResponseMessage? response, Exception e, TimeSpan elapsed)
    {
        _requestFailed(_log, ((Context)ctx!).RequestId, null);
    }

    public static class EventIds
    {
        public static readonly EventId RequestStart = new(1, "RequestStart");
        public static readonly EventId RequestStop = new(2, "RequestStop");
        public static readonly EventId RequestFailed = new(3, "RequestFailed");
    }

    record Context(Guid RequestId);
}

//使用
{
    var services = new ServiceCollection();

    services.AddLogging(config =>
    {
        config.SetMinimumLevel(LogLevel.Trace);
        config.AddConsole();
    });

    //1、先注册日志类
    services.AddSingleton<RequestIdLogger>();

    services
        // 全局配置
        .ConfigureHttpClientDefaults(options =>
        {
        })
        // 配置到HttpClient
        .AddHttpClient<HttpClient>(String.Empty,c =>
        {
            c.BaseAddress = new Uri(webApiBaseUrl);
        })
        //可选:取消默认日志记录
        .RemoveAllLoggers()
        //2、配置到HttpClient
        .AddLogger<RequestIdLogger>()
        ;

    var factory = services.BuildServiceProvider().GetService<IHttpClientFactory>();
    var client = factory.CreateClient(String.Empty);
    var response = await client.GetAsync("api/hello/get");

    response.EnsureSuccessStatusCode();

    var content = await response.Content.ReadAsStringAsync();

    Console.WriteLine($"API 影响内容:{content}");
}

7 工厂 + Polly V8

IFactoryHttpClient 与 Polly配合,可轻松实现重试、熔断、降级、限流等功能,本文只是简略的给出常用的使用方法,详情会写在 Polly学习项目中。Polly 官方参考
使用步骤:

  1. 引用 Polly v8 和 Microsoft.Extensions.Http.Polly 包
  2. 配置命名客户端
  3. 使用 AddTransientHttpErrorPolicy 快捷方法,配置策略
  4. 使用其它方式配置,并且可以使用多策略、注册策略、上下文等功能

基础应用

使用快捷方法AddTransientHttpErrorPolicy,进行常用功能使用。

/*
    便捷应用:AddTransientHttpErrorPolicy() 方法,添加常用瞬时错误重试策略
*/
{
    var services = new ServiceCollection();

    services.AddHttpClient(string.Empty)
        //配置默认命名客户端
        .ConfigureHttpClient(client => 
        {
            client.BaseAddress = new Uri(webApiBaseUrl);
        })
        //设置Policy错误处理快捷扩展方法
        .AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync
        (
            new[]
            {
                TimeSpan.FromSeconds(1),
                TimeSpan.FromSeconds(2),
                TimeSpan.FromSeconds(4),
            }
        ))
        //可以多次调用:设置多个策略
        .AddTransientHttpErrorPolicy(builder => builder.RetryAsync(1));
    
    var factory = services.BuildServiceProvider().GetService<IHttpClientFactory>();
    var content = await factory.CreateClient().GetStringAsync("/api/polly8/RandomException");

    Console.WriteLine($"响应内容:{content}");
}

使用通过传统 Polly 语法配置的任何策略

使用 AddPolicyHandler 方法及其重载也可用于接受任何 IAsyncPolicy ,因此可以定义和应用任何类型的策略:可以指定要处理的内容和处理方式。

/*
    传统方式配置Polly策略
*/
//创建策略
{
    var services = new ServiceCollection();

    //重试策略
    var retryePolicy = Policy
        .Handle<HttpRequestException>()
        .OrResult<HttpResponseMessage>(response => 
        {
            return response.StatusCode == System.Net.HttpStatusCode.Created;
        })
        .WaitAndRetryAsync(new TimeSpan[]{TimeSpan.FromSeconds(1), TimeSpan.FromMilliseconds(2)});
    //调用
    services
        .AddHttpClient(string.Empty)
        .AddPolicyHandler(retryePolicy);

    //超时策略
    var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(10);
    services
        .AddHttpClient("timeoutPolicy")
        .AddPolicyHandler(timeoutPolicy);
    
    /* 普通策略转换
        所有通过 HttpClient 的调用都返回 HttpResponseMessage 因此配置的策略必须是 IAsyncPolicy<HttpResponseMessage> 
        通过简单、便捷的 AsAsyncPolicy<HttpResponseMessage>()方法,将非通用策略 IAsyncPolicy 转换为 IAsyncPolicy<HttpResponseMessage>   
    */
    var timeoutPolicy2 = Policy.TimeoutAsync(2);

    services
        .AddHttpClient("timeoutPolicy2")
        //AsAsyncPolicy转换通用策略
        .AddPolicyHandler(timeoutPolicy2.AsAsyncPolicy<HttpResponseMessage>());
}

//示例
{
    //创建策略
    var policy = Policy.RateLimitAsync<HttpResponseMessage>(3,TimeSpan.FromSeconds(10));

    //使用
    var services = new ServiceCollection();

    services.AddHttpClient(string.Empty)
        .ConfigureHttpClient(client => 
        {
            client.BaseAddress = new Uri(webApiBaseUrl);
        })
        .AddTransientHttpErrorPolicy
        (
            builder => builder.WaitAndRetryAsync
            (
                new[]
                {
                    TimeSpan.FromSeconds(1),
                    TimeSpan.FromSeconds(2),
                    TimeSpan.FromSeconds(4)
                }
            )
        )
        .AddPolicyHandler(policy);
    try
    {
        var factory = services.BuildServiceProvider().GetService<IHttpClientFactory>();
        var content = await factory.CreateClient().GetStringAsync("/api/polly8/RandomException");
        Console.WriteLine($"响应内容:{content}");
    }
    catch(Exception ex)
    {
        Console.WriteLine($"未处理的异常:{ex.Message}");
    }
}

应用多个策略

{
    var services = new ServiceCollection();

    services.AddHttpClient(string.Empty)
    .ConfigureHttpClient(client => 
    {
        client.BaseAddress = new Uri(webApiBaseUrl);
    })
    .AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync
    (
        new[]
        {
            TimeSpan.FromSeconds(1),
            TimeSpan.FromSeconds(2),
            TimeSpan.FromSeconds(3),
        }
    ))
    //断路器
    .AddTransientHttpErrorPolicy(builder => builder.CircuitBreakerAsync(
        handledEventsAllowedBeforeBreaking: 3,
        durationOfBreak: TimeSpan.FromSeconds(30)
    ));

    try
    {
        var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();
        var content = await factory.CreateClient().GetStringAsync("/api/polly8/RandomException");
        Console.WriteLine(content);
    }
    catch(Exception ex)
    {

        Console.WriteLine("API异常:"+ex.Message);
    }
}

动态选择策略

//实质是AddPolicyHandler中选择一个策略
{
    var retryPolicy = Polly.Extensions.Http.HttpPolicyExtensions
        .HandleTransientHttpError()
        .WaitAndRetryAsync(new[]
        {
            TimeSpan.FromSeconds(1),
            TimeSpan.FromSeconds(2),
            TimeSpan.FromSeconds(4)
        });
    var noOpPolicy = Policy.NoOpAsync().AsAsyncPolicy<HttpResponseMessage>();

    var services = new ServiceCollection();
    services.AddHttpClient(string.Empty, client =>
    {
        client.BaseAddress = new Uri(webApiBaseUrl);
    })
    // 根据请求方法,选择策略
    .AddPolicyHandler(request => request.Method == HttpMethod.Get ? retryPolicy : noOpPolicy);

    var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();
    var client1 = factory.CreateClient(string.Empty);
    var content1 = await client1.GetStringAsync("/api/hello/get");
    Console.WriteLine(content1);

    var client2 = factory.CreateClient(string.Empty);
    var response2 = await client2.PostAsync("/api/hello/post",null);
    var content2 = await response2.Content.ReadAsStringAsync();
    Console.WriteLine(content2);
}

从注册表中选择策略

{
    var registry = new PolicyRegistry()
    {
        { "defaultretrystrategy", HttpPolicyExtensions.HandleTransientHttpError().WaitAndRetryAsync(new TimeSpan[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3)}) },
        { "defaultcircuitbreaker", HttpPolicyExtensions.HandleTransientHttpError().CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)) },
    };

    var services = new ServiceCollection();
    services.AddPolicyRegistry(registry);

    services.AddHttpClient("a", client => { client.BaseAddress = new Uri(webApiBaseUrl); })
        .AddPolicyHandlerFromRegistry("defaultretrystrategy")
        //.AddPolicyHandlerFromRegistry("defaultcircuitbreaker")
        ;

    services.AddHttpClient("b", client => { client.BaseAddress = new Uri(webApiBaseUrl); })
        //.AddPolicyHandlerFromRegistry("defaultretrystrategy")
        .AddPolicyHandlerFromRegistry("defaultcircuitbreaker")
        ;

    var factory = services.BuildServiceProvider().GetService<IHttpClientFactory>();
    var clientA = factory.CreateClient("a");
    var clientB = factory.CreateClient("b");

    try
    {
        var resultA = await clientA.GetStringAsync("/api/polly8/exception");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
    
    var resultB = await clientB.GetStringAsync("/api/polly8/hello");
}

8、综合管理:工厂 + 类型化客户端 + 请求管道 + Polly(默认使用 连接池和IoC容器)

综合示例1

/* 综合示例1
   工厂 + 类型化客户端 + 管道 + Polly + 日志(自定义) 
*/

//类型化客户端
public class HelloApiService 
{
    public HttpClient Client { get; set; }

    public HelloApiService(HttpClient httpClient)
    {
        Client = httpClient;
    }

    public async Task<string> Ping()
    {
        var content = await Client.GetStringAsync("/api/Hello/Ping");
        return content;
    }

    public async Task<string> Index()
    {
        var content = await Client.GetStringAsync("/api/Hello/Index");
        return content;
    }

    public async Task<string> Get()
    {
        var content = await Client.GetStringAsync("/api/Hello/Get");
        return content;
    }

    public async Task<string> Post()
    {
        var response = await Client.PostAsync("/api/Hello/Post", null);
        var content = await response.Content.ReadAsStringAsync();
        return content;
    }
}

//类型化客户端
public class Polly8ApiService 
{

    public HttpClient Client { get; set; }

    public Polly8ApiService(HttpClient httpClient)
    {
        Client = httpClient;
    } 

    public async Task<string> Hello()
    {
        var content = await Client.GetStringAsync("/api/Polly8/Hello");
        return content;
    }

    public async Task<string> Exception()
    {
        var response = await Client.GetAsync("/api/Polly8/Exception");
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        return  content;
    }

    public async Task<string> RetryException()
    {
        var response = await Client.GetAsync("/api/Polly8/RetryException");
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        return  content;
    }

    public async Task<string> RandomException()
    {
        var response = await Client.GetAsync("/api/Polly8/RandomException");
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        return  content;
    }

    public async Task<string> ToggleException()
    {
        var response = await Client.GetAsync("/api/Polly8/ToggleException?toggleId="+Guid.NewGuid().ToString());
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        return  content;
    }
}

//Token管理中间件
public class TokenDelegatingHandler : DelegatingHandler 
{
    protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        Console.WriteLine("TokenDelegatingHandler -> Send -> Added Token");

        if (!request.Headers.Contains(Microsoft.Net.Http.Headers.HeaderNames.Authorization)) 
        {
            Console.WriteLine("没有 Token, TokenDelegatingHandler 添加之");
            request.Headers.Add(Microsoft.Net.Http.Headers.HeaderNames.Authorization, "Bearer " + "a.b.c");
        }
        else
        {
            Console.WriteLine($"已有Token, {request.Headers.Authorization}");
        }

        HttpResponseMessage response = base.Send(request, cancellationToken);

        Console.WriteLine("TokenDelegatingHandler -> Send -> After");

        return response;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        Console.WriteLine("TokenDelegatingHandler -> SendAsync -> Before");

        HttpResponseMessage response = await base.SendAsync(request, cancellationToken);

        Console.WriteLine("TokenDelegatingHandler -> SendAsync -> After");

        return response;
    }
}

//自定义日志
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}");
    }
}

//polly策略
var policy = Policy
    .Handle<HttpRequestException>()
    .OrResult<HttpResponseMessage>(message => message.StatusCode != System.Net.HttpStatusCode.OK)
    .WaitAndRetryAsync(new TimeSpan[]{TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2),TimeSpan.FromSeconds(4),});

//使用
{
    var services = new ServiceCollection();

    //注册基础类型
    services
        //注册日志类
        .AddTransient<CustomLogger>()
        .AddScoped<TokenDelegatingHandler>()
        ;

    //基础配置
    services
        // 基础日志配置(默认日志)
        .AddLogging(builder => 
        {
            //日志级别
            builder.SetMinimumLevel(LogLevel.Trace);

            //控制台日志
            builder.AddConsole();
        })
        //全局配置
        .ConfigureHttpClientDefaults(clientBuilder =>
        {
            clientBuilder.AddDefaultLogger();
            clientBuilder.ConfigureHttpClient(client => 
            {
                client.BaseAddress = new Uri(webApiBaseUrl);
            });
        });

        //默认命名客户端
        services.AddHttpClient<HttpClient>(string.Empty, config => 
        {
            config.DefaultRequestHeaders.Add("X-Custom-Demo", "true");
        })
        //配置客户端
        .ConfigureHttpClient(client => 
        {
            //client.BaseAddress = new Uri(webApiBaseUrl);
            client.Timeout = TimeSpan.FromSeconds(10);
        })
        //添加类型化客户端
        .AddTypedClient<HelloApiService>()
        //添加自定义管道
        .AddHttpMessageHandler<TokenDelegatingHandler>()
        //添加默认日志:全局配置已添加
        //.AddDefaultLogger()
        //添加自定义日志
        .AddLogger<CustomLogger>()
        //日志转发头(所有请求头)
        .RedactLoggedHeaders( headerName => true)

        //配置SocketsHttpHandler
        .UseSocketsHttpHandler(config =>
        {
            //配置连接池等
            config.Configure((handler,provider) => 
            {
                handler.AllowAutoRedirect = true;
                handler.PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30);
                handler.PooledConnectionLifetime = TimeSpan.FromSeconds(30);
                handler.UseProxy = false;
                handler.UseCookies = true;
            });
        })
        //设置生命周期
        .SetHandlerLifetime(TimeSpan.FromSeconds(30))
        //Polly策略配置
        .AddPolicyHandler(policy)
        //便捷配置
        .AddTransientHttpErrorPolicy(builder => builder.CircuitBreakerAsync<HttpResponseMessage>(11, TimeSpan.FromSeconds(30)))
        ;

    //自定义
    services.AddHttpClient<HttpClient>("ClientA", config => 
        {
            config.DefaultRequestHeaders.Add("X-Custom-Demo", "ClientA");
        })
        //配置客户端
        .ConfigureHttpClient(client => 
        {
            //client.BaseAddress = new Uri(webApiBaseUrl);
            client.Timeout = TimeSpan.FromSeconds(10);
        })
        //添加类型化客户端
        .AddTypedClient<Polly8ApiService>()
        //添加自定义管道
        .AddHttpMessageHandler<TokenDelegatingHandler>()
        //添加默认日志:全局配置已添加
        //.AddDefaultLogger()
        //添加自定义日志
        .AddLogger<CustomLogger>()
        //日志转发头(所有请求头)
        .RedactLoggedHeaders( headerName => true)
        //配置SocketsHttpHandler
        .UseSocketsHttpHandler(config =>
        {
            //配置连接池等
            config.Configure((handler,provider) => 
            {
                handler.AllowAutoRedirect = true;
                handler.PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30);
                handler.PooledConnectionLifetime = TimeSpan.FromSeconds(30);
                handler.UseProxy = false;
                handler.UseCookies = true;
            });
        })
        //设置生命周期
        .SetHandlerLifetime(TimeSpan.FromSeconds(30))
        //Polly策略配置
        .AddPolicyHandler(policy)
        //便捷配置
        .AddTransientHttpErrorPolicy(builder => builder.CircuitBreakerAsync<HttpResponseMessage>(11, TimeSpan.FromSeconds(30)))
        ;

    var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();

    var defaultClient = factory.CreateClient();
    var defaultContent = await defaultClient.GetStringAsync("api/hello/ping");
    Console.WriteLine(defaultContent);

    var clientA = factory.CreateClient();
    var contentA = await clientA.GetStringAsync("api/polly8/hello");
    Console.WriteLine(contentA);

    //类型化客户端
    HelloApiService helloApiService = services.BuildServiceProvider().GetRequiredService<HelloApiService>();
    Console.WriteLine(await helloApiService.Ping());
    Console.WriteLine(await helloApiService.Index());
    Console.WriteLine(await helloApiService.Get());
    Console.WriteLine(await helloApiService.Post());

    Polly8ApiService polly8ApiService = services.BuildServiceProvider().GetRequiredService<Polly8ApiService>();
    Console.WriteLine(await polly8ApiService.Hello());

}