Let's be real—every time Microsoft drops a new version of C#, there's a flood of articles hyping up every single feature like it's going to revolutionize how we write software. I’ve been using C# 12 in production for a while now, and the truth is, about 80% of the new syntax sugar is stuff I’ll rarely touch.
But that other 20%? It completely changes how clean your codebase looks, especially when dealing with ASP.NET Core dependency injection.
Here’s my unfiltered breakdown of the C# 12 features you actually need to care about.
1. Primary Constructors (The Absolute MVP)
If you only take one thing away from C# 12, it should be Primary Constructors for classes. We've had them for record types since C# 9, but bringing them to regular classes is the biggest quality-of-life improvement for API development in years.
Look at the boilerplate we’ve been writing for the last decade just to inject a logger and a database context:
// The old, painful way
public class PaymentService
{
private readonly IPaymentGateway _gateway;
private readonly ApplicationDbContext _db;
private readonly ILogger<PaymentService> _logger;
public PaymentService(
IPaymentGateway gateway,
ApplicationDbContext db,
ILogger<PaymentService> logger)
{
_gateway = gateway;
_db = db;
_logger = logger;
}
// Now I can finally write my method...
}
That’s 15 lines of code just to set up dependencies. I absolutely hated doing this. Now, with C# 12:
// The new, glorious way
public class PaymentService(
IPaymentGateway gateway,
ApplicationDbContext db,
ILogger<PaymentService> logger)
{
public async Task ProcessAsync(PaymentReq req)
{
logger.LogInformation("Processing payment...");
// You just use 'gateway' and 'db' directly. No private fields needed.
}
}
The Gotcha: Be careful here. Unlike records, the parameters in a class primary constructor do not automatically become public properties. They are essentially private fields scoped to the class. If you need a parameter to be publicly accessible, you still have to manually map it to a property (public IPaymentGateway Gateway { get; } = gateway;).
2. Collection Expressions (Goodbye new List<string>())
I've always found initializing lists and arrays in C# unnecessarily verbose compared to JavaScript or Python. C# 12 finally gives us a clean, unified syntax for creating collections.
// Before C# 12
List<string> allowedOrigins = new List<string> { "localhost", "api.com" };
int[] primes = new[] { 2, 3, 5, 7, 11 };
// C# 12
List<string> allowedOrigins = ["localhost", "api.com"];
int[] primes = [2, 3, 5, 7, 11];
Span<int> fastPrimes = [2, 3, 5, 7, 11]; // Yes, it works on Spans too!
They also introduced the spread operator (..). If you've used JavaScript, you know exactly how this works. It’s perfect for merging config arrays without doing ugly .Concat().ToArray() chains.
string[] defaultRoles = ["User", "Guest"];
string[] allRoles = ["Admin", ..defaultRoles, "SuperAdmin"];
3. Alias Any Type (Actually Useful for Tuples)
The using directive got a nice upgrade. You could always alias named types, but now you can alias tuples and arrays.
I'm generally against aliasing primitive types because it confuses junior devs (don't write using MyString = System.String;, please). But it is a godsend for complex Tuples.
Instead of passing (double Lat, double Lon) into five different methods and hoping you don't mess up the order, you can do this:
using Coordinates = (double Lat, double Lon);
public class MapService
{
public double CalculateDistance(Coordinates start, Coordinates end)
{
// Much cleaner than (double Lat, double Lon) everywhere
return Math.Sqrt(Math.Pow(end.Lat - start.Lat, 2));
}
}
The Features I Ignore
- Inline Arrays: Unless you are writing an ultra-low-level, high-performance library (like Kestrel internals) where you need fixed-size arrays without the
unsafekeyword, you will never use this. Stick toList<T>orSpan<T>. - Default Lambda Parameters: Nice to have, I guess? But if your lambda is getting so complex that it needs default parameters, it probably shouldn't be a lambda anymore. Just write a local function or a private method.
Final Thoughts
Primary constructors alone make upgrading to C# 12 worth it. Go update your .csproj files to <LangVersion>12.0</LangVersion> (or just use .NET 8) and start deleting those useless private fields!