Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 50

Typed HTTP Clients with Aspire Service References

Recommended resources

Olá! Bem-vindo de volta.

Na lição anterior, desvendamos como o .NET Aspire resolve o problema fundamental de "onde está o serviço?" por meio do seu mecanismo de descoberta de serviços. Vimos que, ao usar WithReference() no AppHost, podemos nos comunicar entre serviços usando nomes lógicos (como http://products-api), eliminando a necessidade de URLs fixas e frágeis.

Hoje, vamos dar o próximo passo lógico. Agora que sabemos endereçar os serviços, qual é a melhor maneira de escrever o código que realiza essa comunicação? Esta lição mergulha em uma das melhores práticas do desenvolvimento .NET moderno: os Typed HTTP Clients. Nosso objetivo de aprendizagem é configurar e injetar HttpClients tipados para comunicação entre serviços, usando os padrões de referência de serviço do Aspire. Ao final, você saberá como construir um código de comunicação entre serviços que não é apenas funcional, mas também robusto, manutenível e fácil de testar.

Por que a Simples Instanciação de HttpClient é Problemática?

Antes de entrarmos na solução, é crucial entender o problema que ela resolve. Sua experiência como desenvolvedor back-end pode já tê-lo exposto aos desafios do uso incorreto do HttpClient. A abordagem ingênua seria:

// Abordagem problemática
using var client = new HttpClient();
var result = await client.GetStringAsync("http://service-b/data");

Embora pareça inofensivo, este padrão, especialmente dentro de um loop ou em um endpoint de API muito requisitado, pode levar a dois problemas clássicos:

  1. Esgotamento de Sockets (Socket Exhaustion): Cada new HttpClient() pode reservar um novo socket. Mesmo com o bloco using, o sistema operacional leva um tempo para liberar esse socket. Sob carga pesada, você pode esgotar o pool de sockets disponíveis, resultando em exceções SocketException.
  2. Não Respeitar Alterações de DNS: A alternativa de usar uma instância estática ou singleton de HttpClient (static HttpClient client = new HttpClient();) resolve o esgotamento de sockets, mas cria outro problema. Essa instância única armazena em cache as informações de DNS indefinidamente e não detectará alterações se, por exemplo, um serviço se mover para um novo endereço IP.

Para entender a fundo esses problemas, a documentação da Microsoft oferece uma excelente explicação.

Use IHttpClientFactory to implement resilient HTTP requests

Este documento da Microsoft detalha os problemas históricos com a classe HttpClient e introduz a solução moderna, IHttpClientFactory.

Leia a seção "Issues with the original HttpClient class available in .NET". O objetivo é solidificar o seu entendimento sobre o 'porquê' de precisarmos de um padrão melhor do que simplesmente instanciar HttpClient diretamente.


A Solução .NET: IHttpClientFactory e Typed Clients

Para resolver esses dilemas, o .NET introduziu a IHttpClientFactory. Esta fábrica gerencia o ciclo de vida dos HttpMessageHandlers (os objetos que realmente lidam com as conexões HTTP), agrupando-os e reciclando-os de forma inteligente. Isso resolve ambos os problemas: evita o esgotamento de sockets ao reutilizar handlers e respeita as alterações de DNS ao reciclá-los periodicamente.

Existem várias maneiras de usar a IHttpClientFactory, mas a mais estruturada e recomendada para comunicação entre serviços é o padrão Typed Client.

Um Typed Client é uma classe que encapsula toda a lógica de comunicação com uma API remota específica. Em vez de espalhar HttpClient.GetAsync(...) por todo o seu código, você centraliza tudo em um só lugar.

Benefícios de um Typed Client:

  • Encapsulamento: A lógica para construir URLs, serializar/desserializar objetos e lidar com headers fica contida em uma única classe.
  • Forte Tipagem: Os métodos retornam objetos fortemente tipados (ex: Task<Product>) em vez de HttpResponseMessage, tornando o código do consumidor mais limpo.
  • Configuração Centralizada: Você pode configurar o BaseAddress, timeouts e políticas de resiliência (que veremos mais tarde) em um único local (Program.cs).
  • Testabilidade: É muito mais fácil de mocar (mock) uma interface como IProductsApiClient em seus testes unitários do que mocar um HttpClient.

Veja a aparência de um Typed Client:

Using Typed HttpClient in .NET 7 Web API
Nesta imagem, a classe `ExternalProductsService` é um exemplo de Typed Client. Note como ela recebe um `HttpClient` via injeção de dependência em seu construtor e o utiliza para encapsular a chamada à API externa.

O documento da Microsoft que você acabou de ler também descreve este padrão em detalhes.

Use IHttpClientFactory to implement resilient HTTP requests

Vamos continuar na documentação da Microsoft para ver como o padrão Typed Client é implementado.

Agora, leia as seções "How to use Typed Clients with IHttpClientFactory" e "Implement your Typed Client classes that use the injected and configured HttpClient". Preste atenção em como o Typed Client é uma classe normal que recebe um HttpClient no construtor e como ela é registrada no contêiner de injeção de dependência.


Unindo Tudo: Typed Clients com Service Discovery do Aspire

Agora, vamos conectar o padrão Typed Client com o que aprendemos na lição anterior sobre o service discovery do Aspire. A "mágica" acontece no momento em que registramos nosso Typed Client no Program.cs do serviço consumidor.

Considere um AppHost com um serviço products-api e um store-frontend que o consome:

// No projeto AppHost
var productsApi = builder.AddProject<Projects.ProductsApi>("products-api");
var storeFrontend = builder.AddProject<Projects.StoreFrontend>("store-frontend")
                         .WithReference(productsApi);

No projeto store-frontend, criamos nossa classe ProductsApiClient. Em seguida, no Program.cs do store-frontend, nós a registramos da seguinte forma:

// No Program.cs do store-frontend

// 1. Adicionamos o nosso Typed Client ao contêiner de DI.
builder.Services.AddHttpClient<ProductsApiClient>(client =>
{
    // 2. Aqui está a conexão! Usamos o nome lógico do serviço
    //    definido no AppHost como o BaseAddress.
    client.BaseAddress = new Uri("https+http://products-api");
});

É isso! Ao fazer isso, você está instruindo a IHttpClientFactory a:

  1. Criar instâncias de HttpClient para o seu ProductsApiClient.
  2. Configurar cada instância com um endereço base que será resolvido dinamicamente pelo mecanismo de service discovery do Aspire.

O vídeo a seguir demonstra de forma prática a evolução de uma abordagem menos estruturada para um Typed Client e, finalmente, sua integração com o .NET Aspire para eliminar completamente as URLs fixas.

5 HttpClient Best Practices Every .NET Dev Should Know

Este vídeo de Milan Jovanović sobre as melhores práticas do HttpClient é perfeito para o nosso tópico. Ele o guiará na refatoração de um código para usar um Typed Client e, em seguida, conectá-lo ao service discovery do Aspire.

Assista em duas partes: Primeiro, assista de 00:04:00 a 00:08:13. Observe como ele cria a classe GitHubClient para encapsular a lógica HTTP. Isso é a criação do Typed Client. Em seguida, assista de 00:09:59 a 00:13:28. Preste atenção em como ele modifica a configuração no AppHost e no registro do HttpClient para usar um nome de serviço lógico em vez de uma URL hard-coded. Esta é a integração com o Aspire.

O fluxo completo, unindo a lição anterior e esta, é o seguinte:

.NET Aspire Service Discovery Flow
Este diagrama, que já vimos, resume o processo. O passo 4 (`Create HTTP Client`) é onde nosso Typed Client é instanciado. O passo 5 (`Resolve API URL`) é realizado pela `IHttpClientFactory` em conjunto com os `ServiceDefaults` do Aspire, usando o nome lógico que definimos no `BaseAddress`.
Teste seu entendimento!

Você tem um projeto AppHost que define dois serviços: users-api e notifications-api. O notifications-api precisa chamar o users-api.

AppHost Program.cs:

var usersApi = builder.AddProject<Projects.UsersApi>("users-api");
builder.AddProject<Projects.NotificationsApi>("notifications-api")
       .WithReference(usersApi);

Você criou um Typed Client UsersApiClient no projeto NotificationsApi. Qual trecho de código você deve adicionar ao Program.cs do NotificationsApi para registrar corretamente o UsersApiClient e conectá-lo ao service discovery do Aspire?

a) builder.Services.AddSingleton<UsersApiClient>();
b) builder.Services.AddHttpClient<UsersApiClient>(c => c.BaseAddress = new Uri(builder.Configuration["Services:UsersApi:Http:0"]));
c) builder.Services.AddHttpClient<UsersApiClient>(c => c.BaseAddress = new Uri("https+http://users-api"));
d) builder.Services.AddScoped<UsersApiClient>(sp => new UsersApiClient(new HttpClient()));

Show answer

A resposta correta é a (c). Este é o padrão exato que aprendemos:

  • AddHttpClient<UsersApiClient> registra o Typed Client e configura a injeção do HttpClient.
  • c.BaseAddress = new Uri("https+http://users-api") usa o nome lógico do serviço (users-api) definido no AppHost. O Aspire e a IHttpClientFactory cuidarão da resolução do endereço real em tempo de execução.

A opção (b) é incorreta porque tenta ler a variável de ambiente diretamente, o que é um detalhe de implementação que o padrão de service discovery abstrai para nós.


Conclusão

Nesta lição, você aprendeu a implementar a comunicação entre serviços de uma maneira robusta e padronizada no ecossistema .NET e Aspire. Ao combinar o padrão Typed Client com o service discovery do Aspire, ganhamos o melhor dos dois mundos: um código cliente limpo, encapsulado e testável, que se comunica de forma transparente com serviços cujos endereços são gerenciados dinamicamente.

Principais Pontos:

  • Evite new HttpClient() para prevenir problemas de esgotamento de sockets e cache de DNS.
  • Use o padrão Typed Client para encapsular a lógica de comunicação com APIs externas, tornando seu código mais limpo e fácil de manter.
  • Registre seus Typed Clients usando builder.Services.AddHttpClient<TClient>().
  • Para integrar com o Aspire, configure o BaseAddress do cliente para o nome lógico do serviço de destino (ex: https+http://meu-servico).

Com este conhecimento, você está agora totalmente equipado para construir a camada de comunicação de suas aplicações distribuídas seguindo as melhores práticas da indústria.

Próxima lição:
Até agora, assumimos que cada serviço expõe apenas um endpoint HTTP. No entanto, um serviço pode precisar expor múltiplas portas para diferentes propósitos (ex: uma porta para HTTP e outra para gRPC). Na próxima lição, aprenderemos a usar endpoints nomeados para expor e consumir múltiplas portas de um único serviço, adicionando ainda mais flexibilidade à nossa arquitetura.