Skip to content
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

Merged
merged 44 commits into from
May 2, 2018
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
1b897ac
Starting work on HttpClientFactory documentation (WIP)
stevejgordon Feb 15, 2018
37a56fb
Fixing some minor issues
stevejgordon Feb 15, 2018
0523d99
Updating per initial feedback
stevejgordon Feb 16, 2018
ca0cc90
Adding refit example
stevejgordon Feb 16, 2018
7026d16
Further feedback adjustments
stevejgordon Feb 16, 2018
ed1b3dd
Adding delegating handler middleware section
stevejgordon Feb 16, 2018
bfed490
Minor typo
stevejgordon Feb 16, 2018
812cf28
Initial Polly example
stevejgordon Feb 16, 2018
86e239d
Updating with latest feedback
stevejgordon Feb 19, 2018
ec14f81
Pass to remove you/your + improve readability and clarity
stevejgordon Feb 19, 2018
d527081
Additional tidy-up
stevejgordon Feb 19, 2018
ad34a62
Edits for latest feedback
stevejgordon Feb 21, 2018
37a5deb
UE pass
scottaddie Feb 21, 2018
b5aa44b
Snippet fixes
stevejgordon Feb 22, 2018
135a6e1
Add ms.custom metadata
scottaddie Feb 22, 2018
b62b134
Adding WIP sample project
stevejgordon Feb 23, 2018
c71f194
Minor edits to Fundamentals index page
scottaddie Feb 23, 2018
a7e1c51
Relocate master TOC link
scottaddie Feb 23, 2018
590150d
Feedback and sample update to 2.1 preview
stevejgordon Feb 27, 2018
567afbb
Fixing some missed HttpClientFactory items
stevejgordon Mar 1, 2018
5d18655
Initial snippets from sample
stevejgordon Mar 1, 2018
2251cb9
Fixing a copy/paste mistake
stevejgordon Mar 1, 2018
c8b6438
fixing code link
stevejgordon Mar 1, 2018
aaf26d3
Remove gerund from title
scottaddie Mar 1, 2018
c9d0daf
Fix wording in the description metadata value
scottaddie Mar 1, 2018
7825296
Minor verbiage tweaks
scottaddie Mar 2, 2018
b077c4b
WIP
stevejgordon Mar 20, 2018
44be1a5
Working on Polly sample and documentation
stevejgordon Apr 4, 2018
f95bd56
Using code from sample in docs
stevejgordon Apr 4, 2018
d231feb
UE pass
scottaddie Apr 5, 2018
957f760
Convert usage H2 headings to H3
scottaddie Apr 5, 2018
f715142
Adding initial logging information
stevejgordon Apr 6, 2018
05e139f
Verbiage tweaks
scottaddie Apr 10, 2018
af9c32f
Updating to 2.1 preview 2
stevejgordon Apr 16, 2018
aaf3240
Removing HTTPS redirection since launchSettings does not default to c…
stevejgordon Apr 17, 2018
fdf6d7c
Add monikerRange metadata for 2.1+
scottaddie Apr 23, 2018
c7d9e6d
Major sample and doc content update
stevejgordon Apr 26, 2018
b2c7d60
Adding section about configuring HttpMessageHandler
stevejgordon Apr 27, 2018
699e07d
Updating with feedback from Ryan
stevejgordon Apr 27, 2018
e4bd7f0
Feedback and updates
stevejgordon Apr 28, 2018
d7a5b8c
Incorporate Acrolinx feedback
scottaddie Apr 30, 2018
4c76827
Updating sample with preferred idioms and minor doc tweaks
stevejgordon May 2, 2018
8a37fc7
Fixing grammar and spelling
stevejgordon May 2, 2018
b34cda6
Add 2.1 Preview include
scottaddie May 2, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
391 changes: 391 additions & 0 deletions aspnetcore/fundamentals/http-requests.md
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HttpClientFactory --> IHttpClientFactory

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)
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest adding another bullet point here:

  • Adds a configurable logging experience (via ILogger) for all requests sent through clients created by the factory.

## 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • whereever --> anywhere
  • We try to keep sentences as concise as possible. This improves the machine translation result used for localization of the content. I recommend rewriting this as 2 sentences. Maybe something like the following:
    "Once registered, you can accept a IHttpClientFactory anywhere services can be injected by the DI framework. The IHttpClientFactory can then be used to create a HttpClient instance."


```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/");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this line use await? If so, also update the action signature.

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here AddHttpClient has been called twice, once with the name 'github' and once without.
In the preceding code, AddHttpClient is called twice: once with the name "github" and once without.


The configuration function here will get called every time ceateClient is called, as a new instance of `HttpClient` is created each time.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ceateClient --> CreateClient
  • will get called --> is called


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();
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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 😆

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • gitHubClient --> gitHubClient
  • defaultClient --> defaultClient


`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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HttpClient --> 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;
Copy link
Member

Choose a reason for hiding this comment

The 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with the DI framework.
with DI.


```csharp
public class MyController : Controller
{
private GitHubService _gitHubService;

public MyController(GitHubService gitHubService)
{
_gitHubService = gitHubService;
}

public IActionResult Index()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to mark this action as async and return a Task.

{
var result = await _gitHubService.Client.GetStringAsync("/orgs/octokit/repos");
return Ok(result);
}
}
```
Copy link
Member

Choose a reason for hiding this comment

The 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetValues --> 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));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also have another variant here that will takes <TInterface, TImpl>.

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 HttpClient, or you can provide your own factory methods.


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.
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Member

@scottaddie scottaddie Feb 16, 2018

Choose a reason for hiding this comment

The 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:
"The HttpClientFactory supports registering and chaining a DelegatingHandler to easily build an outgoing request middleware pipeline."


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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Here --> In the preceding code,
  • the DI framework --> DI
  • Add a comma after "Once registered"


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
Copy link
Member

Choose a reason for hiding this comment

The 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._
Copy link
Member

Choose a reason for hiding this comment

The 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:

> [!NOTE]
> This section is a work in progress, since the Polly Extensions API is in the design phase.


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
Copy link
Member

Choose a reason for hiding this comment

The 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


4 changes: 4 additions & 0 deletions aspnetcore/fundamentals/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,7 @@ For more information, see [Choosing between .NET Core and .NET Framework](/dotne
## Choose between ASP.NET Core and ASP.NET

For more information on choosing between ASP.NET Core and ASP.NET, see [Choose between ASP.NET Core and ASP.NET](xref:fundamentals/choose-between-aspnet-and-aspnetcore).

## Making HTTP requests

For information about using `HttpClientFactory` to access HttpClient instances to make HTTP requests, see [Initiate HTTP requests](xref:fundamentals/http-requests).
Loading