.NET 10 & C# 14 Deep Dive Series - PART 2
Part 2: Null-Conditional Assignment - Stop Writing if (x != null) Everywhere
Welcome back! In Part 1, we explored how the field keyword eliminates property boilerplate. Today, we're tackling another everyday frustration: assigning values to potentially null objects.
Spoiler: C# 14 makes it ridiculously clean.
The Null Check Dance
You know this pattern. You've written it a thousand times:
if (customer != null)
{
customer.Order = GetCurrentOrder();
}
Or maybe you prefer the slightly shorter version:
if (customer is not null)
customer.Order = GetCurrentOrder();
It works. It's safe. It's also verbose and error-prone when you forget that one check in a 500-line method.
Now imagine doing this with nested objects:
if (company != null)
{
if (company.Department != null)
{
if (company.Department.Manager != null)
{
company.Department.Manager.Assistant = newAssistant;
}
}
}
This is what developers call "the pyramid of doom." Three levels deep and we've lost 80 characters of horizontal space to whitespace.
C# Already Had a Solution... Sort Of
C# 6 introduced the null-conditional operator (?.) for reading values safely:
// This won't crash if customer is null
var orderDate = customer?.Order?.OrderDate;
Beautiful, right? One line. No if statements. No crashes.
But here's the catch: It only worked for reading values, not writing them.
You couldn't do this:
customer?.Order = GetCurrentOrder(); // ❌ Compiler error in C# 13
C# 14: The Missing Piece
C# 14 extends the null-conditional operator to work on the left side of assignments. Now you can write:
customer?.Order = GetCurrentOrder(); // ✅ Works in C# 14!
That's it. If customer is null, nothing happens. If customer exists, the assignment proceeds.
How It Works
The semantics are straightforward:
- Evaluate the left side - Check if
customeris null - If null - Skip the entire operation (including the right side)
- If not null - Perform the assignment
The key insight: The right-hand side is only evaluated when needed.
customer?.Id = GenerateNextCustomerId();
// GenerateNextCustomerId() is NOT called if customer is null
This is crucial for expensive operations or methods with side effects.
Real-World Examples
1. Optional Telemetry Logging
Imagine middleware that logs telemetry, but telemetry is optional (null when disabled):
Before C# 14:
public async Task InvokeAsync(HttpContext context, Telemetry? telemetry)
{
var start = DateTime.UtcNow;
await _next(context);
if (telemetry != null)
{
telemetry.RequestPath = context.Request.Path;
telemetry.DurationMs = (DateTime.UtcNow - start).TotalMilliseconds;
telemetry.StatusCode = context.Response.StatusCode;
}
}
With C# 14:
public async Task InvokeAsync(HttpContext context, Telemetry? telemetry)
{
var start = DateTime.UtcNow;
await _next(context);
telemetry?.RequestPath = context.Request.Path;
telemetry?.DurationMs = (DateTime.UtcNow - start).TotalMilliseconds;
telemetry?.StatusCode = context.Response.StatusCode;
}
Six lines down to three. The intent is crystal clear: "Log this if telemetry is enabled."
2. Event Subscription
Subscribing to events on optional components:
// Before
if (eventBus != null)
eventBus.OnDataReceived += HandleDataReceived;
// After
eventBus?.OnDataReceived += HandleDataReceived;
One line. No ceremony.
3. Configuration Updates
Updating deeply nested configuration objects:
Before:
if (appConfig != null)
{
if (appConfig.UserPreferences != null)
{
appConfig.UserPreferences.Theme = "dark";
appConfig.UserPreferences.Language = "en-US";
}
}
After:
appConfig?.UserPreferences?.Theme = "dark";
appConfig?.UserPreferences?.Language = "en-US";
Pyramid of doom? Obliterated.
4. Collection Updates
Works with indexers too:
// Dictionary assignment
scores?["math"] = 95;
scores?["science"] = 88;
// Array assignment
matrix?[row, col] = CalculateValue();
If the collection is null, nothing happens. No NullReferenceException. No defensive checks.
5. Combining with Null-Coalescing Assignment
This gets really powerful when combined with ??=:
// Ensure config.Settings exists, but only if config is not null
config?.Settings ??= new Settings();
Translation: "If config exists and its Settings property is null, create a new Settings object."
Compare that to the pre-C# 14 version:
if (config != null)
{
if (config.Settings == null)
config.Settings = new Settings();
}
One line versus four. And more readable.
Advanced Patterns
Chained Null-Conditional Assignments
You can chain multiple levels:
company?.Department?.Manager?.Assistant = newAssistant;
The assignment only happens if every part of the chain is non-null. One null anywhere? The entire operation is skipped.
Multiple Assignments with Guards
public void UpdateUser(User? user, string name, int age)
{
user?.Name = name;
user?.Age = age;
user?.LastUpdated = DateTime.UtcNow;
}
If user is null, all three assignments are safely skipped. If user exists, all three properties get updated.
Conditional Method Invocation After Assignment
cache?.Data = FetchFromDatabase();
cache?.Invalidate(); // Method call after assignment
Both operations respect the null check.
What You Can't Do (Yet)
C# 14's null-conditional assignment is powerful, but has limitations:
❌ No Increment/Decrement Operators
counter?.Value++; // Compiler error
counter?.Value += 1; // Also an error
Why? These operators return a value (the incremented result). The semantics get unclear: What should be returned if the object is null?
Workaround:
if (counter != null)
counter.Value++;
❌ Not for Value Types
int? number = 5;
number?.Value = 10; // Doesn't make sense - int? is a value type
Value types have different nullability semantics. Use regular nullable operations instead.
❌ No ref Assignment
ref int target = ref array?[0]; // Not supported
ref semantics require the target to exist. Null-conditional logic conflicts with this.
Performance Considerations
Question: Does ?. add overhead compared to explicit null checks?
Answer: No meaningful difference in release builds. The JIT compiler optimizes both to similar IL code.
Benchmark comparison:
// Method 1: Explicit check
if (customer != null)
customer.Order = order;
// Method 2: Null-conditional
customer?.Order = order;
Both compile to essentially the same machine code. Use ?. for readability without worrying about performance.
Migration Tips
Start with Non-Critical Code
Begin using null-conditional assignments in logging, telemetry, or other non-critical paths:
logger?.LogInformation("User logged in");
metrics?.RecordLogin(userId);
Replace Nested Null Checks First
These see the biggest readability improvement:
// Before: 7 lines, 3 indentation levels
if (response != null)
{
if (response.Data != null)
{
response.Data.ProcessedAt = DateTime.UtcNow;
}
}
// After: 1 line, 0 indentation levels
response?.Data?.ProcessedAt = DateTime.UtcNow;
Common Pitfalls
Pitfall 1: Forgetting Side Effects
cache?.Data = FetchExpensiveData();
// FetchExpensiveData() is NOT called if cache is null
This is usually what you want, but be aware of the behavior.
Pitfall 2: Overusing in Critical Paths
// ❌ Bad: Hides a logic error
order?.Total = CalculateTotal();
// ✅ Better: Orders should never be null here
if (order == null)
throw new InvalidOperationException("Order cannot be null");
order.Total = CalculateTotal();
Use ?. for truly optional objects, not to hide bugs.
The Bottom Line
Null-conditional assignment completes C#'s null-safety story. You can now:
- ✅ Read nullable properties:
var x = customer?.Order?.Date - ✅ Write nullable properties:
customer?.Order = newOrder - ✅ Chain multiple levels:
company?.Dept?.Manager?.Name = "Alice"
Result: Less boilerplate, fewer bugs, more readable code.
Coming Up Next
Next, we'll explore Extension Members - the ability to add properties, operators, and static members to types you don't own. Imagine adding a .IsWeekend property to DateTime or custom operators to third-party types.
Previously: Part 1 - The field Keyword - Simplifying Property Logic