O ecossistema .NET continua avançando com um objetivo muito claro: reduzir o código boilerplate sem abrir mão da segurança de tipos e do alto desempenho. Com o lançamento do .NET 8 e do C# 12, a Microsoft entregou melhorias focadas na produtividade do desenvolvedor, transformando padrões repetitivos em construções nativas elegantes.
Para quem constrói APIs web, microserviços e sistemas corporativos no dia a dia, duas funcionalidades se destacam: os Primary Constructors em classes e structs convencionais e as Collection Expressions com o spread operator (..). Se você já cansou de declarar campos privados readonly apenas para injeção de dependência ou de usar diferentes sintaxes para inicializar arrays e listas, essas novidades resolvem diretamente essas dores.
Neste artigo prático, vamos analisar como refatorar código legado escrito em .NET 6/7 para as convenções do C# 12 e .NET 8, alinhando boa arquitetura (Clean Code e DDD) com eficiência de memória.
1. Primary Constructors (Construtores Primários)
O Problema do Código Legado
Nas versões .NET 6 e .NET 7, a injeção de dependências em serviços de aplicação exigia a declaração de campos privados readonly e de um construtor explícito.
C#
// ❌ Padrão Tradicional (.NET 6 / .NET 7)
namespace PaymentContext.Domain.Services;
public class PaymentProcessor : IPaymentProcessor
{
private readonly IPaymentRepository _paymentRepository;
private readonly INotificationService _notificationService;
private readonly ILogger<PaymentProcessor> _logger;
public PaymentProcessor(
IPaymentRepository paymentRepository,
INotificationService notificationService,
ILogger<PaymentProcessor> logger)
{
_paymentRepository = paymentRepository ?? throw new ArgumentNullException(nameof(paymentRepository));
_notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task ProcessAsync(Payment payment, CancellationToken cancellationToken)
{
_logger.LogInformation("Processando pagamento {PaymentId}", payment.Id);
await _paymentRepository.SaveAsync(payment, cancellationToken);
await _notificationService.NotifySuccessAsync(payment.CustomerEmail);
}
}
A Solução no C# 12 (.NET 8)
Com os Primary Constructors, os parâmetros são declarados diretamente no cabeçalho da classe e ficam automaticamente visíveis em todo o seu escopo.
C#
// ✅ Padrão C# 12 (.NET 8)
namespace PaymentContext.Domain.Services;
public class PaymentProcessor(
IPaymentRepository paymentRepository,
INotificationService notificationService,
ILogger<PaymentProcessor> logger) : IPaymentProcessor
{
public async Task ProcessAsync(Payment payment, CancellationToken cancellationToken)
{
// Os parâmetros do construtor primário estão disponíveis no corpo da classe
logger.LogInformation("Processando pagamento {PaymentId}", payment.Id);
await paymentRepository.SaveAsync(payment, cancellationToken);
await notificationService.NotifySuccessAsync(payment.CustomerEmail);
}
}
2. Collection Expressions e o Spread Operator (..)
Sintaxe Fragmentada no .NET 6/7
Inicializar e concatenar diferentes tipos de coleções exigia métodos distintos para cada estrutura de dados (new[], List<T>, Span<T>).
C#
// ❌ Inicialização e concatenação legada (.NET 6 / .NET 7)
int[] baseCodes = new int[] { 101, 102, 103 };
int[] extraCodes = new int[] { 201, 202 };
List<int> allCodesList = new List<int>(baseCodes);
allCodesList.AddRange(extraCodes);
allCodesList.Add(301);
int[] result = allCodesList.ToArray();
Unificação no C# 12 com Colchetes [...]
Com as Collection Expressions, a criação de coleções é padronizada através do uso de colchetes [...] e do spread operator .. para achatar elementos.
C#
// ✅ Padrão C# 12 (.NET 8)
int[] baseCodes = [101, 102, 103]; int[] extraCodes = [201, 202]; // Concatenação limpa e declarativa usando o spread operator (..) List<int> allCodesList = [.. baseCodes, .. extraCodes, 301]; // Alocação direta em memória de pilha/assembly via ReadOnlySpan sem pressão no Garbage Collector ReadOnlySpan<byte> activeStatus = [1, 2, 4];
Casos de Uso Reais e Performance
Onde Usar
- Primary Constructors: Ideais para injeção de dependência em Use Cases, Services, Handlers do MediatR e API Controllers.
- Collection Expressions: Ideais para inicializar buffers, retornar coleções imutáveis, passar argumentos para métodos e manipular ReadOnlySpan<T> com alocação zero no Heap.
Onde EVITAR
- Parâmetros Mutáveis em Primary Constructors: Lembre-se de que parâmetros de construtores primários em classes normais não são campos readonly automáticos (diferente de records). Se o valor for reatribuído dentro de métodos da classe, isso pode gerar bugs colaterais. Se a imutabilidade estrita for um requisito no seu modelo de domínio (DDD), continue atribuindo o valor a um campo readonly explícito ou utilize record.
- Inicializações com Lógica Complexa: Se o seu construtor precisa rodar validações extensas ou executar rotinas pesadas, prefira um construtor tradicional para manter a clareza.
Ganho de Performance no .NET 8
- Zero-Allocation Data Spans: Ao inicializar um ReadOnlySpan<T> com Collection Expressions, o compilador do C# 12 embute os bytes diretamente no segmento de dados da aplicação (Assembly), evitando qualquer alocação na Heap.
- Dimensionamento de Capacidade: Ao usar expressões de coleção com a sintaxe List<T> list = [.. a, .. b], o compilador pré-calcula a contagem total de elementos antes de alocar a lista, prevenindo múltiplos redimensionamentos internos (Array.Resize).
Conclusão e Call to Action
O C# 12 e o .NET 8 trazem um avanço direto na produtividade técnica e no desempenho de execução. A adotação de Primary Constructors e Collection Expressions elimina o código repetitivo mantendo o código conciso, legível e seguro.
Comece a refatorar seus serviços hoje mesmo para tirar proveito dessas inovações!
👉 Quer impulsionar sua carreira em desenvolvimento .NET? Explore as melhores vagas de TI e continue evoluindo seu conhecimento técnico com a comunidade da MasterStack!