C# - Cinco Recursos para melhorar o seu código
   Este artigo apresenta cinco recursos da plataforma .NET que podem facilitar o desenvolvimento do seu código no dia a dia.

Devido a correria do dia a dia geralmente trabalhamos no modo automático repetindo procedimentos que atualmente já foram melhorados. Isso se aplicar ao desenvolvimento de código usando C#, onde , muitas vezes, continuamos usando uma abordagem antiga que já foi melhorada e que pode facilitar a nossa vida.

 

Hoje vou apresentar cinco recursos da linguagem C# que foram melhorados e que você pode usar para alavancar seu código.

1. FrozenDictionary : Buscas mais rápidas para dados apenas leitura

Muitas aplicações contêm dados de consulta que nunca mudam. Exemplos incluem:

- Códigos de países
- Mapeamentos de moedas
- Códigos de erro
- Categorias de produtos
- Valores de configuração

A maioria de nós armazena essas informações em um dicionário :  Dictionary<TKey, TValue>
var paises = new Dictionary<string, string>
{
    ["BR"] = "Brasil",
    ["US"] = "Estados Unidos",
    ["JP"] = "Japão"
}; 

Isso funciona perfeitamente. Mas um dicionário regular é projetado para coleções que podem mudar ao longo do tempo.

Se os dados forem criados uma vez e nunca modificados, o .NET oferece uma opção melhor : um dicionário congelado ou FrozenDictionary. 

Este recurso é otimizado durante a sua criação. Depois disso, ele se torna imutável e fornece buscas mais rápidas do que um dicionário padrão.
using System.Collections.Frozen;

var paises = new Dictionary<string, string>
{
    ["BR"] = "Brasil",
    ["US"] = "Estados Unidos",
    ["JP"] = "Japão"
}.ToFrozenDictionary();

Imagine uma aplicação de pagamento que valida códigos de moeda para cada requisição. Em vez de recriar ou modificar o dicionário, podemos criá-lo uma vez durante a inicialização e reutilizá-lo durante todo o tempo de vida da aplicação.

Isso melhora tanto a performance quanto a segurança.

Nota: A criação de um FrozenDictionary tem um custo relativamente alto. A vantagem aparece quando a coleção é criada poucas vezes e consultada muitas vezes. Não significa que será sempre mais rápido em qualquer situação.

2. TimeProvider : Torna o tempo testável

O tempo é difícil de testar. Muitas aplicações contêm código como este:

public class ServicoAssinatura
{
    public bool EstaExpirado(DateTime dataExpiracao)
    {
        return dataExpiracao < DateTime.UtcNow;
    }
}

O problema é que DateTime.UtcNow muda continuamente. Escrever testes unitários confiáveis torna-se difícil porque o tempo não pode ser controlado.

O .NET agora fornece a abstração TimeProvider. Em vez de ler o relógio do sistema diretamente, injete um provedor de tempo.

public class ServicoAssinatura
{
    private readonly TimeProvider _provedorTempo;
    public ServicoAssinatura(TimeProvider provedorTempo)
    {
        _provedorTempo = provedorTempo;
    }
    public bool EstaExpirado(DateTime dataExpiracao)
    {
        return dataExpiracao < _provedorTempo
            .GetUtcNow() // GetUtcNow()
            .UtcDateTime;
    }
} 

Registre-o durante a inicialização :  builder.Services.AddSingleton(TimeProvider.System);

Após isso os testes se tornam muito mais simples
:

Precisa incluir o pacote: dotnet add package Microsoft.Extensions.TimeProvider.Testing

using Microsoft.Extensions.Time.Testing; // Importante!
using Xunit;
public class TestesServicoAssinatura
{
    [Fact]
    public void Deve_Identificar_Assinatura_Expirada()
    {
        // 1. Instância do FakeTimeProvider vinda do pacote de testes
        var tempoFalso = new FakeTimeProvider();
        
        // 2. Define o tempo fictício fixo
        tempoFalso.SetUtcNow(new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc)));
        // 3. Injeta a abstração na classe
        var servico = new ServicoAssinatura(tempoFalso);
        // 4. Executa a verificação
        var dataExpiracaoAntiga = new DateTime(2025, 12, 31, 23, 59, 59, DateTimeKind.Utc);
        bool resultado = servico7.EstaExpirado(dataExpiracaoAntiga);
        Assert.True(resultado);
    }
}
 
3. Substitua laço infinitos pelo recurso PeriodicTimer

O PeriodicTimer do C# é um temporizador moderno, leve e orientado a operações assíncronas, introduzido no .NET 6. Ele permite aguardar de forma assíncrona por tiques individuais dentro de um loop `while` padrão, eliminando a necessidade de callbacks complexos ou do tratamento manual de eventos.

Os Timers mais antigos acionam um callback independentemente de a execução anterior ter sido concluída. O `PeriodicTimer` evita inerentemente a reentrância, pois o próximo "tick" não é aguardado até que a iteração atual do loop seja finalizada.

Serviços em segundo plano geralmente contêm loops como este:

while (!cancellationToken.IsCancellationRequested)
{
    await RefreshCacheAsync();
    await Task.Delay(TimeSpan.FromMinutes(5),cancellationToken);
} 

Este trecho de código executa a primeira atualização imediatamente e depois aguarda cinco minutos.

Não há nada tecnicamente errado com isso.  Mas o .NET fornece uma API projetada especificamente para tarefas assíncronas recorrentes.

var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));

while (await timer.WaitForNextTickAsync(cancellationToken))
{
    await RefreshCacheAsync();
}

Já neste código, a primeira execução acontece somente depois de cinco minutos.
 
A intenção fica imediatamente evidente.  O temporizador controla o agendamento.  O loop executa o trabalho.

Esse padrão funciona muito bem para:
   Atualização de cache
   Sincronização em segundo plano
   Coleta de métricas
   Monitoramento de integridade
   Tarefas agendadas de limpeza

Além de ser mais fácil de ler, ele oferece suporte ao cancelamento de forma natural.

4. Channel<T> - Um forma melhor de processar trabalhos em segundo plano

O Channel<T> no C# é uma estrutura de dados de sincronização do namespace System.Threading.Channels usada para passar dados de forma assíncrona entre produtores e consumidores. Ele funciona como uma fila em memória de alto desempenho, ideal para gerenciar tarefas em segundo plano e cenários com múltiplas threads

Imagine uma API que recebe centenas de solicitações de pedidos por segundo. Enviar e-mails de confirmação imediatamente dentro do pipeline da requisição aumenta o tempo de resposta.

Uma abordagem melhor é separar o processamento da requisição do trabalho executado em segundo plano.

É exatamente aqui que o Channel<T> se destaca. A API grava o trabalho no canal.

Um serviço em segundo plano faz a leitura desse canal.

Produtor:
public class OrderService
{
    private readonly Channel<Order> _channel;
    public OrderService(Channel<Order> channel)
    {
        _channel = channel;
    }
    public async Task CreateOrderAsync(Order order)
    {
        // Salvar pedido...
        await _channel.Writer.WriteAsync(order);
    }
}

Consumidor:

public class EmailWorker : BackgroundService
{
    private readonly Channel<Order> _channel;
    public EmailWorker(Channel<Order> channel)
    {
        _channel = channel;
    }
    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        await foreach (var order in _channel.Reader
            .ReadAllAsync(stoppingToken))
        {
            await SendConfirmationEmail(order);
        }
    }
}

A API retorna a resposta imediatamente.  O processamento dos e-mails acontece de forma independente.

O resultado é um menor tempo de resposta e uma melhor escalabilidade.

5. ValueTask - Uma pequena otimização com grande impacto no cenário certo

O recurso ValueTask é uma estrutura (struct) criada para melhorar o desempenho de códigos assíncronos, evitando o uso desnecessário de memória em operações que frequentemente terminam de forma imediata (síncrona). Ela funciona como uma alternativa mais leve ao tradicional

A maioria dos métodos assíncronos retorna um
Task.

public async Task<User?> GetUserAsync(int id)
{
   return await repository.GetAsync(id);
}

Essa é a escolha correta para a grande maioria das aplicações.

No entanto, em alguns cenários, o resultado já está disponível imediatamente, sem a necessidade de executar uma operação assíncrona.

Um exemplo típico é a leitura de dados armazenados em um cache em memória.

public ValueTask<User?> GetUserAsync(int id)
{
    if (_cache.TryGetValue(id, out var user))
    {
        return ValueTask.FromResult(user);
    }
    return new ValueTask<User?>(
        LoadFromDatabaseAsync(id));
}

Nesse caso, ValueTask evita a criação de um novo objeto Task quando o resultado já está disponível.

Em sistemas com alta taxa de processamento, essa pequena otimização pode reduzir significativamente a quantidade de alocações de memória e, consequentemente, diminuir a pressão sobre o Garbage Collector.

Entretanto, ValueTask deve ser utilizado com cautela.

Diferentemente de Task, um ValueTask não foi projetado para ser aguardado (await) várias vezes, nem para ser armazenado e reutilizado posteriormente.

Por isso, para a maior parte do código de uma aplicação, Task continua sendo a melhor opção, pois oferece uma API mais simples, segura e adequada para a maioria dos cenários.

Reserve o uso de ValueTask para situações críticas de desempenho, nas quais medições e ferramentas de profiling demonstrem que a criação frequente de objetos Task representa um custo significativo.

Conclusão:

Os recursos apresentados mostram como o .NET oferece alternativas para criar aplicações assíncronas mais eficientes e escaláveis. O PeriodicTimer simplifica tarefas recorrentes, o Channel<T> desacopla o processamento em segundo plano e o ValueTask pode reduzir alocações em cenários de alto desempenho.

No entanto, essas ferramentas devem ser usadas quando realmente fazem sentido. Na maioria das aplicações, Task continua sendo a melhor escolha. O importante é conhecer esses recursos para aplicá-los de forma consciente, construindo soluções mais simples e fáceis de manter.

E estamos conversados..

"Eu me deitei e dormi; acordei, porque o Senhor me sustentou."
Salmos 3:5

Referências:


José Carlos Macoratti