-
Notifications
You must be signed in to change notification settings - Fork 72
Consumption Patterns
There are several ways that HttpClientFactory
can be used and none of them are strictly superior to another, it really depends on your application and the constraints you are working under.
Here we register a generic HttpClient
with no special configuration.
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
services.AddMvc();
}
Accepting an IHttpClientFactory
and using it to create a HttpClient to use when needed.
public class MyController : Controller
{
IHttpClientFactory _httpClientFactory;
public MyController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public IActionResult Index()
{
var client = _httpClientFactory.CreateClient();
var result = client.GetStringAsync("http://myurl/");
return View();
}
}
Using HttpClientFactory
like this is a good way to start refactoring an existing application, as it has no impact on the way you use HttpClient
. You would just replace the places you create new HttpClients with a call to CreateClient
. If you have some configuration commmon to all uses of HttpClient then you can put it into the registration section instead of duplicating it throughout your codebase.
Each time you call CreateClient
you get a new instance of HttpClient
, but the factory will reuse the underlying HttpMessageHandler
when appropriate. The HttpMessageHandler
is responsible for creating and maintainging the underlying Operating System connection. Reusing the HttpMessageHandler
will save you from creating many connections on your host maching.
If you have multiple distinct uses of HttpClient, each with different configurations, then you may want to use named clients.
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); // Github API versioning
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); // Github requires a user-agent
});
services.AddHttpClient();
}
Here we call AddHttpClient
twice, once with the name 'github' and once without. The github specific client has some default configuration applied, namely the base address and two headers required to work with the GitHub API.
The configuration function here will get called every time CreateClient
is called, as a new instance of HttpClient
is created each time.
public class MyController : Controller
{
IHttpClientFactory _httpClientFactory;
public MyController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public IActionResult Index()
{
var defaultClient = _httpClientFactory.CreateClient();
var gitHubClient = _httpClientFactory.CreateClient("github");
return View();
}
}
In the above code the gitHubClient
will have the BaseAddress and headers set where as the defaultClient doesn't. This provides you the with the ability to have different configurations for different purposes. This may mean different configurations per endpoint/API, but could also mean different configuration for different purposes or any other way you choose to pivot client configuration.
HttpClientFactory
will create, and cache, a single HttpMessageHandler
per named client. Meaning that if you were to use netstat or some other tool to view connections on the host machine you would generally see a single TCP connection for each named client, rather than one per instance when you new-up and dispose of a HttpClient
manually.
NOTE: The factory will re-create a HttpMessageHandler
periodically, so you may see more connections as a new connection is created and the previous has not yet been released.
Typed Clients give you the same capabilities as named clients without strings as keys, giving you intellisense and compiler help. They also provide a single location to configure and interact with a particular HttpClient
. For example, a single typed client might be used for a single backend endpoint and encapsulate all logic dealing with that endpoint. In this example we only moved configuration into the type, but we could also have methods with behaviour and not actually expose the HttpClient
if we want all access to go through this type. There is an example of this later in this document.
Not everyone will want or be able to move configuration like this to the constructor of their Typed Client. It's shown here as an example of something that could be done, rather than a hard recommendation.
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient<GitHubService>();
services.AddMvc();
}
public class GitHubService
{
public HttpClient Client { get; private set; }
public GitHubService(HttpClient client)
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); // Github API versioning
client.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); // Github requires a user-agent
Client = client;
}
}
public class IndexModel : PageModel
{
private GitHubService _ghService;
public IndexModel(GitHubService ghService)
{
_ghService = ghService;
}
public async Task OnGet()
{
var result = await _ghService.Client.GetStringAsync("/orgs/octokit/repos");
}
}
When generating a client or proxy that uses HttpClient, such as when using a library such as Refit, you will want the HttpClientFactory
to be responsible for creating the HttpClient
that your generated code will use.
In this example we will use Refit to generate a proxy for a REST API we are going to consume. When using Refit you create an interface for the API and let Refit generate the code that actually calls the API. You can find out more about Refit here: https://github.com/paulcbetts/refit
We are taking advantage of Refits generated code being able to accept an instance of HttpClient
instead of creating its own. As long as the generated code being used in your application allows the same then you should be able to do something similar to what we are doing here without having to change your generated code to take a direct dependency on HttpClientFactory
, though that might possible as well.
public interface IGitHubApi
{
[Get("/users/{user}")]
Task<User> GetUser(string user);
}
The same GitHub registration that we used earlier.
services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); // Github API versioning
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); // Github requires a user-agent
})
.AddTypedClient(c => Refit.RestService.For<IGitHubApi>(c));
Here we take advantage of Refits ability to accept a HttpClient
and give it one that HttpClientFactory
manages.
private IGitHubApi _gh;
public HomeController(IGitHubApi gh)
{
_gh = gh;
}
public async Task<IActionResult> Index()
{
var user = await _gh.GetUser("glennc");
return View(user);
}
In this example we will build a Typed Client that encapsulates all access to my backend service, which in this case is the default Web API template in Visual Studio.
NOTE: This logic would be improved with the addition of a Circuit Breaker library, such as Polly, which will also be available with HttpClientFactory
.
public class ValuesService
{
public HttpClient Client { get; set; }
public IMemoryCache Cache { get; set; }
private ILogger<ValuesService> _logger;
public ValuesService() { }
public ValuesService(HttpClient client, IMemoryCache cache, ILogger<ValuesService> logger)
{
Client = client;
Cache = cache;
_logger = logger;
}
public virtual async Task<IEnumerable<string>> GetValues()
{
var result = await Client.GetAsync("api/values");
var resultObj = Enumerable.Empty<string>();
if (result.IsSuccessStatusCode)
{
resultObj = JsonConvert.DeserializeObject<IEnumerable<string>>(await result.Content.ReadAsStringAsync());
Cache.Set("GetValue", resultObj);
}
else
{
if (Cache.TryGetValue("GetValue", out resultObj))
{
_logger.LogWarning("Returning cached values as the values service is unavailable.");
return resultObj;
}
result.EnsureSuccessStatusCode();
}
return resultObj;
}
}
This type can then be registered and consumed with code like the following:
services.AddHttpClient<ValuesService>(client => client.BaseAddress = new Uri(Configuration["values:uri"]));
public class IndexModel : PageModel
{
private ValuesService _valuesService;
public IEnumerable<string> Values;
public IndexModel(ValuesService valuesService)
{
_valuesService = valuesService;
}
public async Task OnGet()
{
Values = await _valuesService.GetValues();
}
}
This pattern shows encapsulating common logic about accessing HTTP endpoints within a single type and makes consumption of that type easy.
If we take the ValuesService
and IndexModel
that we showed in the previous example, then we can write tests like the following using Xunit and Moq though the same should work with any testing framework and mocking library (or your own mock types if you prefer):
[Fact]
public async Task GET_populates_values()
{
IEnumerable<string> testValues = new List<string>() { "value1", "value2", "value3" };
var valueService = new Mock<ValuesService>();
valueService.Setup(x => x.GetValues()).Returns(Task.FromResult(testValues));
var indexUnderTest = new IndexModel(valueService.Object);
await indexUnderTest.OnGet();
Assert.Equal(testValues, indexUnderTest.Values);
}
The test for the service itself are more complicated as we are mocking the HttpClient itself.
//TODO: Finish cleaning up this example and add to the document.