ASP.NET Core no .NET 11 Preview 7: análise técnica das novidades
O Preview 7 reduz o custo operacional do Blazor, adiciona CacheView e traz mudanças que exigem atenção antes do lançamento do .NET 11.
Os channels trabalham com o conceito de producers, que produzem (obviamente) dados, e consumers que consomem (ah vá) os dados, ambos de forma assíncrona.
Até aí, nada de especial, muitas estruturas fazem isso.
Qual a diferença de uma lista ou de um blocking collection?
A grande diferença é a eficiência thread-safe e o controle de backpressure, evitando esgotamento de recursos e bloqueio de threads.
Ele foi criado para cenários de stress, de alto throughput.
Podemos imaginar um Channel como um pipe:
[ Producers ] —> ( Channel / Buffer ) —> [ Consumers ]

// Criando um canal limitado (Bounded)
var options = new BoundedChannelOptions(capacity: 1000)
{
// Define o que acontece quando o canal está cheio
FullMode = BoundedChannelFullMode.DropOldest, // .Wait / .DropNewest / .DropOldest / .DropWrite
SingleReader = true, // Otimização se houver apenas um Consumer
SingleWriter = false // Múltiplos produtores (ex: várias instâncias da API)
};
Channel auditChannel = Channel.CreateBounded(options);Primeiro, Channels usam ValueTasks na maioria das operações, como ValueTask é um struct, ele pode evitar a criação de uma Task quando a operação for síncrona.
Reduzindo a alocação na heap. Ou seja, ganho de performance (reduz pressão no GC).
Segundo, você pode limitar o tamanho da fila (bounded channels), evitando falta de recursos.
Se a fila chegar no limite, você decide como agir. O producer fica aguardando liberar um slot (Wait – padrão), descartar novos dados (DropNewest), descartar o começo da fila (DropOldest) ou descartar a escrita atual (DropWrite).
Eles são ótimos para combinarmos com outro recurso interessante do .NET: Background Services.
(esse artigo está ficando maior do que eu queria, mas continue, vale a pena)
Imagine que você precisa registrar todas as chamadas da sua API, que tem milhares de requisições por minuto.
Se o seu middleware gravar a informação a cada request, você vai aumentar o tempo de resposta por causa do I/O de banco de dados, que vai se tornar um gargalo.
Você poderia ir na opção ingênua e usar um Fire & Forget?
Poderia.
Deveria? Não.
Você perderia o controle da concorrência e poderia perder o processo por falta de recursos.
Então você vai usar Channels+Background services, colocando os eventos na esteira e tratando de forma assíncrona.
public class AuditoriaWorker(
IAuditoriaQueue queue,
IServiceScopeFactory scopeFactory) : BackgroundService
{
private const int BatchSize = 30;
private readonly TimeSpan _maxWaitTime = TimeSpan.FromSeconds(10);
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var buffer = new List(BatchSize);
while (!stoppingToken.IsCancellationRequested)
{
try
{
// WaitToReadAsync aguarda de forma não bloqueante até que haja algo no Channel
// ou até que o timeout de _maxWaitTime expire.
using var cts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
cts.CancelAfter(_maxWaitTime);
try
{
if (await queue.Reader.WaitToReadAsync(cts.Token))
{
// Se houver dados, tentamos encher o buffer o máximo possível
// sem esperar (TryRead), aproveitando o que já está na memória.
while (buffer.Count 0)
{
await FlushBatchAsync(buffer, stoppingToken);
buffer.Clear();
}
}
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(5000, stoppingToken);
}
}
}
private async Task FlushBatchAsync(List logs, CancellationToken ct)
{
// Processamento do batch (ex: salvar no banco)
}
}Você pode, inclusive, apenas registrar os logs (gravar num banco, por exemplo), quando a fila alcançar uma quantidade configurada de registros, assim você usa um bulk insert, evitando idas desnecessárias ao banco de dados.
(convém configurar um tempo máximo de espera, para evitar espera eterna)
Para uma leitura simples, item a item, você usará ReadAllAsync.
Cada evento recebido no Channel, você processa individualmente.
Para controlar manualmente a fila, você pode usar WaitToReadAsync, ideal para cenários de tratamento em lote (bulk/batching).
Com ele, você pode aguardar até que tenha pelo menos um item disponível antes de iniciar a leitura.
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (await _reader.WaitToReadAsync(stoppingToken))
{
var batch = new List();
// Tenta pegar o que estiver disponível imediatamente até um limite (ex: 50 itens)
while (batch.Count < 50 && _reader.TryRead(out var log))
batch.Add(log);
if (batch.Any())
await _repository.BulkInsertAsync(batch);
}
}Channels são muito úteis para pipelines internos de processamento dentro da aplicação, especialmente quando não há necessidade de persistência da fila ou comunicação entre múltiplos serviços, quando não há necessidade de uma fila distribuída como Kafka ou RabbitMQ.
Importante: Channels não persistem dados! Se o processo morrer, os dados em fila serão perdidos.
Channels são um recurso simples de ser usado, eficiente e pouco explorado.
Oferecem uma forma simples de desacoplar produção e consumo de eventos, sem perder o controle sobre a concorrência e consumo de recursos.
Se usarmos com Background Services, podem evitar o uso de filas distribuídas em casos de pipelines internos, de forma performática.