.NET Aspire: criando aplicativos nativos da nuvem da maneira certa
Se você já criou um aplicativo distribuído em .NET, você sabe o que fazer. Você cria uma API Web, adiciona um trabalhador em segundo plano, adiciona Redis para armazenamento em cache, PostgreSQL para persistência, talvez RabbitMQ para mensagens - e de repente você está gastando mais tempo conectando infraestrutura do que escrevendo lógica de negócios. Cadeias de conexão espalhadas por arquivos appsettings.json, verificações de integridade que você esqueceu de configurar, observabilidade que é sempre o “problema do próximo sprint”.
Eu estive lá. Mais vezes do que eu gostaria de admitir.
Esse é exatamente o problema para o qual o .NET Aspire foi projetado para resolver. Depois de executá-lo em produção por vários meses, quero compartilhar o que aprendi - o que é bom, o que é ótimo e as pegadinhas.
O que é o .NET Aspire?
O .NET Aspire é uma pilha opinativa para a construção de aplicativos distribuídos, observáveis e prontos para produção com o .NET. Não é uma estrutura no sentido tradicional — ela não substitui o ASP.NET Core nem força você a adotar um novo modelo de programação. Em vez disso, ele se baseia no que você já sabe e preenche as lacunas que sempre existiram na criação de aplicativos nativos da nuvem.
Basicamente, o Aspire oferece quatro coisas:
- AppHost — Um projeto que define toda a topologia de seu aplicativo distribuído. Quais serviços existem, de que dependem e como se conectam.
- Padrões de serviço — Um projeto compartilhado que configura preocupações transversais, como verificações de integridade, políticas de resiliência e OpenTelemetry — uma vez, para todos os seus serviços.
- Componentes — pacotes NuGet que fornecem integrações padronizadas com serviços de apoio como Redis, PostgreSQL, RabbitMQ, Azure Storage e muito mais.
- Painel do desenvolvedor — Uma IU em tempo real que mostra logs, rastreamentos e métricas de todo o seu aplicativo distribuído durante o desenvolvimento local.
A filosofia é simples: se todo aplicativo em nuvem .NET precisa dessas coisas, por que estamos todos implementando-as do zero sempre?
Configurando seu primeiro projeto Aspire
Começar é simples. Você precisará do .NET 8 ou posterior e da carga de trabalho do Aspire instalada:
dotnet workload update
dotnet workload install aspire
Agora crie um novo projeto inicial do Aspire:
dotnet new aspire-starter -n MyCloudApp
Isso gera uma solução com quatro projetos:
MyCloudApp.AppHost— O orquestradorMyCloudApp.ServiceDefaults— Configuração compartilhadaMyCloudApp.ApiService— Um exemplo de API da WebMyCloudApp.Web— Uma interface do Blazor
Execute o AppHost e você verá imediatamente o painel do Aspire aberto em seu navegador, mostrando todos os seus serviços, seus logs e sua saúde. Nenhum arquivo Docker Compose. Sem gerenciamento manual de portas. Simplesmente funciona.
dotnet run --project MyCloudApp.AppHost
Essa primeira experiência foi o que me fisgou. Em menos de um minuto, você tem um aplicativo distribuído totalmente orquestrado com observabilidade integrada.
O padrão AppHost
O AppHost é onde mora a magia. É um pequeno aplicativo de console que usa um padrão de construtor para definir a topologia do seu aplicativo distribuído — quais recursos existem e como os serviços se conectam a eles.
Esta é a aparência de um AppHost realista:
var builder = DistributedApplication.CreateBuilder(args);
// Infrastructure resources
var postgres = builder.AddPostgres("postgres")
.WithPgAdmin()
.AddDatabase("catalogdb");
var redis = builder.AddRedis("cache");
var rabbitmq = builder.AddRabbitMQ("messaging")
.WithManagementPlugin();
// Application services
var catalogApi = builder.AddProject<Projects.CatalogApi>("catalog-api")
.WithReference(postgres)
.WithReference(redis)
.WithReference(rabbitmq);
var orderApi = builder.AddProject<Projects.OrderApi>("order-api")
.WithReference(postgres)
.WithReference(rabbitmq);
var frontend = builder.AddProject<Projects.WebFrontend>("frontend")
.WithExternalHttpEndpoints()
.WithReference(catalogApi)
.WithReference(orderApi);
builder.Build().Run();
```Leia esse código em voz alta. Praticamente se documenta. "A API do catálogo faz referência a PostgreSQL, Redis e RabbitMQ. O frontend faz referência à API do catálogo e à API do pedido." Essa é a sua arquitetura, expressa em código.
Algumas coisas dignas de nota:
- **`WithReference`** faz o trabalho pesado. Ele injeta automaticamente cadeias de conexão e URLs de serviço no projeto de consumo por meio de variáveis de ambiente e configuração. Seus serviços não precisam saber *onde* o Redis está sendo executado — o Aspire cuida disso.
- **`WithPgAdmin()`** e **`WithManagementPlugin()`** ativam UIs de administração para PostgreSQL e RabbitMQ juntamente com os serviços reais. Durante o desenvolvimento, eles são inestimáveis.
- **`WithExternalHttpEndpoints()`** marca um serviço como acessível externamente, o que é importante no momento da implantação.
### Configuração de recursos
Você pode configurar recursos com controle refinado:
```csharp
var postgres = builder.AddPostgres("postgres")
.WithEnvironment("POSTGRES_MAX_CONNECTIONS", "200")
.WithDataVolume("postgres-data")
.AddDatabase("catalogdb");
var redis = builder.AddRedis("cache")
.WithDataVolume("redis-data");
Os volumes de dados garantem que seus dados de desenvolvimento local sobrevivam às reinicializações do contêiner. Pequenos detalhes, grande melhoria na qualidade de vida.
Padrões de serviço: o herói desconhecido
O projeto ServiceDefaults é a parte mais subestimada do Aspire. É uma biblioteca compartilhada à qual todos os serviços da sua solução fazem referência e configura todas as preocupações transversais que você esqueceria ou implementaria de forma inconsistente.
Esta é a aparência de um Extensions.cs típico em ServiceDefaults:
public static class Extensions
{
public static IHostApplicationBuilder AddServiceDefaults(
this IHostApplicationBuilder builder)
{
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler();
http.AddServiceDiscovery();
});
return builder;
}
public static IHostApplicationBuilder ConfigureOpenTelemetry(
this IHostApplicationBuilder builder)
{
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
});
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation();
})
.WithTracing(tracing =>
{
tracing.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddGrpcClientInstrumentation();
});
builder.AddOpenTelemetryExporters();
return builder;
}
public static IHostApplicationBuilder AddDefaultHealthChecks(
this IHostApplicationBuilder builder)
{
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(),
["live"]);
return builder;
}
}
Então, em Program.cs de cada serviço, uma linha faz tudo:
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
// Your service-specific configuration...
var app = builder.Build();
app.MapDefaultEndpoints();
app.Run();
Essa única chamada AddServiceDefaults() fornece:
- OpenTelemetry com registro estruturado, métricas e rastreamento distribuído
- Verificações de integridade com pontos de extremidade de atividade e prontidão
- Descoberta de serviços para que os serviços possam se encontrar pelo nome
- Políticas de resiliência em todas as chamadas HTTP de saída (novas tentativas, disjuntores, tempos limite)
Isso é o que separa uma demonstração “funciona na minha máquina” de um sistema pronto para produção. E o Aspire torna isso o padrão, não uma reflexão tardia.
Componentes do Aspire
Os componentes Aspire são pacotes NuGet que padronizam como seus serviços se conectam à infraestrutura de apoio. Elas são mais do que apenas bibliotecas cliente: elas incluem verificações de integridade, registro, rastreamento e resiliência configurável pronta para uso.
Redis
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.AddRedisDistributedCache("cache");
É isso. A string de conexão vem do AppHost via WithReference. O componente registra um IDistributedCache apoiado por Redis, com verificações de integridade e instrumentação OpenTelemetry já instaladas.
Você também pode usar o Redis para cache de saída:
builder.AddRedisOutputCache("cache");
PostgreSQL com Entity Framework Core
builder.AddNpgsqlDbContext<CatalogDbContext>("catalogdb", settings =>
{
settings.DisableRetry = false;
});
Isso registra seu DbContext com uma conexão com o banco de dados catalogdb definido no AppHost. Inclui pool de conexões, verificações de integridade e políticas de nova tentativa.
CoelhoMQ
builder.AddRabbitMQClient("messaging");
Registra um IConnection da biblioteca cliente RabbitMQ, totalmente configurado e com integridade verificada.
Integrações Azure
O Aspire também possui componentes de primeira classe para serviços Azure:
// Azure Blob Storage
builder.AddAzureBlobClient("blobs");
// Azure Service Bus
builder.AddAzureServiceBusClient("servicebus");
// Azure Key Vault
builder.AddAzureKeyVaultClient("secrets");
```O padrão é sempre o mesmo: uma linha no AppHost para definir o recurso, uma linha no serviço consumidor para utilizá-lo. Os detalhes da conexão fluem automaticamente.
## O painel do desenvolvedor
O painel do Aspire é um daqueles recursos que parece interessante até você realmente usá-lo - então você não consegue se imaginar trabalhando sem ele.
Quando você executa seu AppHost localmente, o painel é iniciado e fornece:
- **Visão geral dos recursos** — Visão geral de todos os seus serviços e infraestrutura, com indicadores de status
- **Logs estruturados** — Streaming de logs em tempo real de todos os serviços, filtráveis e pesquisáveis
- **Rastreamentos distribuídos** — Rastreamentos de solicitação de ponta a ponta abrangendo vários serviços, visualizados como gráficos em chamas
- **Métricas** — taxas de solicitação HTTP, taxas de erro, latências e métricas personalizadas em tempo real
- **Logs do console** — stdout/stderr brutos de cada contêiner e projeto
O rastreamento distribuído é particularmente valioso. Quando uma solicitação chega ao seu frontend, flui pela API do catálogo, toca no Redis e no PostgreSQL – você vê toda a cadeia com tempo para cada salto. Chega de adivinhar onde está o gargalo.
Achei o painel mais útil durante a depuração. Em vez de seguir várias janelas de terminal ou alternar entre arquivos de log, tudo fica em um só lugar com IDs de correlação que vinculam eventos relacionados entre serviços.
O painel também está disponível como um contêiner independente, o que significa que você pode usá-lo em ambientes de teste ou pipelines de CI:
```bash
docker run --rm -p 18888:18888 \
mcr.microsoft.com/dotnet/aspire-dashboard:latest
Implantação
O Aspire ajuda durante o desenvolvimento, mas e a produção? É aqui que as coisas ficam práticas.
Aplicativos de contêiner do Azure
O caminho de implantação mais simples é o Azure Container Apps (ACA), que tem suporte Aspire de primeira classe. Você pode implantar diretamente usando a CLI do Desenvolvedor do Azure:
azd init
azd up
O comando azd init detecta seu Aspire AppHost e gera a infraestrutura como código necessária. azd up provisiona tudo — registro de contêiner, aplicativos de contêiner, bancos de dados, instâncias Redis — com base em sua topologia AppHost.
Seu AppHost se torna essencialmente seu manifesto de implantação. O mesmo código que define “catalog-api depende de PostgreSQL e Redis” orienta o provisionamento da infraestrutura.
###Kubernetes
Para implantações do Kubernetes, o Aspire não gera manifestos diretamente, mas a topologia do AppHost é mapeada de forma limpa para os recursos do Kubernetes. A ferramenta da comunidade aspirate (Aspir8) pode gerar gráficos Helm ou manifestos Kubernetes a partir de seu AppHost:
dotnet tool install -g aspirate
aspirate generate
aspirate apply
Considerações de implantação
Algumas coisas para manter em mente:- As cadeias de conexão mudam entre ambientes. Localmente, o Aspire ativa contêineres e injeta cadeias de conexão automaticamente. Na produção, você apontará para serviços gerenciados. O Aspire usa configuração .NET padrão, portanto, as variáveis de ambiente e o Azure Key Vault funcionam conforme esperado.
- O AppHost não funciona em produção. É uma ferramenta de orquestração de desenvolvimento e implantação. Na produção, seus serviços são executados de forma independente, configurados por meio de variáveis de ambiente e orquestradores.
- Os recursos de infraestrutura tornam-se serviços geridos. O seu contentor PostgreSQL local torna-se a Base de Dados Azure para PostgreSQL. O seu contentor Redis local torna-se Azure Cache para Redis. O código de consumo não muda.
Dicas do mundo real
Depois de executar o Aspire em produção por um tempo, aqui estão as lições que nos pouparam tempo:
1. Use verificações personalizadas do ciclo de vida dos recursos
Não confie apenas na inicialização do contêiner para determinar se um recurso está pronto. O PostgreSQL pode aceitar conexões TCP antes de estar realmente pronto para atender consultas.
var postgres = builder.AddPostgres("postgres")
.AddDatabase("catalogdb")
.WithHealthCheck();
O Aspire pode executar verificações de integridade de recursos e manter serviços dependentes até que estejam realmente prontos.
2. Extraia padrões comuns em extensões
Se vários serviços compartilharem configurações semelhantes, crie métodos de extensão:
public static class AppHostExtensions
{
public static IResourceBuilder<ProjectResource> AddWorkerService(
this IDistributedApplicationBuilder builder,
string name,
IResourceBuilder<IResourceWithConnectionString> db,
IResourceBuilder<IResourceWithConnectionString> messaging)
{
return builder.AddProject<Projects.WorkerService>(name)
.WithReference(db)
.WithReference(messaging)
.WithReplicas(3);
}
}
3. Aproveite WithReplicas para testes de carga
var catalogApi = builder.AddProject<Projects.CatalogApi>("catalog-api")
.WithReference(postgres)
.WithReference(redis)
.WithReplicas(5);
WithReplicas ativa várias instâncias de um serviço localmente. Isso é ótimo para testar balanceamento de carga, bugs de simultaneidade e comportamento de cache distribuído sem implantar em um cluster.
4. Use parâmetros para valores sensíveis
Não codifique credenciais, mesmo para desenvolvimento local:
var dbPassword = builder.AddParameter("db-password", secret: true);
var postgres = builder.AddPostgres("postgres", password: dbPassword)
.AddDatabase("catalogdb");
Ao executar localmente, o Aspire solicitará o valor ou o lerá nos segredos do usuário. No CI/CD, isso vem de variáveis de ambiente.
5. Testes de integração tornam-se triviais
O Aspire inclui um pacote de testes que torna o teste de integração de aplicativos distribuídos extremamente fácil:
[Fact]
public async Task CatalogApiReturnsProducts()
{
var appHost = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.MyCloudApp_AppHost>();
await using var app = await appHost.BuildAsync();
await app.StartAsync();
var httpClient = app.CreateHttpClient("catalog-api");
var response = await httpClient.GetAsync("/api/products");
response.EnsureSuccessStatusCode();
var products = await response.Content
.ReadFromJsonAsync<List<Product>>();
Assert.NotEmpty(products);
}
Isso acelera todo o seu aplicativo distribuído - incluindo bancos de dados e corretores de mensagens - executa seu teste e o desmonta. Testes reais de integração em infraestrutura real, em seu pipeline de CI. Sem zombarias.
6. Monitore o uso de recursos localmente
Ao executar vários contêineres localmente, fique atento ao consumo de recursos. Uma instância PostgreSQL, Redis e RabbitMQ com UI de gerenciamento pode consumir facilmente de 2 a 3 GB de RAM. Se você estiver em uma máquina restrita, considere usar configurações de recursos mais leves:
var redis = builder.AddRedis("cache")
.WithContainerRuntimeArgs("--memory=256m");
Conclusão
O .NET Aspire mudou fundamentalmente a forma como construo aplicativos distribuídos. Não porque introduza conceitos revolucionários — verificações de integridade, OpenTelemetry e orquestração de contêineres não são novidade. Mas porque isso os torna padrão. Ele toma as centenas de pequenas decisões que você normalmente teria que tomar, implementa-as com padrões sensatos e permite substituí-las quando necessário.O padrão AppHost é, na minha opinião, a maior vitória. Ter a topologia de seu aplicativo distribuído expressa como código (não espalhada em arquivos Docker Compose, manifestos do Kubernetes e documentos README) torna o sistema compreensível. Novos membros da equipe podem abrir Program.cs no AppHost e entender toda a arquitetura em minutos.
Se você estiver construindo aplicativos distribuídos com .NET, dê uma olhada séria no Aspire. Comece com o modelo aspire-starter, explore o painel e adote gradualmente os componentes conforme necessário. Você não precisa apostar tudo no primeiro dia – o Aspire é aditivo por design.
Os dias em que você passava seu primeiro sprint conectando o padrão de infraestrutura acabaram. Deixe o Aspire cuidar do encanamento para que você possa se concentrar no que realmente importa: sua aplicação.