Fazer solicitações HTTP usando IHttpClientFactory no ASP.NET Core

Por Kirk Larkin, Steve Gordon, Glenn Condrone Ryan Nowak.

É possível registrar e usar um IHttpClientFactory para configurar e criar instâncias de HttpClient em um aplicativo. IHttpClientFactory oferece os seguintes benefícios:

  • Fornece um local central para nomear e configurar instâncias lógicas de HttpClient. Por exemplo, um cliente chamado GitHub pode ser registrado e configurado para acessar GitHub. Um cliente padrão pode ser registrado para acesso geral.
  • Codifica o conceito de middleware de saída por meio da delegação de manipuladores no HttpClient . Fornece extensões para o middleware baseado em Polly para tirar proveito da delegação de manipuladores no HttpClient .
  • Gerencia o pooling e o tempo de vida de instâncias subjacentes HttpClientMessageHandler . O gerenciamento automático evita problemas comuns de DNS (sistema de nomes de domínio) que ocorrem ao gerenciar manualmente os HttpClient tempos de vida.
  • Adiciona uma experiência de registro em log configurável (via ILogger) para todas as solicitações enviadas por meio de clientes criados pelo alocador.

Exiba ou baixe o código de exemplo (como baixar).

O código de exemplo nesta versão do tópico usa System.Text.Json para desserializar o conteúdo JSON retornado em respostas http. Para obter exemplos que usam Json.NET e ReadAsAsync<T> , use o seletor de versão para selecionar uma versão 2. x deste tópico.

Padrões de consumo

Há várias maneiras de usar o IHttpClientFactory em um aplicativo:

A melhor abordagem depende dos requisitos do aplicativo.

Uso básico

IHttpClientFactory pode ser registrado chamando AddHttpClient :

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient();
        // Remaining code deleted for brevity.

Um IHttpClientFactory pode ser solicitado usando injeção de dependência (di). O código a seguir usa IHttpClientFactory para criar uma HttpClient instância do:

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            Branches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(responseStream);
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }
    }
}

Usar IHttpClientFactory como no exemplo anterior é uma boa maneira de refatorar um aplicativo existente. Ele não tem impacto sobre como o HttpClient é usado. Em locais onde HttpClient as instâncias são criadas em um aplicativo existente, substitua essas ocorrências por chamadas para CreateClient .

Clientes nomeados

Os clientes nomeados são uma boa opção quando:

  • O aplicativo requer muitos usos distintos do HttpClient .
  • Muitos HttpClient s têm configuração diferente.

A configuração de um nome HttpClient pode ser especificada durante o registro em Startup.ConfigureServices :

services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

No código anterior, o cliente está configurado com:

  • O endereço base https://api.github.com/ .
  • dois cabeçalhos necessários para trabalhar com a API de GitHub.

CreateClient

Cada vez CreateClient é chamado:

  • Uma nova instância do HttpClient é criada.
  • A ação de configuração é chamada.

Para criar um cliente nomeado, passe seu nome para CreateClient :

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            PullRequests = await JsonSerializer.DeserializeAsync
                    <IEnumerable<GitHubPullRequest>>(responseStream);
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

No código anterior, a solicitação não precisa especificar um nome do host. O código pode passar apenas o caminho, já que o endereço base configurado para o cliente é usado.

Clientes com tipo

Clientes com tipo:

  • Fornecem as mesmas funcionalidade que os clientes nomeados sem a necessidade de usar cadeias de caracteres como chaves.
  • Fornecem a ajuda do IntelliSense e do compilador durante o consumo de clientes.
  • Fornecem um único local para configurar e interagir com um determinado HttpClient. Por exemplo, um único cliente tipado pode ser usado:
    • Para um ponto de extremidade de back-end único.
    • Para encapsular toda a lógica lidando com o ponto de extremidade.
  • Trabalhe com DI e pode ser injetado onde necessário no aplicativo.

Um cliente tipado aceita um HttpClient parâmetro em seu construtor:

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept",
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent",
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        return await Client.GetFromJsonAsync<IEnumerable<GitHubIssue>>(
          "/repos/aspnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");
    }
}

No código anterior:

  • A configuração é movida para o cliente digitado.
  • O objeto HttpClient é exposto como uma propriedade pública.

Métodos específicos de API podem ser criados para expor a HttpClient funcionalidade. Por exemplo, o GetAspNetDocsIssues método encapsula o código para recuperar problemas abertos.

O código a seguir AddHttpClient chama Startup.ConfigureServices para registrar uma classe de cliente tipada:

services.AddHttpClient<GitHubService>();

O cliente com tipo é registrado como transitório com a DI. No código anterior, o AddHttpClient registra GitHubService como um serviço transitório. Esse registro usa um método de fábrica para:

  1. Crie uma instância de HttpClient.
  2. Crie uma instância do GitHubService , passando a instância do HttpClient para seu construtor.

O cliente com tipo pode ser injetado e consumido diretamente:

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

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

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}

A configuração para um cliente tipado pode ser especificada durante o registro em Startup.ConfigureServices , em vez de no construtor do cliente digitado:

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

O HttpClient pode ser encapsulado em um cliente digitado. Em vez de expô-lo como uma propriedade, defina um método que chame a HttpClient instância internamente:

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<string>>(responseStream);
    }
}

No código anterior, o HttpClient é armazenado em um campo particular. O acesso ao HttpClient é pelo método público GetRepos .

Clientes gerados

IHttpClientFactory pode ser usado em combinação com bibliotecas de terceiros, como rEFIt. Refit é uma biblioteca REST para .NET. Ela converte APIs REST em interfaces dinâmicas. Uma implementação da interface é gerada dinamicamente pelo RestService usando HttpClient para fazer as chamadas de HTTP externas.

Uma interface e uma resposta são definidas para representar a API externa e sua resposta:

public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}

Um cliente com tipo pode ser adicionado usando a Refit para gerar a implementação:

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddControllers();
}

A interface definida pode ser consumida, quando necessário, com a implementação fornecida pela DI e pela Refit:

[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();
    }
}

Fazer solicitações POST, PUT e DELETE

Nos exemplos anteriores, todas as solicitações HTTP usam o verbo GET HTTP. HttpClient também dá suporte a outros verbos HTTP, incluindo:

  • POST
  • PUT
  • DELETE
  • PATCH

Para obter uma lista completa de verbos HTTP com suporte, consulte HttpMethod .

O exemplo a seguir mostra como fazer uma solicitação HTTP POST:

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem, _jsonSerializerOptions),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

No código anterior, o CreateItemAsync método:

  • Serializa o TodoItem parâmetro para JSON usando System.Text.Json . Isso usa uma instância do JsonSerializerOptions para configurar o processo de serialização.
  • Cria uma instância do StringContent para empacotar o JSON serializado para envio no corpo da solicitação HTTP.
  • Chamadas PostAsync para enviar o conteúdo JSON para a URL especificada. Essa é uma URL relativa que é adicionada ao HttpClient. BaseAddress.
  • Chamadas EnsureSuccessStatusCode para gerar uma exceção se o código de status de resposta não indicar êxito.

HttpClient também oferece suporte a outros tipos de conteúdo. Por exemplo, MultipartContent e StreamContent. Para obter uma lista completa de conteúdo com suporte, consulte HttpContent .

O exemplo a seguir mostra uma solicitação HTTP PUT:

public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

O código anterior é muito semelhante ao exemplo de POST. O SaveItemAsync método chama PutAsync em vez de PostAsync .

O exemplo a seguir mostra uma solicitação HTTP DELETE:

public async Task DeleteItemAsync(long itemId)
{
    using var httpResponse =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    httpResponse.EnsureSuccessStatusCode();
}

No código anterior, o DeleteItemAsync método chama DeleteAsync . Como as solicitações HTTP DELETE normalmente não contêm corpo, o DeleteAsync método não fornece uma sobrecarga que aceita uma instância do HttpContent .

Para saber mais sobre como usar verbos HTTP diferentes com o HttpClient , consulte HttpClient .

Middleware de solicitação de saída

HttpClient tem o conceito de delegar manipuladores que podem ser vinculados juntos para solicitações HTTP de saída. IHttpClientFactory:

  • Simplifica a definição dos manipuladores a serem aplicados para cada cliente nomeado.

  • Dá suporte ao registro e encadeamento de vários manipuladores para criar um pipeline de middleware de solicitação de saída. Cada um desses manipuladores é capaz de realizar o trabalho antes e após a solicitação de saída. Esse padrão:

    • É semelhante ao pipeline de middleware de entrada no ASP.NET Core.
    • Fornece um mecanismo para gerenciar preocupações abrangentes em relação a solicitações HTTP, como:
      • cache
      • tratamento de erros
      • serialização
      • registro em log

Para criar um manipulador de delegação:

  • Derivar de DelegatingHandler .
  • Substitua SendAsync. Execute o código antes de passar a solicitação para o próximo manipulador no pipeline:
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

O código anterior verifica se o X-API-KEY cabeçalho está na solicitação. Se X-API-KEY estiver ausente, BadRequest será retornado.

Mais de um manipulador pode ser adicionado à configuração para um HttpClient com Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler :

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ValidateHeaderHandler>();

    services.AddHttpClient("externalservice", c =>
    {
        // Assume this is an "external" service which requires an API KEY
        c.BaseAddress = new Uri("https://localhost:5001/");
    })
    .AddHttpMessageHandler<ValidateHeaderHandler>();

    // Remaining code deleted for brevity.

No código anterior, o ValidateHeaderHandler é registrado com a DI. Depois de registrado, o AddHttpMessageHandler poderá ser chamado, passando o tipo para o manipulador.

Vários manipuladores podem ser registrados na ordem em que eles devem ser executados. Cada manipulador encapsula o próximo manipulador até que o HttpClientHandler final execute a solicitação:

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

Usar DI no middleware de solicitação de saída

Quando IHttpClientFactory o cria um novo manipulador de delegação, ele usa di para atender aos parâmetros do construtor do manipulador. IHttpClientFactory Cria um escopo de di separado para cada manipulador, o que pode levar a um comportamento surpreendente quando um manipulador consome um serviço com escopo .

Por exemplo, considere a seguinte interface e sua implementação, que representa uma tarefa como uma operação com um identificador, OperationId :

public interface IOperationScoped 
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

Como o nome sugere, IOperationScoped é registrado com di usando um tempo de vida no escopo :

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(options =>
        options.UseInMemoryDatabase("TodoItems"));

    services.AddHttpContextAccessor();

    services.AddHttpClient<TodoClient>((sp, httpClient) =>
    {
        var httpRequest = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request;

        // For sample purposes, assume TodoClient is used in the context of an incoming request.
        httpClient.BaseAddress = new Uri(UriHelper.BuildAbsolute(httpRequest.Scheme,
                                         httpRequest.Host, httpRequest.PathBase));
        httpClient.Timeout = TimeSpan.FromSeconds(5);
    });

    services.AddScoped<IOperationScoped, OperationScoped>();
    
    services.AddTransient<OperationHandler>();
    services.AddTransient<OperationResponseHandler>();

    services.AddHttpClient("Operation")
        .AddHttpMessageHandler<OperationHandler>()
        .AddHttpMessageHandler<OperationResponseHandler>()
        .SetHandlerLifetime(TimeSpan.FromSeconds(5));

    services.AddControllers();
    services.AddRazorPages();
}

O manipulador de delegação a seguir consome e usa IOperationScoped para definir o X-OPERATION-ID cabeçalho para a solicitação de saída:

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationService;

    public OperationHandler(IOperationScoped operationScoped)
    {
        _operationService = operationScoped;
    }

    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationService.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}

No HttpRequestsSample Download], navegue até /Operation e atualize a página. O valor do escopo da solicitação é alterado para cada solicitação, mas o valor do escopo do manipulador só é alterado a cada 5 segundos.

Os manipuladores podem depender dos serviços de qualquer escopo. Os serviços dos quais os manipuladores dependem são descartados quando o manipulador é descartado.

Use uma das seguintes abordagens para compartilhar o estado por solicitação com os manipuladores de mensagens:

Usar manipuladores baseados no Polly

IHttpClientFactory integra-se com a biblioteca de terceiros Polly. O Polly é uma biblioteca abrangente de tratamento de falha transitória e de resiliência para .NET. Ela permite que os desenvolvedores expressem políticas, como Repetição, Disjuntor, Tempo Limite, Isolamento de Bulkhead e Fallback de maneira fluente e thread-safe.

Os métodos de extensão são fornecidos para habilitar o uso de políticas do Polly com instâncias de HttpClient configuradas. As extensões Polly dão suporte à adição de manipuladores baseados em Polly a clientes. Polly requer o pacote de NuGet Microsoft. Extensions. Http. Polly .

Tratar falhas transitórias

As falhas normalmente ocorrem quando chamadas HTTP externas são transitórias. AddTransientHttpErrorPolicy permite que uma política seja definida para lidar com erros transitórios. Políticas configuradas com AddTransientHttpErrorPolicy o tratamento das seguintes respostas:

AddTransientHttpErrorPolicy fornece acesso a um PolicyBuilder objeto configurado para tratar erros que representam uma possível falha transitória:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient<UnreliableEndpointCallerService>()
        .AddTransientHttpErrorPolicy(p => 
            p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

    // Remaining code deleted for brevity.

No código anterior, uma política WaitAndRetryAsync é definida. As solicitações com falha são repetidas até três vezes com um atraso de 600 ms entre as tentativas.

Selecionar políticas dinamicamente

Os métodos de extensão são fornecidos para adicionar manipuladores baseados em Polly, por exemplo, AddPolicyHandler . A sobrecarga a seguir AddPolicyHandler inspeciona a solicitação para decidir qual política aplicar:

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

No código anterior, se a solicitação de saída é um HTTP GET, um tempo limite de 10 segundos é aplicado. Para qualquer outro método HTTP, é usado um tempo limite de 30 segundos.

Adicionar vários manipuladores do Polly

É comum aninhar políticas Polly:

services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

No exemplo anterior:

  • Dois manipuladores são adicionados.
  • O primeiro manipulador usa AddTransientHttpErrorPolicy para adicionar uma política de repetição. As solicitações com falha são repetidas até três vezes.
  • A segunda AddTransientHttpErrorPolicy chamada adiciona uma política de disjuntor. Outras solicitações externas serão bloqueadas por 30 segundos se 5 tentativas com falha ocorrerem em sequência. As políticas de disjuntor são políticas com estado. Todas as chamadas por meio desse cliente compartilham o mesmo estado do circuito.

Adicionar políticas do registro do Polly

Uma abordagem para gerenciar as políticas usadas com frequência é defini-las uma única vez e registrá-las em um PolicyRegistry.

No seguinte código:

public void ConfigureServices(IServiceCollection services)
{           
    var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(10));
    var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(30));
    
    var registry = services.AddPolicyRegistry();

    registry.Add("regular", timeout);
    registry.Add("long", longTimeout);
    
    services.AddHttpClient("regularTimeoutHandler")
        .AddPolicyHandlerFromRegistry("regular");

    services.AddHttpClient("longTimeoutHandler")
       .AddPolicyHandlerFromRegistry("long");

    // Remaining code deleted for brevity.

Para obter mais informações sobre as IHttpClientFactory integrações do e do Polly, consulte o wiki do Polly.

HttpClient e gerenciamento de tempo de vida

Uma nova instância de HttpClient é chamada, sempre que CreateClient é chamado na IHttpClientFactory. Um HttpMessageHandler é criado por cliente nomeado. O alocador gerencia a vida útil das instâncias do HttpMessageHandler.

IHttpClientFactory cria pools com as instâncias de HttpMessageHandler criadas pelo alocador para reduzir o consumo de recursos. Uma instância de HttpMessageHandler poderá ser reutilizada no pool, ao criar uma nova instância de HttpClient, se o respectivo tempo de vida não tiver expirado.

O pooling de manipuladores é preferível porque normalmente cada manipulador gerencia as próprias conexões HTTP subjacentes. Criar mais manipuladores do que o necessário pode resultar em atrasos de conexão. Alguns manipuladores também mantêm as conexões abertas indefinidamente, o que pode impedir que o manipulador reajam às alterações de DNS (sistema de nomes de domínio).

O tempo de vida padrão do manipulador é de 2 minutos. O valor padrão pode ser substituído em uma base de cliente por nome:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient("extendedhandlerlifetime")
        .SetHandlerLifetime(TimeSpan.FromMinutes(5));

    // Remaining code deleted for brevity.

HttpClient as instâncias geralmente podem ser tratadas como objetos .NET que não exigem descarte. O descarte cancela as solicitações de saída e garante que a determinada instância de HttpClient não seja usada depois de chamar Dispose. IHttpClientFactory rastreia e descarta recursos usados pelas instâncias de HttpClient.

Manter uma única instância de HttpClient ativa por uma longa duração é um padrão comum usado, antes do início de IHttpClientFactory. Esse padrão se torna desnecessário após a migração para IHttpClientFactory.

Alternativas para IHttpClientFactory

IHttpClientFactoryO uso do em um aplicativo habilitado para di evita:

  • Problemas de esgotamento de recursos por instâncias de Pooling HttpMessageHandler .
  • Problemas de DNS obsoletos por HttpMessageHandler instâncias de ciclo em intervalos regulares.

Há maneiras alternativas de resolver os problemas anteriores usando uma instância de vida longa SocketsHttpHandler .

  • Crie uma instância do SocketsHttpHandler quando o aplicativo for iniciado e use-o para a vida útil do aplicativo.
  • Configure PooledConnectionLifetime para um valor apropriado com base em tempos de atualização de DNS.
  • Crie HttpClient instâncias usando new HttpClient(handler, disposeHandler: false) conforme necessário.

As abordagens anteriores resolvem os problemas de gerenciamento de recursos que são IHttpClientFactory resolvidos de forma semelhante.

  • O SocketsHttpHandler compartilha conexões entre HttpClient instâncias. Esse compartilhamento impede o esgotamento de soquete.
  • O SocketsHttpHandler ciclos se conexões de acordo com o PooledConnectionLifetime para evitar problemas de DNS obsoletos.

Cookies

As HttpMessageHandler instâncias em pool resultam em CookieContainer objetos que estão sendo compartilhados. O CookieContainer compartilhamento de objeto inesperado geralmente resulta em código incorreto. Para aplicativos que exigem cookie s, considere o:

  • Desabilitando a cookie manipulação automática
  • Evitá IHttpClientFactory

Chamada ConfigurePrimaryHttpMessageHandler para desabilitar a cookie manipulação automática:

services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });

Registro em log

Os clientes criado pelo IHttpClientFactory registram mensagens de log para todas as solicitações. Habilite o nível de informações apropriado na configuração de log para ver as mensagens de log padrão. Os registros em log adicionais, como o registro em log dos cabeçalhos de solicitação, estão incluídos somente no nível de rastreamento.

A categoria de log usada para cada cliente inclui o nome do cliente. Um cliente chamado MyNamedClient, por exemplo, registra mensagens com uma categoria de "System .net. http. HttpClient. MyNamedClient. LogicalHandler". Mensagens sufixadas com LogicalHandler ocorrem fora do pipeline do manipulador de solicitações. Na solicitação, as mensagens são registradas em log antes que qualquer outro manipulador no pipeline a tenha processado. Na resposta, as mensagens são registradas depois que qualquer outro manipulador do pipeline tenha recebido a resposta.

O registro em log também ocorre dentro do pipeline do manipulador de solicitações. No exemplo de MyNamedClient , essas mensagens são registradas com a categoria de log "System .net. http. HttpClient. MyNamedClient. ClientHandler". Para a solicitação, isso ocorre depois que todos os outros manipuladores forem executados e imediatamente antes de a solicitação ser enviada. Na resposta, esse registro em log inclui o estado da resposta antes que ela seja retornada por meio do pipeline do manipulador.

Habilitar o registro em log dentro e fora do pipeline permite a inspeção das alterações feitas por outros manipuladores do pipeline. Isso pode incluir alterações nos cabeçalhos de solicitação ou no código de status de resposta.

A inclusão do nome do cliente na categoria de log permite a filtragem de log para clientes nomeados específicos.

Configurar o HttpMessageHandler

Talvez seja necessário controlar a configuração do HttpMessageHandler interno usado por um cliente.

Um IHttpClientBuilder é retornado ao adicionar clientes nomeados ou com tipo. O método de extensão ConfigurePrimaryHttpMessageHandler pode ser usado para definir um representante. O representante que é usado para criar e configurar o HttpMessageHandler primário usado pelo cliente:

public void ConfigureServices(IServiceCollection services)
{            
    services.AddHttpClient("configured-inner-handler")
        .ConfigurePrimaryHttpMessageHandler(() =>
        {
            return new HttpClientHandler()
            {
                AllowAutoRedirect = false,
                UseDefaultCredentials = true
            };
        });

    // Remaining code deleted for brevity.

Usar IHttpClientFactory em um aplicativo de console

Em um aplicativo de console, adicione as seguintes referências de pacote ao projeto:

No exemplo a seguir:

  • IHttpClientFactory é registrado no contêiner de serviço do Host genérico.
  • MyService cria uma instância de fábrica do cliente do serviço, que é usada para criar um HttpClient. HttpClient é usado para recuperar uma página da Web.
  • Main cria um escopo para executar o método GetPage do serviço e gravar os primeiros 500 caracteres do conteúdo da página da Web no console.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}

Middleware de propagação de cabeçalho

a propagação de cabeçalho é um middleware ASP.NET Core para propagar cabeçalhos http da solicitação de entrada para as solicitações de cliente HTTP de saída. Para usar a propagação de cabeçalho:

  • Referencie o pacote Microsoft. AspNetCore. HeaderPropagation .

  • Configure o middleware e HttpClient em Startup :

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
        services.AddHeaderPropagation(options =>
        {
            options.Headers.Add("X-TraceId");
        });
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseHttpsRedirection();
    
        app.UseHeaderPropagation();
    
        app.UseRouting();
    
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    
  • O cliente inclui os cabeçalhos configurados nas solicitações de saída:

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

Recursos adicionais

Por Kirk Larkin, Steve Gordon, Glenn Condrone Ryan Nowak.

É possível registrar e usar um IHttpClientFactory para configurar e criar instâncias de HttpClient em um aplicativo. IHttpClientFactory oferece os seguintes benefícios:

  • Fornece um local central para nomear e configurar instâncias lógicas de HttpClient. Por exemplo, um cliente chamado GitHub pode ser registrado e configurado para acessar GitHub. Um cliente padrão pode ser registrado para acesso geral.
  • Codifica o conceito de middleware de saída por meio da delegação de manipuladores no HttpClient . Fornece extensões para o middleware baseado em Polly para tirar proveito da delegação de manipuladores no HttpClient .
  • Gerencia o pooling e o tempo de vida de instâncias subjacentes HttpClientMessageHandler . O gerenciamento automático evita problemas comuns de DNS (sistema de nomes de domínio) que ocorrem ao gerenciar manualmente os HttpClient tempos de vida.
  • Adiciona uma experiência de registro em log configurável (via ILogger) para todas as solicitações enviadas por meio de clientes criados pelo alocador.

Exiba ou baixe o código de exemplo (como baixar).

O código de exemplo nesta versão do tópico usa System.Text.Json para desserializar o conteúdo JSON retornado em respostas http. Para obter exemplos que usam Json.NET e ReadAsAsync<T> , use o seletor de versão para selecionar uma versão 2. x deste tópico.

Padrões de consumo

Há várias maneiras de usar o IHttpClientFactory em um aplicativo:

A melhor abordagem depende dos requisitos do aplicativo.

Uso básico

IHttpClientFactory pode ser registrado chamando AddHttpClient :

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient();
        // Remaining code deleted for brevity.

Um IHttpClientFactory pode ser solicitado usando injeção de dependência (di). O código a seguir usa IHttpClientFactory para criar uma HttpClient instância do:

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            Branches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(responseStream);
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }
    }
}

Usar IHttpClientFactory como no exemplo anterior é uma boa maneira de refatorar um aplicativo existente. Ele não tem impacto sobre como o HttpClient é usado. Em locais onde HttpClient as instâncias são criadas em um aplicativo existente, substitua essas ocorrências por chamadas para CreateClient .

Clientes nomeados

Os clientes nomeados são uma boa opção quando:

  • O aplicativo requer muitos usos distintos do HttpClient .
  • Muitos HttpClient s têm configuração diferente.

A configuração de um nome HttpClient pode ser especificada durante o registro em Startup.ConfigureServices :

services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

No código anterior, o cliente está configurado com:

  • O endereço base https://api.github.com/ .
  • dois cabeçalhos necessários para trabalhar com a API de GitHub.

CreateClient

Cada vez CreateClient é chamado:

  • Uma nova instância do HttpClient é criada.
  • A ação de configuração é chamada.

Para criar um cliente nomeado, passe seu nome para CreateClient :

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            PullRequests = await JsonSerializer.DeserializeAsync
                    <IEnumerable<GitHubPullRequest>>(responseStream);
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

No código anterior, a solicitação não precisa especificar um nome do host. O código pode passar apenas o caminho, já que o endereço base configurado para o cliente é usado.

Clientes com tipo

Clientes com tipo:

  • Fornecem as mesmas funcionalidade que os clientes nomeados sem a necessidade de usar cadeias de caracteres como chaves.
  • Fornecem a ajuda do IntelliSense e do compilador durante o consumo de clientes.
  • Fornecem um único local para configurar e interagir com um determinado HttpClient. Por exemplo, um único cliente com tipo pode ser usado:
    • Para um único ponto de extremidade de back-end.
    • Para encapsular toda a lógica que lida com o ponto de extremidade.
  • Trabalhe com DI e possa ser injetado quando necessário no aplicativo.

Um cliente digitado aceita um HttpClient parâmetro em seu construtor:

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept",
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent",
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        return await Client.GetFromJsonAsync<IEnumerable<GitHubIssue>>(
          "/repos/aspnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");
    }
}

No código anterior:

  • A configuração é movida para o cliente digitado.
  • O objeto HttpClient é exposto como uma propriedade pública.

Métodos específicos da API podem ser criados que HttpClient expõem a funcionalidade. Por exemplo, o GetAspNetDocsIssues método encapsula o código para recuperar problemas abertos.

O código a seguir AddHttpClient chama para registrar uma classe de cliente Startup.ConfigureServices digitada:

services.AddHttpClient<GitHubService>();

O cliente com tipo é registrado como transitório com a DI. No código anterior, AddHttpClient registra-se GitHubService como um serviço transitório. Esse registro usa um método de fábrica para:

  1. Crie uma instância de HttpClient.
  2. Crie uma instância GitHubService de , passando a instância de para seu HttpClient construtor.

O cliente com tipo pode ser injetado e consumido diretamente:

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

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

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}

A configuração de um cliente digitado pode ser especificada durante o registro no , em vez de no Startup.ConfigureServices construtor do cliente digitado:

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

O HttpClient pode ser encapsulado dentro de um cliente digitado. Em vez de expô-lo como uma propriedade, defina um método que chama a HttpClient instância internamente:

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<string>>(responseStream);
    }
}

No código anterior, o HttpClient é armazenado em um campo privado. O acesso ao HttpClient é pelo método GetRepos público.

Clientes gerados

IHttpClientFactory pode ser usado em combinação com bibliotecas de terceiros, como Reajuste. Refit é uma biblioteca REST para .NET. Ela converte APIs REST em interfaces dinâmicas. Uma implementação da interface é gerada dinamicamente pelo RestService usando HttpClient para fazer as chamadas de HTTP externas.

Uma interface e uma resposta são definidas para representar a API externa e sua resposta:

public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}

Um cliente com tipo pode ser adicionado usando a Refit para gerar a implementação:

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddControllers();
}

A interface definida pode ser consumida, quando necessário, com a implementação fornecida pela DI e pela Refit:

[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();
    }
}

Fazer solicitações POST, PUT e DELETE

Nos exemplos anteriores, todas as solicitações HTTP usam o verbo GET HTTP. HttpClient também dá suporte a outros verbos HTTP, incluindo:

  • POST
  • PUT
  • DELETE
  • PATCH

Para ver uma lista completa dos verbos HTTP com suporte, consulte HttpMethod .

O exemplo a seguir mostra como fazer uma solicitação HTTP POST:

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem, _jsonSerializerOptions),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

No código anterior, o CreateItemAsync método :

  • Serializa o parâmetro TodoItem para JSON usando System.Text.Json . Isso usa uma instância do JsonSerializerOptions para configurar o processo de serialização.
  • Cria uma instância de StringContent para empacotar o JSON serializado para enviar no corpo da solicitação HTTP.
  • Chama PostAsync para enviar o conteúdo JSON para a URL especificada. Essa é uma URL relativa que é adicionada ao HttpClient.BaseAddress.
  • Chama EnsureSuccessStatusCode para lançar uma exceção se o código de status de resposta não indicar êxito.

HttpClient também dá suporte a outros tipos de conteúdo. Por exemplo, MultipartContent e StreamContent. Para ver uma lista completa do conteúdo com suporte, consulte HttpContent .

O exemplo a seguir mostra uma solicitação HTTP PUT:

public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

O código anterior é muito semelhante ao exemplo POST. O SaveItemAsync método chama em vez de PutAsync PostAsync .

O exemplo a seguir mostra uma solicitação HTTP DELETE:

public async Task DeleteItemAsync(long itemId)
{
    using var httpResponse =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    httpResponse.EnsureSuccessStatusCode();
}

No código anterior, o método DeleteItemAsync chama DeleteAsync . Como as solicitações HTTP DELETE normalmente não contêm nenhum corpo, o método não fornece uma sobrecarga DeleteAsync que aceita uma instância do HttpContent .

Para saber mais sobre como usar diferentes verbos HTTP com HttpClient , consulte HttpClient .

Middleware de solicitação de saída

HttpClient tem o conceito de delegar manipuladores que podem ser vinculados em conjunto para solicitações HTTP de saída. IHttpClientFactory:

  • Simplifica a definição dos manipuladores a aplicar a cada cliente nomeado.

  • Dá suporte ao registro e encadeamento de vários manipuladores para criar um pipeline de middleware de solicitação de saída. Cada um desses manipuladores é capaz de executar o trabalho antes e depois da solicitação de saída. Esse padrão:

    • É semelhante ao pipeline de middleware de entrada no ASP.NET Core.
    • Fornece um mecanismo para gerenciar preocupações abrangentes em relação a solicitações HTTP, como:
      • cache
      • tratamento de erros
      • serialização
      • registro em log

Para criar um manipulador de delegação:

  • Derive de DelegatingHandler .
  • Substitua SendAsync. Execute o código antes de passar a solicitação para o próximo manipulador no pipeline:
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

O código anterior verifica se X-API-KEY o header está na solicitação. Se X-API-KEY estiver ausente, BadRequest será retornado.

Mais de um manipulador pode ser adicionado à configuração de um HttpClient com Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler :

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ValidateHeaderHandler>();

    services.AddHttpClient("externalservice", c =>
    {
        // Assume this is an "external" service which requires an API KEY
        c.BaseAddress = new Uri("https://localhost:5001/");
    })
    .AddHttpMessageHandler<ValidateHeaderHandler>();

    // Remaining code deleted for brevity.

No código anterior, o ValidateHeaderHandler é registrado com a DI. Depois de registrado, o AddHttpMessageHandler poderá ser chamado, passando o tipo para o manipulador.

Vários manipuladores podem ser registrados na ordem em que eles devem ser executados. Cada manipulador encapsula o próximo manipulador até que o HttpClientHandler final execute a solicitação:

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

Usar DI no middleware de solicitação de saída

Quando IHttpClientFactory cria um novo manipulador de delegação, ele usa a DI para atender aos parâmetros do construtor do manipulador. IHttpClientFactorycria um escopo de DI separado para cada manipulador, o que pode levar a um comportamento surpreendente quando um manipulador consome um serviço com escopo.

Por exemplo, considere a interface a seguir e sua implementação, que representa uma tarefa como uma operação com um identificador, OperationId :

public interface IOperationScoped 
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

Como seu nome sugere, é IOperationScoped registrado com DI usando um tempo de vida com escopo:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(options =>
        options.UseInMemoryDatabase("TodoItems"));

    services.AddHttpContextAccessor();

    services.AddHttpClient<TodoClient>((sp, httpClient) =>
    {
        var httpRequest = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request;

        // For sample purposes, assume TodoClient is used in the context of an incoming request.
        httpClient.BaseAddress = new Uri(UriHelper.BuildAbsolute(httpRequest.Scheme,
                                         httpRequest.Host, httpRequest.PathBase));
        httpClient.Timeout = TimeSpan.FromSeconds(5);
    });

    services.AddScoped<IOperationScoped, OperationScoped>();
    
    services.AddTransient<OperationHandler>();
    services.AddTransient<OperationResponseHandler>();

    services.AddHttpClient("Operation")
        .AddHttpMessageHandler<OperationHandler>()
        .AddHttpMessageHandler<OperationResponseHandler>()
        .SetHandlerLifetime(TimeSpan.FromSeconds(5));

    services.AddControllers();
    services.AddRazorPages();
}

O manipulador de delegação a seguir consome e IOperationScoped usa para definir o X-OPERATION-ID header para a solicitação de saída:

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationService;

    public OperationHandler(IOperationScoped operationScoped)
    {
        _operationService = operationScoped;
    }

    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationService.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}

No HttpRequestsSample download], navegue até /Operation e atualize a página. O valor do escopo da solicitação muda para cada solicitação, mas o valor do escopo do manipulador só muda a cada 5 segundos.

Os manipuladores podem depender de serviços de qualquer escopo. Os serviços dos quais os manipuladores dependem são descartados quando o manipulador é descartado.

Use uma das seguintes abordagens para compartilhar o estado por solicitação com os manipuladores de mensagens:

Usar manipuladores baseados no Polly

IHttpClientFactory integra-se à biblioteca de terceiros Polly. O Polly é uma biblioteca abrangente de tratamento de falha transitória e de resiliência para .NET. Ela permite que os desenvolvedores expressem políticas, como Repetição, Disjuntor, Tempo Limite, Isolamento de Bulkhead e Fallback de maneira fluente e thread-safe.

Os métodos de extensão são fornecidos para habilitar o uso de políticas do Polly com instâncias de HttpClient configuradas. As extensões do Polly são suportadas pela adição de manipuladores baseados em Polly aos clientes. A Polly requer o pacote Microsoft.Extensions.Http.Polly NuGet pacote.

Tratar falhas transitórias

Normalmente, ocorrem falhas quando chamadas HTTP externas são transitórias. AddTransientHttpErrorPolicy permite que uma política seja definida para tratar erros transitórios. As políticas configuradas com AddTransientHttpErrorPolicy lidam com as seguintes respostas:

AddTransientHttpErrorPolicy fornece acesso a um PolicyBuilder objeto configurado para tratar erros que representam uma possível falha transitória:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient<UnreliableEndpointCallerService>()
        .AddTransientHttpErrorPolicy(p => 
            p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

    // Remaining code deleted for brevity.

No código anterior, uma política WaitAndRetryAsync é definida. As solicitações com falha são repetidas até três vezes com um atraso de 600 ms entre as tentativas.

Selecionar políticas dinamicamente

Métodos de extensão são fornecidos para adicionar manipuladores baseados em Polly, por exemplo, AddPolicyHandler . A sobrecarga AddPolicyHandler a seguir inspeciona a solicitação para decidir qual política aplicar:

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

No código anterior, se a solicitação de saída é um HTTP GET, um tempo limite de 10 segundos é aplicado. Para qualquer outro método HTTP, é usado um tempo limite de 30 segundos.

Adicionar vários manipuladores do Polly

É comum aninhar políticas do Polly:

services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

No exemplo anterior:

  • Dois manipuladores são adicionados.
  • O primeiro manipulador usa AddTransientHttpErrorPolicy para adicionar uma política de nova tentativa. As solicitações com falha são repetidas até três vezes.
  • A segunda AddTransientHttpErrorPolicy chamada adiciona uma política de disjuntor. Outras solicitações externas serão bloqueadas por 30 segundos se 5 tentativas com falha ocorrerem sequencialmente. As políticas de disjuntor são políticas com estado. Todas as chamadas por meio desse cliente compartilham o mesmo estado do circuito.

Adicionar políticas do registro do Polly

Uma abordagem para gerenciar as políticas usadas com frequência é defini-las uma única vez e registrá-las em um PolicyRegistry.

No seguinte código:

public void ConfigureServices(IServiceCollection services)
{           
    var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(10));
    var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(30));
    
    var registry = services.AddPolicyRegistry();

    registry.Add("regular", timeout);
    registry.Add("long", longTimeout);
    
    services.AddHttpClient("regularTimeoutHandler")
        .AddPolicyHandlerFromRegistry("regular");

    services.AddHttpClient("longTimeoutHandler")
       .AddPolicyHandlerFromRegistry("long");

    // Remaining code deleted for brevity.

Para obter mais informações sobre IHttpClientFactory as integrações do e do Polly, consulte o wiki da Polly.

HttpClient e gerenciamento de tempo de vida

Uma nova instância de HttpClient é chamada, sempre que CreateClient é chamado na IHttpClientFactory. Um HttpMessageHandler é criado por cliente nomeado. O alocador gerencia a vida útil das instâncias do HttpMessageHandler.

IHttpClientFactory cria pools com as instâncias de HttpMessageHandler criadas pelo alocador para reduzir o consumo de recursos. Uma instância de HttpMessageHandler poderá ser reutilizada no pool, ao criar uma nova instância de HttpClient, se o respectivo tempo de vida não tiver expirado.

O pooling de manipuladores é preferível porque normalmente cada manipulador gerencia as próprias conexões HTTP subjacentes. Criar mais manipuladores do que o necessário pode resultar em atrasos de conexão. Alguns manipuladores também mantêm as conexões abertas indefinidamente, o que pode impedir que o manipulador reaja a alterações de DNS (Sistema de Nomes de Domínio).

O tempo de vida padrão do manipulador é de 2 minutos. O valor padrão pode ser substituído por cliente nomeado:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient("extendedhandlerlifetime")
        .SetHandlerLifetime(TimeSpan.FromMinutes(5));

    // Remaining code deleted for brevity.

HttpClient as instâncias geralmente podem ser tratadas como objetos .NET que não exigem descarte. O descarte cancela as solicitações de saída e garante que a determinada instância de HttpClient não seja usada depois de chamar Dispose. IHttpClientFactory rastreia e descarta recursos usados pelas instâncias de HttpClient.

Manter uma única instância de HttpClient ativa por uma longa duração é um padrão comum usado, antes do início de IHttpClientFactory. Esse padrão se torna desnecessário após a migração para IHttpClientFactory.

Alternativas para IHttpClientFactory

Usar IHttpClientFactory em um aplicativo habilitado para DI evita:

  • Problemas de esgotamento de recursos por HttpMessageHandler instâncias de pool.
  • Problemas de DNS desleixados por HttpMessageHandler instâncias de ciclo em intervalos regulares.

Há maneiras alternativas de resolver os problemas anteriores usando uma instância de longa SocketsHttpHandler duração.

  • Crie uma instância de SocketsHttpHandler quando o aplicativo é iniciado e use-a durante a vida útil do aplicativo.
  • Configure PooledConnectionLifetime para um valor apropriado com base nos tempos de atualização do DNS.
  • Crie HttpClient instâncias usando new HttpClient(handler, disposeHandler: false) conforme necessário.

As abordagens anteriores resolvem os problemas de gerenciamento de recursos IHttpClientFactory que resolvem de maneira semelhante.

  • O SocketsHttpHandler compartilha conexões entre HttpClient instâncias. Esse compartilhamento impede o esgotamento de soquete.
  • Os SocketsHttpHandler ciclos de conexões de acordo PooledConnectionLifetime com para evitar problemas de DNS desa gastos.

Cookies

As HttpMessageHandler instâncias em pool resulta em CookieContainer objetos sendo compartilhados. O compartilhamento de objeto CookieContainer não esperado geralmente resulta em código incorreto. Para aplicativos que cookie exigem s, considere:

  • Desabilitando o tratamento cookie automático
  • Evitando IHttpClientFactory

Chame ConfigurePrimaryHttpMessageHandler para desabilitar o tratamento cookie automático:

services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });

Registro em log

Os clientes criado pelo IHttpClientFactory registram mensagens de log para todas as solicitações. Habilita o nível de informações apropriado na configuração de log para ver as mensagens de log padrão. Os registros em log adicionais, como o registro em log dos cabeçalhos de solicitação, estão incluídos somente no nível de rastreamento.

A categoria de log usada para cada cliente inclui o nome do cliente. Um cliente chamado MyNamedClient, por exemplo, registra mensagens com uma categoria de "System.Net.Http.HttpClient. MyNamedClient. LogicalHandler". Mensagens sufixadas com LogicalHandler ocorrem fora do pipeline do manipulador de solicitações. Na solicitação, as mensagens são registradas em log antes que qualquer outro manipulador no pipeline a tenha processado. Na resposta, as mensagens são registradas depois que qualquer outro manipulador do pipeline tenha recebido a resposta.

O registro em log também ocorre dentro do pipeline do manipulador de solicitações. No exemplo MyNamedClient, essas mensagens são registradas com a categoria de log "System.Net.Http.HttpClient. MyNamedClient. ClientHandler". Para a solicitação, isso ocorre depois que todos os outros manipuladores são executados e imediatamente antes que a solicitação seja enviada. Na resposta, esse registro em log inclui o estado da resposta antes que ela seja retornada por meio do pipeline do manipulador.

Habilitar o registro em log dentro e fora do pipeline permite a inspeção das alterações feitas por outros manipuladores do pipeline. Isso pode incluir alterações nos headers de solicitação ou no código de status de resposta.

Incluir o nome do cliente na categoria de log permite a filtragem de log para clientes nomeados específicos.

Configurar o HttpMessageHandler

Talvez seja necessário controlar a configuração do HttpMessageHandler interno usado por um cliente.

Um IHttpClientBuilder é retornado ao adicionar clientes nomeados ou com tipo. O método de extensão ConfigurePrimaryHttpMessageHandler pode ser usado para definir um representante. O representante que é usado para criar e configurar o HttpMessageHandler primário usado pelo cliente:

public void ConfigureServices(IServiceCollection services)
{            
    services.AddHttpClient("configured-inner-handler")
        .ConfigurePrimaryHttpMessageHandler(() =>
        {
            return new HttpClientHandler()
            {
                AllowAutoRedirect = false,
                UseDefaultCredentials = true
            };
        });

    // Remaining code deleted for brevity.

Usar IHttpClientFactory em um aplicativo de console

Em um aplicativo de console, adicione as seguintes referências de pacote ao projeto:

No exemplo a seguir:

  • IHttpClientFactory é registrado no contêiner de serviço do Host genérico.
  • MyService cria uma instância de fábrica do cliente do serviço, que é usada para criar um HttpClient. HttpClient é usado para recuperar uma página da Web.
  • Main cria um escopo para executar o método GetPage do serviço e gravar os primeiros 500 caracteres do conteúdo da página da Web no console.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}

Middleware de propagação de header

A propagação de cabeçalho é um ASP.NET Core para propagar cabeçalhos HTTP da solicitação de entrada para as solicitações de cliente HTTP de saída. Para usar a propagação de header:

  • Consulte o pacote Microsoft.AspNetCore.HeaderPropagation.

  • Configure o middleware e HttpClient em Startup :

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
        services.AddHeaderPropagation(options =>
        {
            options.Headers.Add("X-TraceId");
        });
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseHttpsRedirection();
    
        app.UseHeaderPropagation();
    
        app.UseRouting();
    
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    
  • O cliente inclui os headers configurados em solicitações de saída:

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

Recursos adicionais

Por Kirk Ltd, Steve Gordon, Gordon Condrone Ryan Nowak.

É possível registrar e usar um IHttpClientFactory para configurar e criar instâncias de HttpClient em um aplicativo. IHttpClientFactory oferece os seguintes benefícios:

  • Fornece um local central para nomear e configurar instâncias lógicas de HttpClient. Por exemplo, um cliente chamado github pode ser registrado e configurado para acessar GitHub. Um cliente padrão pode ser registrado para acesso geral.
  • Codifica o conceito de middleware de saída por meio da delegação de manipuladores no HttpClient . Fornece extensões para middleware baseado em Polly para aproveitar a delegação de manipuladores no HttpClient .
  • Gerencia o pooling e o tempo de vida das HttpClientMessageHandler instâncias subjacentes. O gerenciamento automático evita problemas comuns de DNS (Sistema de Nomes de Domínio) que ocorrem ao gerenciar manualmente os HttpClient tempos de vida.
  • Adiciona uma experiência de registro em log configurável (via ILogger) para todas as solicitações enviadas por meio de clientes criados pelo alocador.

Exibir ou baixar o código de exemplo ( comobaixar).

O código de exemplo nesta versão de tópico System.Text.Json usa para desseerializar o conteúdo JSON retornado em respostas HTTP. Para exemplos que usam Json.NET e , use o ReadAsAsync<T> seletor de versão para selecionar uma versão 2.x deste tópico.

Padrões de consumo

Há várias maneiras de usar o IHttpClientFactory em um aplicativo:

A melhor abordagem depende dos requisitos do aplicativo.

Uso básico

IHttpClientFactory pode ser registrado chamando AddHttpClient :

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient();
        // Remaining code deleted for brevity.

Um IHttpClientFactory pode ser solicitado usando a DI (injeção de dependência). O código a seguir IHttpClientFactory usa para criar uma HttpClient instância:

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            Branches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(responseStream);
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }
    }
}

Usar IHttpClientFactory como no exemplo anterior é uma boa maneira defasar um aplicativo existente. Ele não tem nenhum impacto sobre como HttpClient é usado. Em locais em HttpClient que as instâncias são criadas em um aplicativo existente, substitua essas ocorrências por chamadas para CreateClient .

Clientes nomeados

Clientes nomeados são uma boa opção quando:

  • O aplicativo requer muitos usos distintos de HttpClient .
  • Muitos HttpClient s têm configurações diferentes.

A configuração de um HttpClient nomeado pode ser especificada durante o registro em Startup.ConfigureServices :

services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

No código anterior, o cliente está configurado com:

  • O endereço base https://api.github.com/ .
  • Dois headers necessários para trabalhar com a API GitHub.

CreateClient

Cada vez CreateClient é chamado:

  • Uma nova instância do HttpClient é criada.
  • A ação de configuração é chamada.

Para criar um cliente nomeado, passe seu nome para CreateClient :

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            PullRequests = await JsonSerializer.DeserializeAsync
                    <IEnumerable<GitHubPullRequest>>(responseStream);
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

No código anterior, a solicitação não precisa especificar um nome do host. O código pode passar apenas o caminho, pois o endereço base configurado para o cliente é usado.

Clientes com tipo

Clientes com tipo:

  • Fornecem as mesmas funcionalidade que os clientes nomeados sem a necessidade de usar cadeias de caracteres como chaves.
  • Fornecem a ajuda do IntelliSense e do compilador durante o consumo de clientes.
  • Fornecem um único local para configurar e interagir com um determinado HttpClient. Por exemplo, um único cliente com tipo pode ser usado:
    • Para um único ponto de extremidade de back-end.
    • Para encapsular toda a lógica que lida com o ponto de extremidade.
  • Trabalhe com DI e possa ser injetado quando necessário no aplicativo.

Um cliente digitado aceita um HttpClient parâmetro em seu construtor:

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept",
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent",
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        var response = await Client.GetAsync(
            "/repos/dotnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<GitHubIssue>>(responseStream);
    }
}

Se você quiser ver comentários de código traduzidos para idiomas diferentes do inglês, nos avise neste problema de discussão do GitHub.

No código anterior:

  • A configuração é movida para o cliente digitado.
  • O objeto HttpClient é exposto como uma propriedade pública.

Métodos específicos da API podem ser criados que HttpClient expõem a funcionalidade. Por exemplo, o GetAspNetDocsIssues método encapsula o código para recuperar problemas abertos.

O código a seguir AddHttpClient chama para registrar uma classe de cliente Startup.ConfigureServices digitada:

services.AddHttpClient<GitHubService>();

O cliente com tipo é registrado como transitório com a DI. No código anterior, AddHttpClient registra-se GitHubService como um serviço transitório. Esse registro usa um método de fábrica para:

  1. Crie uma instância de HttpClient.
  2. Crie uma instância GitHubService de , passando a instância de para seu HttpClient construtor.

O cliente com tipo pode ser injetado e consumido diretamente:

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

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

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}

A configuração de um cliente digitado pode ser especificada durante o registro no , em vez de no Startup.ConfigureServices construtor do cliente digitado:

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

O HttpClient pode ser encapsulado dentro de um cliente digitado. Em vez de expô-lo como uma propriedade, defina um método que chama a HttpClient instância internamente:

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<string>>(responseStream);
    }
}

No código anterior, o HttpClient é armazenado em um campo privado. O acesso ao HttpClient é pelo método GetRepos público.

Clientes gerados

IHttpClientFactory pode ser usado em combinação com bibliotecas de terceiros, como Reajuste. Refit é uma biblioteca REST para .NET. Ela converte APIs REST em interfaces dinâmicas. Uma implementação da interface é gerada dinamicamente pelo RestService usando HttpClient para fazer as chamadas de HTTP externas.

Uma interface e uma resposta são definidas para representar a API externa e sua resposta:

public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}

Um cliente com tipo pode ser adicionado usando a Refit para gerar a implementação:

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddControllers();
}

A interface definida pode ser consumida, quando necessário, com a implementação fornecida pela DI e pela Refit:

[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();
    }
}

Fazer solicitações POST, PUT e DELETE

Nos exemplos anteriores, todas as solicitações HTTP usam o verbo GET HTTP. HttpClient também dá suporte a outros verbos HTTP, incluindo:

  • POST
  • PUT
  • DELETE
  • PATCH

Para ver uma lista completa dos verbos HTTP com suporte, consulte HttpMethod .

O exemplo a seguir mostra como fazer uma solicitação HTTP POST:

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem, _jsonSerializerOptions),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

No código anterior, o CreateItemAsync método :

  • Serializa o parâmetro TodoItem para JSON usando System.Text.Json . Isso usa uma instância do JsonSerializerOptions para configurar o processo de serialização.
  • Cria uma instância de StringContent para empacotar o JSON serializado para enviar no corpo da solicitação HTTP.
  • Chama PostAsync para enviar o conteúdo JSON para a URL especificada. Essa é uma URL relativa que é adicionada ao HttpClient.BaseAddress.
  • Chama EnsureSuccessStatusCode para lançar uma exceção se o código de status de resposta não indicar êxito.

HttpClient também dá suporte a outros tipos de conteúdo. Por exemplo, MultipartContent e StreamContent. Para ver uma lista completa do conteúdo com suporte, consulte HttpContent .

O exemplo a seguir mostra uma solicitação HTTP PUT:

public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

O código anterior é muito semelhante ao exemplo POST. O SaveItemAsync método chama em vez de PutAsync PostAsync .

O exemplo a seguir mostra uma solicitação HTTP DELETE:

public async Task DeleteItemAsync(long itemId)
{
    using var httpResponse =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    httpResponse.EnsureSuccessStatusCode();
}

No código anterior, o método DeleteItemAsync chama DeleteAsync . Como as solicitações HTTP DELETE normalmente não contêm nenhum corpo, o método não fornece uma sobrecarga DeleteAsync que aceita uma instância do HttpContent .

Para saber mais sobre como usar diferentes verbos HTTP com HttpClient , consulte HttpClient .

Middleware de solicitação de saída

HttpClient tem o conceito de delegar manipuladores que podem ser vinculados em conjunto para solicitações HTTP de saída. IHttpClientFactory:

  • Simplifica a definição dos manipuladores a aplicar a cada cliente nomeado.

  • Dá suporte ao registro e encadeamento de vários manipuladores para criar um pipeline de middleware de solicitação de saída. Cada um desses manipuladores é capaz de executar o trabalho antes e depois da solicitação de saída. Esse padrão:

    • É semelhante ao pipeline de middleware de entrada no ASP.NET Core.
    • Fornece um mecanismo para gerenciar preocupações abrangentes em relação a solicitações HTTP, como:
      • cache
      • tratamento de erros
      • serialização
      • registro em log

Para criar um manipulador de delegação:

  • Derive de DelegatingHandler .
  • Substitua SendAsync. Execute o código antes de passar a solicitação para o próximo manipulador no pipeline:
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

O código anterior verifica se X-API-KEY o header está na solicitação. Se X-API-KEY estiver ausente, BadRequest será retornado.

Mais de um manipulador pode ser adicionado à configuração de um HttpClient com Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler :

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ValidateHeaderHandler>();

    services.AddHttpClient("externalservice", c =>
    {
        // Assume this is an "external" service which requires an API KEY
        c.BaseAddress = new Uri("https://localhost:5001/");
    })
    .AddHttpMessageHandler<ValidateHeaderHandler>();

    // Remaining code deleted for brevity.

No código anterior, o ValidateHeaderHandler é registrado com a DI. Depois de registrado, o AddHttpMessageHandler poderá ser chamado, passando o tipo para o manipulador.

Vários manipuladores podem ser registrados na ordem em que eles devem ser executados. Cada manipulador encapsula o próximo manipulador até que o HttpClientHandler final execute a solicitação:

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

Usar DI no middleware de solicitação de saída

Quando IHttpClientFactory cria um novo manipulador de delegação, ele usa a DI para atender aos parâmetros do construtor do manipulador. IHttpClientFactorycria um escopo de DI separado para cada manipulador, o que pode levar a um comportamento surpreendente quando um manipulador consome um serviço com escopo.

Por exemplo, considere a interface a seguir e sua implementação, que representa uma tarefa como uma operação com um identificador, OperationId :

public interface IOperationScoped 
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

Como seu nome sugere, é IOperationScoped registrado com DI usando um tempo de vida com escopo:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(options =>
        options.UseInMemoryDatabase("TodoItems"));

    services.AddHttpContextAccessor();

    services.AddHttpClient<TodoClient>((sp, httpClient) =>
    {
        var httpRequest = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request;

        // For sample purposes, assume TodoClient is used in the context of an incoming request.
        httpClient.BaseAddress = new Uri(UriHelper.BuildAbsolute(httpRequest.Scheme,
                                         httpRequest.Host, httpRequest.PathBase));
        httpClient.Timeout = TimeSpan.FromSeconds(5);
    });

    services.AddScoped<IOperationScoped, OperationScoped>();
    
    services.AddTransient<OperationHandler>();
    services.AddTransient<OperationResponseHandler>();

    services.AddHttpClient("Operation")
        .AddHttpMessageHandler<OperationHandler>()
        .AddHttpMessageHandler<OperationResponseHandler>()
        .SetHandlerLifetime(TimeSpan.FromSeconds(5));

    services.AddControllers();
    services.AddRazorPages();
}

O manipulador de delegação a seguir consome e IOperationScoped usa para definir o X-OPERATION-ID header para a solicitação de saída:

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationService;

    public OperationHandler(IOperationScoped operationScoped)
    {
        _operationService = operationScoped;
    }

    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationService.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}

No HttpRequestsSample download], navegue até /Operation e atualize a página. O valor do escopo da solicitação muda para cada solicitação, mas o valor do escopo do manipulador só muda a cada 5 segundos.

Os manipuladores podem depender de serviços de qualquer escopo. Os serviços dos quais os manipuladores dependem são descartados quando o manipulador é descartado.

Use uma das seguintes abordagens para compartilhar o estado por solicitação com os manipuladores de mensagens:

Usar manipuladores baseados no Polly

IHttpClientFactory integra-se à biblioteca de terceiros Polly. O Polly é uma biblioteca abrangente de tratamento de falha transitória e de resiliência para .NET. Ela permite que os desenvolvedores expressem políticas, como Repetição, Disjuntor, Tempo Limite, Isolamento de Bulkhead e Fallback de maneira fluente e thread-safe.

Os métodos de extensão são fornecidos para habilitar o uso de políticas do Polly com instâncias de HttpClient configuradas. As extensões do Polly são suportadas pela adição de manipuladores baseados em Polly aos clientes. A Polly requer o pacote Microsoft.Extensions.Http.Polly NuGet pacote.

Tratar falhas transitórias

Normalmente, ocorrem falhas quando chamadas HTTP externas são transitórias. AddTransientHttpErrorPolicy permite que uma política seja definida para tratar erros transitórios. As políticas configuradas com AddTransientHttpErrorPolicy lidam com as seguintes respostas:

AddTransientHttpErrorPolicy fornece acesso a um PolicyBuilder objeto configurado para tratar erros que representam uma possível falha transitória:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient<UnreliableEndpointCallerService>()
        .AddTransientHttpErrorPolicy(p => 
            p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

    // Remaining code deleted for brevity.

No código anterior, uma política WaitAndRetryAsync é definida. As solicitações com falha são repetidas até três vezes com um atraso de 600 ms entre as tentativas.

Selecionar políticas dinamicamente

Métodos de extensão são fornecidos para adicionar manipuladores baseados em Polly, por exemplo, AddPolicyHandler . A sobrecarga AddPolicyHandler a seguir inspeciona a solicitação para decidir qual política aplicar:

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

No código anterior, se a solicitação de saída é um HTTP GET, um tempo limite de 10 segundos é aplicado. Para qualquer outro método HTTP, é usado um tempo limite de 30 segundos.

Adicionar vários manipuladores do Polly

É comum aninhar políticas do Polly:

services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

No exemplo anterior:

  • Dois manipuladores são adicionados.
  • O primeiro manipulador usa AddTransientHttpErrorPolicy para adicionar uma política de nova tentativa. As solicitações com falha são repetidas até três vezes.
  • A segunda AddTransientHttpErrorPolicy chamada adiciona uma política de disjuntor. Outras solicitações externas serão bloqueadas por 30 segundos se 5 tentativas com falha ocorrerem sequencialmente. As políticas de disjuntor são políticas com estado. Todas as chamadas por meio desse cliente compartilham o mesmo estado do circuito.

Adicionar políticas do registro do Polly

Uma abordagem para gerenciar as políticas usadas com frequência é defini-las uma única vez e registrá-las em um PolicyRegistry.

No seguinte código:

public void ConfigureServices(IServiceCollection services)
{           
    var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(10));
    var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(30));
    
    var registry = services.AddPolicyRegistry();

    registry.Add("regular", timeout);
    registry.Add("long", longTimeout);
    
    services.AddHttpClient("regularTimeoutHandler")
        .AddPolicyHandlerFromRegistry("regular");

    services.AddHttpClient("longTimeoutHandler")
       .AddPolicyHandlerFromRegistry("long");

    // Remaining code deleted for brevity.

Para obter mais informações sobre IHttpClientFactory as integrações do e do Polly, consulte o wiki da Polly.

HttpClient e gerenciamento de tempo de vida

Uma nova instância de HttpClient é chamada, sempre que CreateClient é chamado na IHttpClientFactory. Um HttpMessageHandler é criado por cliente nomeado. O alocador gerencia a vida útil das instâncias do HttpMessageHandler.

IHttpClientFactory cria pools com as instâncias de HttpMessageHandler criadas pelo alocador para reduzir o consumo de recursos. Uma instância de HttpMessageHandler poderá ser reutilizada no pool, ao criar uma nova instância de HttpClient, se o respectivo tempo de vida não tiver expirado.

O pooling de manipuladores é preferível porque normalmente cada manipulador gerencia as próprias conexões HTTP subjacentes. Criar mais manipuladores do que o necessário pode resultar em atrasos de conexão. Alguns manipuladores também mantêm as conexões abertas indefinidamente, o que pode impedir que o manipulador reaja a alterações de DNS (Sistema de Nomes de Domínio).

O tempo de vida padrão do manipulador é de 2 minutos. O valor padrão pode ser substituído por cliente nomeado:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient("extendedhandlerlifetime")
        .SetHandlerLifetime(TimeSpan.FromMinutes(5));

    // Remaining code deleted for brevity.

HttpClient as instâncias geralmente podem ser tratadas como objetos .NET que não exigem descarte. O descarte cancela as solicitações de saída e garante que a determinada instância de HttpClient não seja usada depois de chamar Dispose. IHttpClientFactory rastreia e descarta recursos usados pelas instâncias de HttpClient.

Manter uma única instância de HttpClient ativa por uma longa duração é um padrão comum usado, antes do início de IHttpClientFactory. Esse padrão se torna desnecessário após a migração para IHttpClientFactory.

Alternativas para IHttpClientFactory

IHttpClientFactoryO uso do em um aplicativo habilitado para di evita:

  • Problemas de esgotamento de recursos por instâncias de Pooling HttpMessageHandler .
  • Problemas de DNS obsoletos por HttpMessageHandler instâncias de ciclo em intervalos regulares.

Há maneiras alternativas de resolver os problemas anteriores usando uma instância de vida longa SocketsHttpHandler .

  • Crie uma instância do SocketsHttpHandler quando o aplicativo for iniciado e use-o para a vida útil do aplicativo.
  • Configure PooledConnectionLifetime para um valor apropriado com base em tempos de atualização de DNS.
  • Crie HttpClient instâncias usando new HttpClient(handler, disposeHandler: false) conforme necessário.

As abordagens anteriores resolvem os problemas de gerenciamento de recursos que são IHttpClientFactory resolvidos de forma semelhante.

  • O SocketsHttpHandler compartilha conexões entre HttpClient instâncias. Esse compartilhamento impede o esgotamento de soquete.
  • O SocketsHttpHandler ciclos se conexões de acordo com o PooledConnectionLifetime para evitar problemas de DNS obsoletos.

Cookies

As HttpMessageHandler instâncias em pool resultam em CookieContainer objetos que estão sendo compartilhados. O CookieContainer compartilhamento de objeto inesperado geralmente resulta em código incorreto. Para aplicativos que exigem cookie s, considere o:

  • Desabilitando a cookie manipulação automática
  • Evitá IHttpClientFactory

Chamada ConfigurePrimaryHttpMessageHandler para desabilitar a cookie manipulação automática:

services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });

Registro em log

Os clientes criado pelo IHttpClientFactory registram mensagens de log para todas as solicitações. Habilite o nível de informações apropriado na configuração de log para ver as mensagens de log padrão. Os registros em log adicionais, como o registro em log dos cabeçalhos de solicitação, estão incluídos somente no nível de rastreamento.

A categoria de log usada para cada cliente inclui o nome do cliente. Um cliente chamado MyNamedClient, por exemplo, registra mensagens com uma categoria de "System .net. http. HttpClient. MyNamedClient. LogicalHandler". Mensagens sufixadas com LogicalHandler ocorrem fora do pipeline do manipulador de solicitações. Na solicitação, as mensagens são registradas em log antes que qualquer outro manipulador no pipeline a tenha processado. Na resposta, as mensagens são registradas depois que qualquer outro manipulador do pipeline tenha recebido a resposta.

O registro em log também ocorre dentro do pipeline do manipulador de solicitações. No exemplo de MyNamedClient , essas mensagens são registradas com a categoria de log "System .net. http. HttpClient. MyNamedClient. ClientHandler". Para a solicitação, isso ocorre depois que todos os outros manipuladores forem executados e imediatamente antes de a solicitação ser enviada. Na resposta, esse registro em log inclui o estado da resposta antes que ela seja retornada por meio do pipeline do manipulador.

Habilitar o registro em log dentro e fora do pipeline permite a inspeção das alterações feitas por outros manipuladores do pipeline. Isso pode incluir alterações nos cabeçalhos de solicitação ou no código de status de resposta.

A inclusão do nome do cliente na categoria de log permite a filtragem de log para clientes nomeados específicos.

Configurar o HttpMessageHandler

Talvez seja necessário controlar a configuração do HttpMessageHandler interno usado por um cliente.

Um IHttpClientBuilder é retornado ao adicionar clientes nomeados ou com tipo. O método de extensão ConfigurePrimaryHttpMessageHandler pode ser usado para definir um representante. O representante que é usado para criar e configurar o HttpMessageHandler primário usado pelo cliente:

public void ConfigureServices(IServiceCollection services)
{            
    services.AddHttpClient("configured-inner-handler")
        .ConfigurePrimaryHttpMessageHandler(() =>
        {
            return new HttpClientHandler()
            {
                AllowAutoRedirect = false,
                UseDefaultCredentials = true
            };
        });

    // Remaining code deleted for brevity.

Usar IHttpClientFactory em um aplicativo de console

Em um aplicativo de console, adicione as seguintes referências de pacote ao projeto:

No exemplo a seguir:

  • IHttpClientFactory é registrado no contêiner de serviço do Host genérico.
  • MyService cria uma instância de fábrica do cliente do serviço, que é usada para criar um HttpClient. HttpClient é usado para recuperar uma página da Web.
  • Main cria um escopo para executar o método GetPage do serviço e gravar os primeiros 500 caracteres do conteúdo da página da Web no console.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}

Middleware de propagação de cabeçalho

a propagação de cabeçalho é um middleware ASP.NET Core para propagar cabeçalhos http da solicitação de entrada para as solicitações de cliente HTTP de saída. Para usar a propagação de cabeçalho:

  • Referencie o pacote Microsoft. AspNetCore. HeaderPropagation .

  • Configure o middleware e HttpClient em Startup :

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
        services.AddHeaderPropagation(options =>
        {
            options.Headers.Add("X-TraceId");
        });
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseHttpsRedirection();
    
        app.UseHeaderPropagation();
    
        app.UseRouting();
    
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    
  • O cliente inclui os cabeçalhos configurados nas solicitações de saída:

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

Recursos adicionais