可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
As part of an ASP.Net Core project that I am working on I have a requirement to communicate with a number of different Rest based API Endpoints from within my WebApi. To achieve this I am using a number of service classes that each instantiate a static HttpClient
. Essentially I have a service class for each of the Rest based endpoints that the WebApi connects to.
An example of how the static HttpClient
is instantiated in each of the service classes can be seen below.
private static HttpClient _client = new HttpClient()
{
BaseAddress = new Uri("http://endpointurlexample"),
};
Whilst the above is working well, it does not allow for effective unit testing of the service classes that are using HttpClient
. To enable me to carry out unit testing I have a fake HttpMessageHandler
that I would like to use for the HttpClient
in my unit tests, whilst the HttpClient
is instantiated as above however I am unable to apply the fake HttpMessageHandler
as part of my unit tests.
What is the best way for the HttpClient
in the service classes to remain a single instance throughout the application (one instance per endpoint), but to allow a different HttpMessageHandler
to be applied during the unit tests?
One approach I have thought of would be not to use a static field to hold the HttpClient
in the service classes, rather to allow it to be injected via constructor injection using a singleton lifecycle, which would allow me to specify a HttpClient
with the desired HttpMessageHandler
during unit tests, the other option I thought of would be to use a HttpClient
Factory Class that instantiated the HttpClient
s in static fields that could then be retrieved by injecting the HttpClient
factory into the service classes, again allowing a different implementation with the relevant HttpMessageHandler
to be returned in unit tests. None of the above feel particularly clean however and it feels like there must be a better way?
Any questions, let me know.
回答1:
Adding to the conversation from the comments looks like you would need a HttpClient
factory
public interface IHttpClientFactory {
HttpClient Create(string endpoint);
}
and the implementation of the core functionality could look something like this.
static IDictionary<string, HttpClient> cache = new Dictionary<string, HttpClient>();
public HttpClient Create(string endpoint) {
HttpClient client = null;
if(cache.TryGetValue(endpoint, out client)) {
return client;
}
client = new HttpClient {
BaseAddress = new Uri(endpoint),
};
cache[endpoint] = client;
return client;
}
That said, if you are not particularly happy with the above design. You could abstract away the HttpClient
dependency behind a service so that the client does not become an implementation detail.
That consumers of the service need not know exactly how the data is retrieved.
回答2:
You think to complicated. All you need is a HttpClient factory or accessor with a HttpClient
property and use it the same way the ASP.NET Core is allowing HttpContext
to be injected
public IHttpClientAccessor
{
HttpClient HttpClient { get; }
}
public DefaultHttpClientAccessor : IHttpClientAccessor
{
public HttpClient Client { get; }
public DefaultHttpClientAccessor()
{
Client = new HttpClient();
}
}
and inject this in your services
public class MyRestClient : IRestClient
{
private readonly HttpClient client;
public MyRestClient(IHttpClientAccessor httpClientAccessor)
{
client = httpClientAccessor.HttpClient;
}
}
registration in Startup.cs:
services.AddSingleton<IHttpClientAccessor, DefaultHttpClientAccessor>();
For unit-testing, just mock it
// Moq-esque
// Arrange
var httpClientAccessor = new Mock<IHttpClientAccessor>();
var httpHandler = new HttpMessageHandler(..) { ... };
var httpContext = new HttpContext(httpHandler);
httpClientAccessor.SetupGet(a => a.HttpClient).Returns(httpContext);
// Act
var restClient = new MyRestClient(httpClientAccessor.Object);
var result = await restClient.GetSomethingAsync(...);
// Assert
...
回答3:
My current preference is to derive from HttpClient
once per target endpoint domain and make it a singleton using dependency injection rather than use HttpClient
directly.
Say I am making HTTP requests to example.com, I would have an ExampleHttpClient
that inherits from HttpClient
and has the same constuctor signature as HttpClient
allowing you to pass and mock the HttpMessageHandler
as normal.
public class ExampleHttpClient : HttpClient
{
public ExampleHttpClient(HttpMessageHandler handler) : base(handler)
{
BaseAddress = new Uri("http://example.com");
// set default headers here: content type, authentication, etc
}
}
I then set ExampleHttpClient
as singleton in my dependency injection registration and add a registration for HttpMessageHandler
as transient as it will be created just once per http client type. Using this pattern I do not need to have multiple complicated registrations for HttpClient
or smart factories to build them based on destination host name.
Anything that needs to talk to example.com should take a constructor dependency on ExampleHttpClient
and then they all share the same instance and you get connection pooling as designed.
This way also gives you a nicer place to put stuff like default headers, content types, authorisation, base address etc, and helps prevent http config for one service leaking to another service.
回答4:
I might be late to the party, but I've created a Helper nuget package that allows you to test HttpClient endpoints in unit tests.
NuGet: install-package WorldDomination.HttpClient.Helpers
Repo: https://github.com/PureKrome/HttpClient.Helpers
The basic idea is that you create the fake response payload and pass that the a FakeHttpMessageHandler
instance to your code, which includes that fake response payload. Then, when your code tries to actually HIT that URI endpoint, it doesn't ... and just returns the fake response instead. MAGIC!
and here's a really simple example:
[Fact]
public async Task GivenSomeValidHttpRequests_GetSomeDataAsync_ReturnsAFoo()
{
// Arrange.
// Fake response.
const string responseData = "{ \"Id\":69, \"Name\":\"Jane\" }";
var messageResponse = FakeHttpMessageHandler.GetStringHttpResponseMessage(responseData);
// Prepare our 'options' with all of the above fake stuff.
var options = new HttpMessageOptions
{
RequestUri = MyService.GetFooEndPoint,
HttpResponseMessage = messageResponse
};
// 3. Use the fake response if that url is attempted.
var messageHandler = new FakeHttpMessageHandler(options);
var myService = new MyService(messageHandler);
// Act.
// NOTE: network traffic will not leave your computer because you've faked the response, above.
var result = await myService.GetSomeFooDataAsync();
// Assert.
result.Id.ShouldBe(69); // Returned from GetSomeFooDataAsync.
result.Baa.ShouldBeNull();
options.NumberOfTimesCalled.ShouldBe(1);
}
回答5:
HttpClient is used inside:
public class CustomAuthorizationAttribute : Attribute, IAuthorizationFilter
{
private string Roles;
private static readonly HttpClient _httpClient = new HttpClient();