-
Notifications
You must be signed in to change notification settings - Fork 25.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Starting work on HttpClientFactory documentation #5483
Changes from 8 commits
1b897ac
37a56fb
0523d99
ca0cc90
7026d16
ed1b3dd
bfed490
812cf28
86e239d
ec14f81
d527081
ad34a62
37a5deb
b5aa44b
135a6e1
b62b134
c71f194
a7e1c51
590150d
567afbb
5d18655
2251cb9
c8b6438
aaf26d3
c9d0daf
7825296
b077c4b
44be1a5
f95bd56
d231feb
957f760
f715142
05e139f
af9c32f
aaf3240
fdf6d7c
c7d9e6d
b2c7d60
699e07d
e4bd7f0
d7a5b8c
4c76827
8a37fc7
b34cda6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,391 @@ | ||
--- | ||
title: Initiate HTTP requests | ||
author: stevejgordon | ||
description: Learn about using the HttpClientFactory to managed logic HttpClient instances. | ||
manager: wpickett | ||
ms.author: riande | ||
ms.date: 02/15/2018 | ||
ms.prod: asp.net-core | ||
ms.technology: aspnet | ||
ms.topic: article | ||
uid: fundamentals/http-requests | ||
--- | ||
# Initiate HTTP requests | ||
|
||
By [Glenn Condron](https://github.com/glennc), [Ryan Nowak](https://github.com/rynowak) and [Steve Gordon](https://github.com/stevejgordon) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need a comma before "and" |
||
|
||
A `HttpClientFactory` can be registered and used to configure and consume `HttpClient` instances in your app. It provides several benefits: | ||
|
||
- Provides a central location for naming and configuring logical `HttpClient` instances. For example, you may configure a "github" client that is pre-configured to access Github and a default client for other purposes. | ||
- Codifies the concept of outgoing middleware via delegating handlers in HttpClient and implementing Polly-based middleware to take advantage of that. HttpClient already has the concept of delegating handlers that can be linked together for outgoing HTTP requests. The `HttpClientFactory` makes registering of these per named client more intuitive as well as implement a Polly handler that allows Polly policies to be used for Retry, CircuitBreakers, etc. | ||
- Manage the lifetime of HttpClientMessageHandlers to avoid common DNS problems that can be hit when managing `HttpClient` lifetimes yourself. | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd suggest adding another bullet point here:
|
||
## Consumption patterns | ||
|
||
There are several ways that `HttpClientFactory` can be used in your app. None of them are strictly superior to another, it really depends on your app and the constraints you are working under. | ||
|
||
## Basic usage | ||
|
||
The `HttpClientFactory` can be used directly in your code to access `HttpClient` instances. First, the services must be registered with the ServiceProvider. | ||
|
||
```csharp | ||
public void ConfigureServices(IServiceCollection services) | ||
{ | ||
services.AddHttpClient(); | ||
services.AddMvc(); | ||
} | ||
``` | ||
|
||
Once registered, you can accept a `IHttpClientFactory` whereever services can be injected by the DI framework which can then be used to create a `HttpClient` instance. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
```csharp | ||
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/"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this line use |
||
return View(); | ||
} | ||
} | ||
``` | ||
|
||
Using `HttpClientFactory` like this is a good way to start refactoring an existing app, as it has no impact on the way you use `HttpClient`. In places where you create HttpClients, replace those with a call to `CreateClient()`. | ||
|
||
## Named clients | ||
|
||
If you have multiple distinct uses of `HttpClient`, each with different configurations, then you may want to use **named clients**. The common configuration for the use of that named `HttpClient` can be specified during registration. | ||
|
||
```csharp | ||
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 `AddHttpClient` has been called 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
The configuration function here will get called every time ceateClient is called, as a new instance of `HttpClient` is created each time. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
To consume a named client in your code you can pass the name of the client to `CreateClient`. | ||
|
||
```csharp | ||
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like the wrong line got replaced here, 2 clients are created and neither is used. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you intended to create two clients here, I have a bit of a gripe with it because we'd never tell a user to do this 😆 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I originally took this sample the text below it from @glennc's work on the Wiki. It appeared the intent was to show both options being used within one app. Happy to factor this out of the final sample app though and focus on each use case in isolation. In the sample the current approach is to register multiple clients to show the various consumption patterns. Do we need to consider that when designing the sample? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think registering multiple clients is fine, my specific gripe is about this method ... |
||
} | ||
} | ||
``` | ||
|
||
In the preceding code the gitHubClient will have the `BaseAddress` and `DefaultRequestHeaders` set whereas the defaultClient does not. This provides you the with the ability to have different configurations for different purposes. This may mean different configurations per endpoint/API for example. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
`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. | ||
|
||
## Typed clients | ||
|
||
Typed clients give you the same capabilities as named clients without the need for using strings as keys. This provides IntelliSense and compiler help when consuming clients. 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 which deals with that endpoint. | ||
|
||
A typed client is expected to accept a HttpClient via it's constructor. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HttpClient --> |
||
|
||
```csharp | ||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. /cc @glennc for visibility. I'm not a super fan of this. If you end up needing to use configuration to get these values then you have to find a way to pass that in here. Perhaps Glenn and I need to discuss this and come to an understanding. |
||
} | ||
} | ||
``` | ||
|
||
To register a typed client the generic `AddHttpClient` method can be used, specifying our typed client class. | ||
|
||
```csharp | ||
public void ConfigureServices(IServiceCollection services) | ||
{ | ||
services.AddHttpClient<GitHubService>(); | ||
services.AddMvc(); | ||
} | ||
``` | ||
|
||
The typed client is registered as transient with the DI framework. The typed client can then be injected and consumed directly. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
```csharp | ||
public class MyController : Controller | ||
{ | ||
private GitHubService _gitHubService; | ||
|
||
public MyController(GitHubService gitHubService) | ||
{ | ||
_gitHubService = gitHubService; | ||
} | ||
|
||
public IActionResult Index() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to mark this action as |
||
{ | ||
var result = await _gitHubService.Client.GetStringAsync("/orgs/octokit/repos"); | ||
return Ok(result); | ||
} | ||
} | ||
``` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is great 👍 |
||
|
||
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. | ||
|
||
If you prefer, the configuration for a typed client can be specified during registration, rather than in the constructor for the typed client. | ||
|
||
```csharp | ||
public void ConfigureServices(IServiceCollection services) | ||
{ | ||
services.AddHttpClient<GitHubService>(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.AddMvc(); | ||
} | ||
``` | ||
|
||
It's possible to entirely encapsulate the `HttpClient` within a typed client. Rather than exposing it as a property, public methods can be provided which internally consume the `HttpClient` instance. | ||
|
||
```csharp | ||
public class ValuesService | ||
{ | ||
private readonly HttpClient _httpClient; | ||
private readonly IMemoryCache _cache; | ||
private readonly ILogger<ValuesService> _logger; | ||
|
||
public ValuesService() { } | ||
|
||
public ValuesService(HttpClient client, IMemoryCache cache, ILogger<ValuesService> logger) | ||
{ | ||
_httpClient = client; | ||
_cache = cache; | ||
_logger = logger; | ||
} | ||
|
||
public async Task<IEnumerable<string>> GetValues() | ||
{ | ||
var result = await _httpClient.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; | ||
} | ||
} | ||
``` | ||
|
||
In this example, the `HttpClient` is stored as a private field and all access to make external calls goes through the GetValues method. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. GetValues --> |
||
|
||
## Generated clients | ||
|
||
`HttpClientFactory` can be used in combination with other third-party libraries such as [Refit](https://github.com/paulcbetts/refit). Refit is a REST library for .NET that is inspired by Square's [Retrofit](http://square.github.io/retrofit/) library. It turns your REST API into a live interface. This interface defines a external REST API. An implementation of the interface is generated by the `RestService` using `HttpClient` to make its calls. | ||
|
||
First an interface and reply are defined which represent the exteranl API and its response. | ||
|
||
```csharp | ||
public interface IHelloClient | ||
{ | ||
[Get("/helloworld")] | ||
Task<Reply> GetMessageAsync(); | ||
} | ||
|
||
public class Reply | ||
{ | ||
public string Message { get; set; } | ||
} | ||
``` | ||
|
||
A typed client can then be added, using Refit to generate the implementation. | ||
|
||
```csharp | ||
public void ConfigureServices(IServiceCollection services) | ||
{ | ||
services.AddHttpClient("hello", c => | ||
{ | ||
c.BaseAddress = new Uri("http://localhost:5000"); | ||
}) | ||
.AddTypedClient(c => Refit.RestService.For<IHelloClient>(c)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We also have another variant here that will takes One other comment I have about this section is that this style of configuration is in no way specific to refit. It can work with any type that accepts an |
||
|
||
services.AddMvc(); | ||
} | ||
``` | ||
|
||
The defined interface can then be consumed wherever necessary in your code, with the implementation provided by the DI framework and Refit. | ||
|
||
```csharp | ||
[ApiController] | ||
public class ValuesController : ControllerBase | ||
{ | ||
private readonly IHelloClient _client; | ||
|
||
public ValuesController(IHelloClient client) | ||
{ | ||
_client = client; | ||
} | ||
|
||
[HttpGet("/")] | ||
public async Task<ActionResult<Reply>> Index() | ||
{ | ||
return await _client.GetMessageAsync(); | ||
} | ||
} | ||
``` | ||
|
||
## Outgoing request middleware | ||
|
||
The `HttpClientFactory` supports registering and chaining `DelegatingHandlers` to easily build an outgoing request middleware pipeline. Each of these handlers is able to perform work before and after the outgoing request, in a very similar pattern to the middleware pipeline in ASP.NET Core. This provides a mechanism to manage cross cutting concerns around the requests an app is making. This includes things such as caching, error handling, serialization and logging. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @scottaddie Do you have a preference around the code backticks around pluralised types? Inclusive of the "s" or not? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I try to avoid the plural form in backticks, as it looks awkward. I suggest rewriting to something like the following: |
||
|
||
To create a handler, a class can be added, deriving from `DelegatingHandler`. The `SendAsync` method can then be overridden to execute code before and after the next handler in the pipeline. | ||
|
||
```csharp | ||
private class RetryHandler : DelegatingHandler | ||
{ | ||
public int RetryCount { get; set; } = 5; | ||
|
||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | ||
{ | ||
for (var i = 0; i < RetryCount; i++) | ||
{ | ||
try | ||
{ | ||
return base.SendAsync(request, cancellationToken); | ||
} | ||
catch (HttpRequestException) when (i == RetryCount - 1) | ||
{ | ||
throw; | ||
} | ||
catch (HttpRequestException) | ||
{ | ||
// Retry | ||
Task.Delay(TimeSpan.FromMilliseconds(50)); | ||
} | ||
} | ||
|
||
// Unreachable. | ||
throw null; | ||
} | ||
} | ||
``` | ||
|
||
The preceding code, defines a basic retry handler which will retry up to five times if a `HttpRequestException` is caught. | ||
|
||
During registration, one or more handlers can be added to the configuration for a `HttpClient` via extension methods on the `HttpClientBuilder`. | ||
|
||
```csharp | ||
public static void Configure(IServiceCollection services) | ||
{ | ||
services.AddTransient<RetryHandler>(); | ||
|
||
services.AddHttpClient("example", c => | ||
{ | ||
c.BaseAddress = new Uri("https://localhost:5000/"); | ||
}) | ||
.AddHttpMessageHandler<RetryHandler>(); // Retry requests to github using the retry handler | ||
} | ||
``` | ||
|
||
Here the `RetryHandler` is registered as a transient service with the DI framework. Once registered `AddHttpMessageHandler` can be called, passing in the type for the handler. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
Multiple handlers can be registered in the order that they should execute. Each handler wraps the next handler until the final `HttpClientHandler` executes the request. | ||
|
||
```csharp | ||
public static void Configure(IServiceCollection services) | ||
{ | ||
services.AddTransient<OuterHandler>(); | ||
services.AddTransient<InnerHandler>(); | ||
|
||
services.AddHttpClient("example", c => | ||
{ | ||
c.BaseAddress = new Uri("https://localhost:5000/"); | ||
}) | ||
.AddHttpMessageHandler<OuterHandler>() // This handler is on the outside and executes first on the way out and last on the way in. | ||
.AddHttpMessageHandler<InnerHandler>(); // This handler is on the inside, closest to the request. | ||
} | ||
``` | ||
|
||
## Handling errors with Polly | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm going to pass over this section for now since Polly is WIP |
||
|
||
_NOTE: This is a WIP since the Polly Extensions API is still currently under design._ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a note to yourself, or would you like the reader to see this? If it's intended for the reader, please use the following syntax:
|
||
|
||
The preceding example demonstrated building a simple retry handler manually. A more robust and feature-rich approach is to leverage a popular third-party library called [Polly](https://github.com/App-vNext/Polly). | ||
|
||
Polly is a comprehensive resilience and transient-fault-handling library for .NET which allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner. | ||
|
||
Extensions are provided for `HttpClientFactory` which enable easy integration and use of Polly policies with configured `HttpClient` instances. Rather than defining a handler manually, the extension can be called passing in a Polly policy. | ||
|
||
```csharp | ||
public static void Configure(IServiceCollection services) | ||
{ | ||
services.AddTransient<OuterHandler>(); | ||
services.AddTransient<InnerHandler>(); | ||
|
||
services.AddHttpClient("example", c => | ||
{ | ||
c.BaseAddress = new Uri("https://localhost:5000/"); | ||
}) | ||
|
||
// Build a totally custom policy using any criteria | ||
.AddPolicyHandler(Policy.Handle<HttpRequestException>().RetryAsync()) | ||
|
||
// Build a policy that will handle exceptions (connection failures) | ||
.AddExceptionPolicyHandler(p => p.RetryAsync()) | ||
|
||
// Build a policy that will handle exceptions and 500s from the remote server | ||
.AddServerErrorPolicyHandler(p => p.RetryAsync()) | ||
|
||
// Build a policy that will handle exceptions, 400s, and 500s from the remote server | ||
.AddBadRequestPolicyHandler(p => p.RetryAsync()); | ||
} | ||
``` | ||
|
||
## DNS and handler rotation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be great to call this section 'HttpClient' and lifetime management and introduce some of these concepts a bit more gently. |
||
|
||
Each time you call `CreateClient` on the `HttpClientFactory` you get a new instance of a `HttpClient`, but the factory will reuse the underlying H`ttpMessageHandler` 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 machine. | ||
|
||
TODO - DNS | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HttpClientFactory --> IHttpClientFactory