目录
介绍
背景
使用代码
兴趣点
介绍HttpClient类是经常使用,但也往往不能完全理解。它的行为可能受到DelegationHandler实现的影响,可以通过依赖注入来使用实例,并且可以通过集成测试来测试它的工作方式。本文介绍了这些事情如何工作。
背景本文适用HttpClient至少使用过一次并希望了解更多信息的.NET Core开发人员。
使用代码首先,我们要设置HttpClient的创建和依赖注入。在ASP.NET Core应用程序中,这通常是在ConfigureServices方法中完成的。一个HttpClient实例通过依赖注入被注入到一个SearchEngineService实例。两个处理程序管理HttpClient:LogHandler和RetryHandler的行为。这是ConfigureServices实现的样子:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddTransient();
services.AddTransient();
var googleLocation = Configuration["Google"];
services.AddHttpClient(c =>
{
c.BaseAddress = new Uri(googleLocation);
}).AddHttpMessageHandler()
.AddHttpMessageHandler();
}
从上面的代码可以清楚地看到,LogHandler设置在RetryHandler之前。LogHandler是第一个处理程序,因此它处理在调用HttpClient时需要直接发生的事情。这是LogHandler实现:
public class LogHandler : DelegatingHandler
{
private readonly ILogger _logger;
public LogHandler(ILogger logger)
{
_logger = logger;
}
protected override async Task
SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
_logger.LogInformation("{response}", response);
return response;
}
}
从上面的代码可以清楚地看到,此处理程序实现仅在调用base方法之后记录来自Web请求的响应。此基本方法触发的内容由第二个处理程序设置:RetryHandler。如果服务器意外错误,此处理程序将重试。如果它直接成功或给出3次以上的服务器错误,则最后的结果计数并将返回。
public class RetryHandler : DelegatingHandler
{
protected override async Task
SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpResponseMessage result = null;
for (int i = 0; i < 3; i++)
{
result = await base.SendAsync(request, cancellationToken);
if (result.StatusCode >= HttpStatusCode.InternalServerError)
{
continue;
}
return result;
}
return result;
}
}
如前所述,需要将这些处理程序管理的HttpClient注入到SearchEngineService实例中。这个类只有一个方法。该方法调用HttpClient实例,并返回内容的长度作为响应。
public class SearchEngineService : ISearchEngineService
{
private readonly HttpClient _httpClient;
public SearchEngineService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task GetNumberOfCharactersFromSearchQuery(string toSearchFor)
{
var result = await _httpClient.GetAsync($"/search?q={toSearchFor}");
var content = await result.Content.ReadAsStringAsync();
return content.Length;
}
}
SearchEngineService是控制器类的依赖,这个控制器类有一个get方法,它将方法调用的结果作为ActionResult返回。这是控制器类。
[Route("api/[controller]")]
[ApiController]
public class SearchEngineController : ControllerBase
{
private readonly ISearchEngineService _searchEngineService;
public SearchEngineController(ISearchEngineService searchEngineService)
{
_searchEngineService = searchEngineService;
}
[HttpGet("{queryEntry}", Name = "GetNumberOfCharacters")]
public async Task GetNumberOfCharacters(string queryEntry)
{
var numberOfCharacters =
await _searchEngineService.GetNumberOfCharactersFromSearchQuery(queryEntry);
return Ok(numberOfCharacters);
}
}
要编写一个集成测试此控制器,我们使用IntegrationFixture(NuGet包这里,文档在这里,文章有一些类似的代码在这里)。外部依赖关系已由模拟服务器代替,该模拟服务器在第一个请求之后返回内部服务器错误,而在第二个请求之后成功。对我们的控制器方法的调用已完成。这会触发对SearchEngineService的调用,从而调用HttpClient。如前所述,此类调用触发对LogHandler的调用,此后触发对RetryHandler的调用。由于第一个调用给出服务器错误,因此重试完成。RetryHandler不触发LogHandler(反之亦然)。因此,我们的应用程序仅记录一个响应,而实际上有两个响应(一个失败和一个成功)。这是我们的集成测试的代码:
[Fact]
public async Task TestDelegate()
{
// arrange
await using (var fixture = new Fixture())
{
using (var searchEngineServer = fixture.FreezeServer("Google"))
{
SetupUnStableServer(searchEngineServer, "Response");
var controller = fixture.Create();
// act
var response = await controller.GetNumberOfCharacters("Hoi");
// assert, external
var externalResponseMessages =
searchEngineServer.LogEntries.Select(l => l.ResponseMessage).ToList();
Assert.Equal(2, externalResponseMessages.Count);
Assert.Equal((int)HttpStatusCode.InternalServerError,
externalResponseMessages.First().StatusCode);
Assert.Equal((int)HttpStatusCode.OK, externalResponseMessages.Last().StatusCode);
// assert, internal
var loggedResponse =
fixture.LogSource.GetLoggedObjects().ToList();
Assert.Single(loggedResponse);
var externalResponseContent =
await loggedResponse.Single().Value.Content.ReadAsStringAsync();
Assert.Equal("Response", externalResponseContent);
Assert.Equal(HttpStatusCode.OK, loggedResponse.Single().Value.StatusCode);
Assert.Equal(8, ((OkObjectResult)response.Result).Value);
}
}
}
private void SetupUnStableServer(FluentMockServer fluentMockServer, string response)
{
fluentMockServer.Given(Request.Create().UsingGet())
.InScenario("UnstableServer")
.WillSetStateTo("FIRSTCALLDONE")
.RespondWith(Response.Create().WithBody(response, encoding: Encoding.UTF8)
.WithStatusCode(HttpStatusCode.InternalServerError));
fluentMockServer.Given(Request.Create().UsingGet())
.InScenario("UnstableServer")
.WhenStateIs("FIRSTCALLDONE")
.RespondWith(Response.Create().WithBody(response, encoding: Encoding.UTF8)
.WithStatusCode(HttpStatusCode.OK));
}
如果查看上面显示的代码,则会看到两个断言部分。在第一个断言部分中,我们验证外部(模拟)服务器的日志。由于第一个Web请求失败。我们希望执行第二个Web请求(带有第二个响应),因此应该有两个响应,这正是我们验证的结果。
在第二个断言部分中,我们验证应用程序本身的日志。如前所述,仅记录了一个响应,因此我们在第二个断言部分对此进行了验证。
如果您想进一步熟悉,建议在本文中显示的GitHub上下载源代码。例如,您可以更改处理程序的顺序或添加新的处理程序,然后看看会发生什么。通过使用IntegrationFixture进行测试,您可以轻松地验证我们自己的应用程序和外部(模拟)服务器的日志。
兴趣点在撰写本文和示例代码时,我对HttpClient实际的工作方式有了更好的了解。通过使用处理程序,您不仅可以执行Web请求,还可以做更多的事情。您可以在进行Web请求时构建日志记录,重试机制或其他所需的内容。