C# 14 Is Here: Everything You Need to Know (With Code Examples)

Microsoft has shipped C# 14 alongside .NET 10, and it’s packed with quality-of-life improvements that make everyday code cleaner, safer, and more expressive. Let’s walk through every major feature — with fresh, practical examples you can try right now.


1. Extension Members — Properties, Statics, and Operators on Any Type

C# has long supported extension methods. C# 14 takes it much further with the new extension block — you can now add properties, static members, and even operators to types you don’t own.

Imagine you work with strings a lot and want a clean .IsValidEmail check, a .WordCount property, and the ability to repeat a string with *:

public static class StringExtensions
{
extension(string text)
{
// Extension property — use it like: email.IsValidEmail
public bool IsValidEmail => text.Contains('@') && text.Contains('.');
// Extension property — use it like: sentence.WordCount
public int WordCount => text.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
}
extension(string)
{
// Static extension property — use it like: string.Placeholder
public static string Placeholder => "N/A";
// User-defined operator — use it like: "hello" * 3 => "hellohellohello"
public static string operator *(string text, int times) =>
string.Concat(Enumerable.Repeat(text, times));
}
}
// Usage:
string email = "dev@example.com";
Console.WriteLine(email.IsValidEmail); // True
Console.WriteLine("hello world".WordCount); // 2
Console.WriteLine(string.Placeholder); // N/A
Console.WriteLine("ping! " * 3); // ping! ping! ping!

This is a game-changer for library authors — your APIs can now feel like true language primitives.


2. The field Keyword — Say Goodbye to Backing Fields

Every C# developer has written a property with validation that forced them to declare a private backing field. The new field keyword lets the compiler generate it for you.

Here’s a UserProfile class that enforces age and username rules — no manual backing fields needed:

public class UserProfile
{
// Compiler synthesizes the backing field automatically
public string Username
{
get;
set => field = string.IsNullOrWhiteSpace(value)
? throw new ArgumentException("Username cannot be blank.")
: value.Trim().ToLower();
}
public int Age
{
get;
set => field = (value is < 0 or > 150)
? throw new ArgumentOutOfRangeException(nameof(Age), "Age must be between 0 and 150.")
: value;
}
}
// Usage:
var profile = new UserProfile();
profile.Username = " Alice "; // stored as "alice"
profile.Age = 30; // fine
profile.Age = 200; // throws ArgumentOutOfRangeException

Cleaner classes, less noise — and full control over validation logic without the ceremony.


3. Null-Conditional Assignment — Assign Only When It Makes Sense

The ?. operator has been a null-safety staple for reading values. In C# 14, you can use it on the left side of an assignment too — so the right side only executes when the target isn’t null.

Consider an e-commerce scenario where you conditionally update a shopping cart:

public class Cart
{
public string PromoCode { get; set; }
public List<string> Items { get; set; } = new();
public decimal Discount { get; set; }
}
Cart activeCart = GetActiveCart(); // might return null
// Old way — manual null guard required:
if (activeCart != null)
{
activeCart.PromoCode = "SAVE20";
activeCart.Discount += 20m;
}
// C# 14 — clean and concise:
activeCart?.PromoCode = "SAVE20";
activeCart?.Discount += 20m; // compound assignment works too!
// The promo code lookup won't even run if activeCart is null:
activeCart?.PromoCode = FetchBestPromoFromApi();

Notice the last line — FetchBestPromoFromApi() is not called at all if activeCart is null. That’s a real benefit when those calls are expensive.


4. Implicit Span Conversions — High-Performance Code, Less Friction

Span<T> and ReadOnlySpan<T> are the go-to tools for zero-allocation data processing in .NET. C# 14 introduces implicit conversions so you can pass arrays where spans are expected — and vice versa — without manual casting.

// A method that processes data efficiently using ReadOnlySpan
static double AverageTemperature(ReadOnlySpan<double> readings)
{
double sum = 0;
foreach (var temp in readings) sum += temp;
return sum / readings.Length;
}
double[] dailyReadings = { 22.5, 23.1, 21.8, 24.0, 22.9 };
// C# 14: the array converts implicitly — no .AsSpan() call needed
double avg = AverageTemperature(dailyReadings);
Console.WriteLine($"Average: {avg:F1}°C"); // Average: 22.9°C
// Span and ReadOnlySpan also compose more naturally with generics
ReadOnlySpan<char> greeting = "Hello, World!";
Span<char> buffer = new char[greeting.Length];
greeting.CopyTo(buffer);

Less ceremony around high-performance code means it’s easier to adopt these patterns throughout your codebase.


5. nameof with Unbound Generics — Cleaner Type Names

Previously, nameof required a closed generic type like nameof(Dictionary<string, int>) just to get back "Dictionary". In C# 14, you can pass the unbound form directly.

// Before C# 14 — you had to supply dummy type arguments:
string name1 = nameof(Dictionary<string, int>); // "Dictionary"
string name2 = nameof(List<object>); // "List"
// C# 14 — just use the unbound form:
string name3 = nameof(Dictionary<,>); // "Dictionary"
string name4 = nameof(List<>); // "List"
string name5 = nameof(Func<,,>); // "Func"
// Real-world use: building a generic cache key or log label
public static string CacheKey<T>() => $"cache:{nameof(List<>)}:{typeof(T).Name}";
Console.WriteLine(CacheKey<int>()); // cache:List:Int32
Console.WriteLine(CacheKey<string>()); // cache:List:String

6. Lambda Parameters with Modifiers — No Full Types Required

When a lambda parameter needed a modifier like ref or out, you previously had to write the full type for every parameter, even the ones you didn’t care about. Not anymore.

// A delegate for a try-parse style conversion
delegate bool TryConvert<TIn, TOut>(TIn input, out TOut result);
// Before C# 14 — all parameter types had to be spelled out:
TryConvert<string, int> parseOld = (string text, out int result) =>
int.TryParse(text, out result);
// C# 14 — just add the modifier, skip the type:
TryConvert<string, int> parseInt = (text, out result) => int.TryParse(text, out result);
TryConvert<string, bool> parseBool = (text, out result) => bool.TryParse(text, out result);
// Works great for inline swap utilities with ref params:
delegate void Swapper<T>(ref T a, ref T b);
Swapper<int> swap = (ref a, ref b) => (a, b) = (b, a);
int x = 10, y = 20;
swap(ref x, ref y);
Console.WriteLine($"x={x}, y={y}"); // x=20, y=10

7. Partial Constructors and Events — Better Source Generator Support

Source generators are a cornerstone of modern .NET tooling (think: EF Core, SignalR, logging). C# 14 extends the partial keyword to constructors and events, making generator-heavy classes much easier to split between generated and hand-written code.

// File: OrderProcessor.cs (your hand-written code)
public partial class OrderProcessor
{
// Defining declaration — no body, no initializer
public partial OrderProcessor(string region);
public void Process(Order order) => Console.WriteLine($"Processing in {_region}");
}
// File: OrderProcessor.Generated.cs (source generator output)
public partial class OrderProcessor
{
private readonly string _region;
private readonly ILogger _logger;
// Implementing declaration — has the body and base/this call
public partial OrderProcessor(string region) : base()
{
_region = region;
_logger = LoggerFactory.Create(b => b.AddConsole()).CreateLogger<OrderProcessor>();
_logger.LogInformation("OrderProcessor created for region: {Region}", region);
}
}

Your hand-written file stays clean and readable. The generated file handles all the plumbing. No mismatch, no duplication.


8. User-Defined Compound Assignment Operators

Custom types can now define their own behavior for compound operators like +=, -=, and *=. Previously this was inferred from the binary operator; now you can override it directly for full control.

public struct Budget
{
public decimal Amount { get; private set; }
public Budget(decimal amount) => Amount = amount;
// Standard addition
public static Budget operator +(Budget a, Budget b) => new(a.Amount + b.Amount);
// User-defined compound: += can now enforce a cap rule
public static Budget operator +=(Budget current, Budget extra)
{
var newAmount = current.Amount + extra.Amount;
return new Budget(Math.Min(newAmount, 100_000m)); // cap at 100k
}
public override string ToString() => $"${Amount:N0}";
}
var budget = new Budget(90_000m);
budget += new Budget(15_000m); // would be 105k — capped to 100k
Console.WriteLine(budget); // $100,000

Getting Started with C# 14 Today

All of these features are available now. To try them out:

C# 14 continues the language team’s philosophy of removing friction without adding complexity. Each of these features solves a real daily annoyance — and the cumulative effect on code readability is significant.

Source: What’s new in C# 14 — Microsoft Learn

Leave a Reply