C# 12 in Practice: Writing Cleaner Code with Primary Constructors and Collection Expressions

Sunday, October 4, 2026 | ReadingTime
C# 12 in Practice: Writing Cleaner Code with Primary Constructors and Collection Expressions

The .NET ecosystem continues to evolve with a singular focus: reducing boilerplate code without sacrificing type safety or high performance. With the launch of .NET 8 and C# 12, Microsoft introduced key features designed to transform repetitive design patterns into clean, native language constructs.


For software engineers building web APIs, microservices, and enterprise applications, two features stand out: Primary Constructors in standard classes/structs and Collection Expressions featuring the spread operator (..). If you are tired of declaring redundant readonly fields for dependency injection or juggling disjointed syntaxes to initialize arrays and lists, C# 12 addresses these challenges directly.


In this practical article, we will refactor legacy code written in .NET 6/7 into modern C# 12 conventions, balancing Clean Code principles and Domain-Driven Design (DDD) with memory performance.


1. Primary Constructors


The Legacy Code Problem


In .NET 6 and .NET 7, performing dependency injection inside application services required explicit private readonly fields and boilerplate constructor declarations.


C#

// ❌ Legacy Pattern (.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("Processing payment {PaymentId}", payment.Id); 

       await _paymentRepository.SaveAsync(payment, cancellationToken); 

       await _notificationService.NotifySuccessAsync(payment.CustomerEmail); 

   } 

} 


The C# 12 Solution (.NET 8)


With Primary Constructors, parameters are defined directly in the class header and automatically made accessible throughout the class body.


C#

// ✅ C# 12 Pattern (.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) 

   { 

       // Primary constructor parameters are directly accessible in class methods 

       logger.LogInformation("Processing payment {PaymentId}", payment.Id); 

        

       await paymentRepository.SaveAsync(payment, cancellationToken); 

       await notificationService.NotifySuccessAsync(payment.CustomerEmail); 

   } 

} 


2. Collection Expressions and the Spread Operator (..)


Fragmented Syntax in .NET 6/7


Initializing and merging collections required disparate code constructs depending on whether you worked with arrays, lists, or spans.


C#

// ❌ Legacy initialization and concatenation (.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(); 


Unified Syntax with C# 12 Square Brackets [...]


Collection Expressions standardize collection initialization across data structures using square brackets [...] combined with the spread operator .. for flattening items.


C#

// ✅ C# 12 Pattern (.NET 8) 

int[] baseCodes = [101, 102, 103]; 

int[] extraCodes = [201, 202]; 

 

// Declarative concatenation using the spread operator (..) 

List<int> allCodesList = [.. baseCodes, .. extraCodes, 301]; 

 

// Direct stack/assembly allocation via ReadOnlySpan with zero Garbage Collector pressure 

ReadOnlySpan<byte> activeStatus = [1, 2, 4]; 


Real-World Use Cases and Performance


When to Use


  • Primary Constructors: Ideal for Dependency Injection in Use Cases, Services, MediatR Handlers, and API Controllers.
  • Collection Expressions: Excellent for initializing byte buffers, constructing immutable response lists, and leveraging ReadOnlySpan<T> for zero-allocation memory access.


When to AVOID


  1. Mutable Parameter State: Note that primary constructor parameters in standard classes are not automatically readonly fields (unlike positional records). If a method reassigned the parameter internally, it could introduce unintended side effects. If strict immutability is required in Domain Models (DDD), continue explicitly assigning parameters to readonly fields or use record types. 
  2. Complex Constructor Logic: If your setup logic requires multi-step validation or heavy initialization routines, prefer standard explicit constructors for clarity.


Performance Gains in .NET 8


  • Zero-Allocation Data Spans: When assigning a collection expression to a ReadOnlySpan<T>, the C# 12 compiler emits raw bytes directly into the assembly's data segment, bypassing Heap allocations completely.
  • Capacity Optimization: When creating a List<T> using spread elements ([.. a, .. b]), the compiler pre-calculates the exact total capacity required, preventing internal array resizing (Array.Resize).


Conclusion


C# 12 and .NET 8 deliver measurable productivity gains and execution optimizations. Adopting Primary Constructors and Collection Expressions produces cleaner, more readable, and highly efficient software.


Start refactoring your .NET services today to take full advantage of these features!


👉 Looking to advance your software development career? Explore premier tech opportunities and advance your skills with the MasterStack platform!

Marcio Mazeu
Written by
Marcio Mazeu

.NET Software Developer

.NET Software Developer with a degree in Computer Engineering, specialized in application modernization and API architecture (C#, SQL Server, JavaScript). Passionate about building high-performance tools and continuous learning, he contributes to creating reliable technological solutions focused on user experience.