为什么 .NetCore HttpClient 在我的单元测试中的第二次重试中被处置?

Why is .NetCore HttpClient disposed in second retry in my Unit Test?

我想在我的 UT 中测试我的 httpclient post 重试功能,这里我模拟了 HttpFactory:

both the first and second time, the HttpFactory always returns HttpStatusCode.InternalServerError

public class MyServiceClient
{
    private readonly IHttpClientFactory _clientFactory;

    public MyServiceClient(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task<string> GetResponse(string test= "te")
    {
        using var client = _clientFactory.CreateClient("MyClient");
        var content = new StringContent("{}", Encoding.UTF8, "application/json");
        var response = await client.PostAsync("http://www.contoso.com/",content);
        if (!response.IsSuccessStatusCode)
        {
            throw new ApplicationException("Application Error!");
        }
        var result = await response.Content.ReadAsStringAsync();
        return result;
    }

    public async Task<string> PollyExecute()
    {
        try
        {
            var policy = Policy
                .Handle<Exception>()
                .WaitAndRetryAsync(3,
                    count => TimeSpan.FromSeconds(2),
                    (ex, timeSpan,retrycount, context) =>
                    {
                        Console.WriteLine(ex);
                    });

            var response = await policy.ExecuteAsync(()=>GetResponse());

            return response;
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            throw ;
        }

    }

}

然后我将我的策略用于 运行 客户端 post 异步方法,在我第一次重试时没有问题,我得到了例外的 500 内部服务器错误。

public class HttpClientTest
{
    [Fact]
    public async Task PoliceTest()
    {
        var messageHandler = new StubHttpMessageHandler(HttpStatusCode.InternalServerError, "Error!!!!");

        var httpClient = new HttpClient(messageHandler)
        {
            BaseAddress = new Uri("http://mockuri")
        };

        var factory = Substitute.For<IHttpClientFactory>();
        factory.CreateClient(Arg.Any<string>()).Returns(httpClient, httpClient);

        var client = new MyServiceClient(factory);

        var result = await client.PollyExecute();

    }
}

public sealed class StubHttpMessageHandler : HttpMessageHandler
{
    public string _response;
    public HttpStatusCode _statusCode;

    public StubHttpMessageHandler(HttpStatusCode statusCode, string response)
    {
        _statusCode = statusCode;
        _response = response;
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        return Task.FromResult(Send(request));
    }

    private HttpResponseMessage Send(HttpRequestMessage request)
    {
        return new HttpResponseMessage
        {
            Content = new StringContent(_response),
            StatusCode = _statusCode,
        };
    }
}

但是,在第二次重试中,当运行ning post异步方法时,

它抛出一个异常,表示 httpclient 已被处理。

为什么?有朋友知道原因吗?提前致谢!

我添加了更多示例代码,并得到了初步调查:

在 Polly 的第二次,如果我删除 Using in

using var client = _clientFactory.CreateClient("MyClient");

那么也不例外。我猜应该是client的作用域导致的,但是实际上在执行PostAsync()的时候,Client还有一些值,好像没有disposed。

很奇怪。

我认为您只是在测试设置中错误地模拟了 IHttpClientFactory。按照设计默认实现工厂 return 的新客户端 (Microsoft docs):

Each call to CreateClient(String) is guaranteed to return a new HttpClient instance. Callers may cache the returned HttpClient instance indefinitely or surround its use in a using block to dispose it when desired.

在示例中,您提供了与工厂 return 相同的 httpClient,该工厂在首次使用后被处置。相同的处置对象一次又一次地被模拟工厂 return 编辑,导致处置错误。 只需将您的示例修改为 return 个客户端的不同实例(请原谅重复的代码)避免处理错误:

...
        var httpClient = new HttpClient(messageHandler)
        {
            BaseAddress = new Uri("http://mockuri")
        };

        var httpClient1 = new HttpClient(messageHandler)
        {
            BaseAddress = new Uri("http://mockuri")
        };

        var httpClient2 = new HttpClient(messageHandler)
        {
            BaseAddress = new Uri("http://mockuri")
        };

        var httpClient3 = new HttpClient(messageHandler)
        {
            BaseAddress = new Uri("http://mockuri")
        };

        var factory = Substitute.For<IHttpClientFactory>();
    
        factory
            .CreateClient(Arg.Any<string>())
            .Returns(httpClient, httpClient1, httpClient2, httpClient3);
...

您的模拟 return 每次都是相同的 httpClient 对象。 HttpClient一旦丢弃就不能重复使用。

每次设置模拟到return一个新实例:

    var factory = Substitute.For<IHttpClientFactory>();
    factory.CreateClient(Arg.Any<string>()).Returns( _ => new HttpClient(messageHandler)
           { 
              BaseAddress = new Uri("http://mockuri")
           });

首先,您需要每次都在 mock 中设置一个新的 HttpClient 实例。

var factory = Substitute.For<IHttpClientFactory>();
            
factory.CreateClient(Arg.Any<string>())
    .Returns(new HttpClient(messageHandler)
    {
        BaseAddress = new Uri("http://mockuri")
    }, new HttpClient(messageHandler)
    {
        BaseAddress = new Uri("http://mockuri")
    });

其次,如果你不想在没有using子句的情况下获取任何值,你需要在使用后手动调用Dispose方法。

var client = _clientFactory.CreateClient("MyClient");
try
{        
    var content = new StringContent("{}", Encoding.UTF8, "application/json");
    var response = await client.PostAsync("http://www.contoso.com/", content);
    if (!response.IsSuccessStatusCode)
    //... your whole logic
}
finally
{
    if (client != null) { client.Dispose(); }
}