C# Nullable Arithmetic — Silent £0.00 from Null BasePrice
A null basePrice in nullable arithmetic returns null, then ?? 0m silently produces £0.00.
20+ years shipping production .NET services in enterprise systems. Lessons pulled from things that broke in production.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- Nullable
wraps any value type with a HasValue flag - Use ?? to provide a default when null, not if statements
- Nullable arithmetic silently returns null — no exception thrown
- 5 bytes vs 4 bytes for int? vs int (one extra byte for bool)
- Accessing .Value on null throws InvalidOperationException, not NullReferenceException
- EF Core maps nullable C# properties directly to nullable SQL columns
Imagine a paper form with a field for 'Date of Birth'. Some forms are filled in completely — the field has a date. But what if someone deliberately left it blank? That blank isn't zero, and it isn't wrong — it genuinely means 'we don't know'. In C#, a regular int or DateTime can't be blank — they always hold a value. Nullable types are how you add that 'intentionally left blank' option to any value type.
Every C# developer eventually hits the same wall: they're modelling real-world data — a database record, a web form, a sensor reading — and the data simply might not exist yet. A customer's loyalty points might be null because they've never made a purchase. A shipment's delivery date is null because it hasn't shipped yet. These aren't errors; they're valid business states. But if you reach for an int or a DateTime, C# won't let you express that state at all — those types must always contain a value.
Nullable types solve this by wrapping any value type in a container that adds one extra possibility: null. This is the difference between asking 'what is your score?' and 'do you even have a score yet?'. Without nullable types, developers resort to sentinel values — using -1 to mean 'no score', or DateTime.MinValue to mean 'no date' — and that produces bugs that are incredibly hard to track down because -1 looks like real data.
By the end of this article you'll understand exactly what int? means under the hood, how to safely read and write nullable values without crashing your app, how the null-coalescing and null-conditional operators make your code cleaner, and the common mistakes that send developers to Stack Overflow at 11pm. You'll also be ready to answer the nullable questions that pop up in virtually every C# interview.
What a Nullable Type Actually Is Under the Hood
When you write int? in C#, the compiler translates it to Nullable<int>. That's not magic — it's a generic struct defined in the .NET base class library with exactly two properties: HasValue (a bool) and Value (the underlying int). That's the whole thing.
This matters for two reasons. First, it means a nullable type is still a value type — it lives on the stack, not the heap. There's no heap allocation, no garbage collector pressure. It's just a slightly bigger struct. Second, it means null for a nullable type doesn't mean 'a null reference' the way it does for a class. It means HasValue is false. The runtime never dereferences a pointer.
Why does that distinction matter? Because it explains the behaviour you'll see: you can assign null, you can compare with null, but if you try to read .Value when HasValue is false, you get an InvalidOperationException — not a NullReferenceException. That different exception type is a clue that something different is happening.
Real-World Nullable Patterns — The Operators That Do the Heavy Lifting
In production code you'll rarely write if (score.HasValue) by hand. C# gives you three operators that handle nullable logic concisely and safely. Learn these and your nullable code will be both shorter and more readable than the HasValue pattern.
The null-coalescing operator (??) returns the left side if it has a value, otherwise the right side. Think of it as 'use this, or fall back to that'. The null-coalescing assignment operator (??=) only assigns if the variable is currently null — perfect for lazy initialisation.
The null-conditional operator (?.) lets you call a method or property on something that might be null, and it short-circuits to null instead of throwing if it is null. This is primarily for reference types, but you'll frequently combine it with ?? when working with nullable value types retrieved from objects.
The as-a-team pattern is: use ?. to safely navigate to a nullable value, then ?? to provide a sensible default. Together they eliminate almost all defensive null-checking boilerplate.
Nullables and Entity Framework — The Database Connection You Must Understand
The single most common place you'll encounter nullable types in professional C# is when mapping database columns. A SQL database column can be NOT NULL or NULL — and your C# model needs to reflect that truthfully. If it doesn't, you're lying to the compiler about your data, and bugs follow.
Entity Framework Core reads nullable properties on your model class and creates nullable columns in the database. Non-nullable properties create NOT NULL columns. This direct mapping means your C# type system is your database schema documentation — get the nullability right in C# and the database reflects reality.
There's a subtler point here too: when EF Core reads a nullable database column and the row contains NULL, it correctly populates your C# property as null. If you'd mapped that column to a non-nullable int, EF Core would throw an exception at runtime because it can't put NULL into an int. A lot of mysterious data-access bugs trace back to exactly this mismatch.
Nullable Types and Pattern Matching — Cleaner State Handling
C# 7+ introduced pattern matching that works beautifully with nullable types. You can check for null directly with the 'is null' and 'is not null' patterns, and you can even switch on nullable values. This leads to code that reads like the business logic itself — not like defensive programming.
Before pattern matching, you'd write if (score.HasValue) { ... } else { ... }. Now you can write if (score is not null) { ... }. It's a small change, but it makes your intent instantly clear: you're checking whether a value exists, not whether a property is true.
Switch expressions take this further. You can match on nullable properties of an object directly, combining property patterns with null checks. This is especially powerful in domain logic like order processing, where the state of an order depends on which nullable timestamps are set.
Common Mistakes With Nullable Types and Exactly How to Fix Them
Nullable types have a small surface area, but there are specific mistakes that come up again and again — even from experienced developers. The two most damaging ones involve blindly accessing .Value and misunderstanding how null propagates through arithmetic.
A third, subtler mistake is using nullable types where you should be using the Null Object Pattern or a default value — nullable is the right tool when absence is meaningful, not when you just want to avoid initialising something.
Understanding these mistakes doesn't just save you from bugs — it makes your intent clearer to the next developer who reads your code. Code that correctly uses nullable types is self-documenting: it says 'this value might legitimately not exist, and we handle that case explicitly'.
Nullable Syntax — The Two Ways to Declare and Why One Is Better
There are two syntaxes for nullable value types: Nullable<T> and the T? shorthand. They compile to identical IL. One of them you should never write in production. Nullable<int> is verbose, clutters code, and tells the next engineer you don't trust the type system. int? is the idiomatic C# way. Use it. The ? suffix signals intent: this variable can be null. No ceremony. For reference types in C# 8+, string? follows the same pattern — it turns on null-state analysis and the compiler will enforce null checks. The old Nullable<T> syntax survives for legacy interop and generic constraints. If you're writing a generic method or a constraint like where T : struct, you'll see Nullable<T> in signatures. That's fine. Everywhere else: use the ? operator. Readability wins.
Accessing Nullable Values — Don’t Get Caught by the Default
You cannot read a nullable value directly. The compiler forces you to check for null. That's a feature, not a bug. There are three ways to access the value. GetValueOrDefault() is the most common — it returns the stored value or the default for that type (0 for int, false for bool, etc.). This is fine when a zero default makes sense. It's a landmine when it doesn't. If a null order count of 0 means 'no orders' and a real zero means 'all orders cancelled', you just lost a bug report. Use the null-conditional operator ?. to short-circuit safely. Or use Value if you've already checked HasValue. Never access .Value without a guard — that throws InvalidOperationException at runtime. Pattern matching with switch or is is cleaner: you match null and valid states explicitly. The GetValueOrDefault(T defaultValue) overload lets you supply your own fallback. Use that when zero is wrong.
GetValueOrDefault() with no argument returns 0. If 0 means 'processed' in your domain and null means 'not yet', you just reported a false positive. Always pass an explicit sentinel value or use pattern matching.The Null Coalescing Operator (??) — Your Shortcut to Ugly Boilerplate
The ?? operator unwraps a nullable into a non-nullable value, substituting a default if the source is null. It's syntactic sugar for a ternary: x != null ? x : defaultValue. But it's better because it's terse and forces you to specify the fallback at the call site. Do not confuse ?? with ?. (null-conditional). ?? returns a value; ?. lets you access members safely. Chaining them is common: customer?.Address?.City ?? "Unknown". That reads: navigate the object graph, return null if anything is null, then substitute. Production note: the right-hand side of ?? is lazily evaluated. That matters when the fallback is an expensive method call or a new allocation. If you write customer?.OrderTotal ?? CalculateDefaultOrder(), that method only runs when OrderTotal is null. Good for performance. Bad for side effects — don't hide a database call or a logging statement there. Keep it pure. The ??= operator (C# 8+) assigns the right side only if the left is null. Perfect for lazy initialization patterns.
Boxing and Unboxing Nullable Types — The Hidden Performance Cost
When you assign a nullable value type to a non-nullable reference (like object), the runtime performs boxing: it wraps the value in a heap object. For nullable types, this boxing behaves differently than you might expect. If the nullable has a value, the runtime boxes the underlying type — not the Nullable
Practical Examples: Assignments by Difficulty Level
Applying nullable types correctly requires matching complexity to context. Beginner: use nullable for optional fields like MiddleName or nullable database columns. Example: string? middleName = null; if (middleName != null) { ... } matches the intent. Intermediate: use pattern matching with nullable enums or result types. Example: StatusCode? result = GetResult(); string message = result switch { 200 => "OK", 404 => "Not Found", null => "Unknown", _ => "Other" }; Advanced: combine nullable with generic constraints or value-task patterns. Example: async TaskNullable.GetValueOrDefault() with explicit default. In Entity Framework, null coalescing in queries (e.g., db.People.Select(p => p.MiddleName ?? "N/A")) translates to SQL COALESCE. Mistmatch difficulty: using null-forgiving operator (!) in simple validation code creates hidden null risks — reserve for interop or tests only.
Nullable Reference Types Deep-Dive
Nullable reference types (NRTs) in C# 8+ enable compile-time null safety. Beyond basic ? annotations, advanced attributes refine null-state analysis. [MaybeNull] indicates a return value may be null even if the type is non-nullable. [NotNullWhen(true)] on a bool-returning method tells the compiler the output parameter is not null when the method returns true. [MemberNotNull(nameof(Property))] ensures a property is initialized after a method call, silencing constructor warnings. These attributes are in System.Diagnostics.CodeAnalysis.
Example: [return: MaybeNull] on a GetValueOrDefault-like method lets callers expect null. TryGetValue(out string? result) with [NotNullWhen(true)] avoids redundant null checks. [MemberNotNull(nameof(Name))] in Initialize() guarantees Name is set, eliminating CS8618 warnings.
Use these to model complex null contracts, especially in libraries or when migrating legacy code.
[MaybeNull], [NotNullWhen], and [MemberNotNull] provide fine-grained control over null-state analysis, reducing runtime null checks and improving code safety.Nullable Analysis in C# 12/13
C# 12 and 13 enhance nullable analysis with constructor warnings and improved null-state tracking. In C# 12, the compiler now warns when a constructor doesn't initialize all non-nullable fields or properties, even if they are set via helper methods. This is part of the CS8618 warning but now more comprehensive. C# 13 introduces field keyword in properties and better analysis of required members. Null-state analysis now tracks assignments through method calls and control flow more precisely, reducing false positives.
Example: A class with a non-nullable string property must be initialized in every constructor path. If a helper method sets it, use [MemberNotNull] to satisfy the compiler. C# 13's field keyword allows property initializers without backing fields, and the compiler tracks null-state through field assignments.
To leverage these, enable nullable context (#nullable enable) and treat warnings as errors. Use required modifier for properties that must be set during initialization.
required and [SetsRequiredMembers] in constructors eliminates common null reference exceptions in object initialization. This is especially useful in ASP.NET Core models and DTOs.DBNull vs null vs Nullable in Database Contexts
When working with databases, three distinct null representations exist: DBNull.Value, null, and Nullable. DBNull is a class used by ADO.NET to represent a database NULL. null is the C# reference type null. Nullable (e.g., int?) is a value type wrapper allowing null. Confusing them leads to InvalidCastException or silent data loss.
In ADO.NET, SqlDataReader returns DBNull.Value for NULL columns. You must check IsDBNull before casting. Entity Framework Core abstracts this: it maps database NULL to null for reference types and Nullable for value types. However, when using raw SQL or DataTable, you'll encounter DBNull.
Example: Reading a nullable int column: int? value = reader.IsDBNull(0) ? null : reader.GetInt32(0);. Using reader.GetValue(0) as int? fails because DBNull is not int?. The null-coalescing operator ?? works only with null, not DBNull. Convert DBNull to null via Convert.IsDBNull(value) ? null : value.
Best practice: Use EF Core for automatic mapping. For raw ADO.NET, create helper methods to convert DBNull to Nullable.
DBNull (ADO.NET), null (reference types), and Nullable<T> (value types). Use helper methods to safely convert between them in database operations.The Silent Order Cancellation — Nullable Arithmetic in a Discount Engine
decimal? finalAmount = basePrice * (1 - discountPercent / 100). Both basePrice and discountPercent were nullable decimals. When basePrice was null (because a legacy product didn't have a price in the new system), the entire expression silently returned null. The code then assigned that null to a non-nullable decimal via ?? 0m, producing £0.00.decimal actualBase = basePrice ?? 0m; decimal discountFactor = 1 - (discountPercent ?? 0m) / 100; finalAmount = actualBase * discountFactor;. Also added a validation step to log warnings when basePrice was null.- Never let nullable values flow into arithmetic without resolving nulls first.
- Use ?? to provide safe defaults before any calculation involving nullable operands.
- Add explicit logging or validation when a nullable is null but the business expects it to have a value.
- Test discount pipelines with deliberately missing data — sentinel values hide null arithmetic.
GetValueOrDefault() or ?? operator. Add nullable logging before the crash.Console.WriteLine($"HasValue: {myNullable.HasValue}");Console.WriteLine($"Value or fallback: {myNullable ?? 0}");| File | Command / Code | Purpose |
|---|---|---|
| NullableInternals.cs | using System; | What a Nullable Type Actually Is Under the Hood |
| RealWorldNullablePatterns.cs | using System; | Real-World Nullable Patterns |
| EntityFrameworkNullableMapping.cs | using System; | Nullables and Entity Framework |
| PatternMatchingWithNullables.cs | using System; | Nullable Types and Pattern Matching |
| NullableMistakesAndFixes.cs | using System; | Common Mistakes With Nullable Types and Exactly How to Fix T |
| NullableDeclarations.cs | int? orderCount = null; | Nullable Syntax |
| AccessPatterns.cs | int? itemsShipped = null; | Accessing Nullable Values |
| CoalescingPatterns.cs | int? cachedTotal = null; | The Null Coalescing Operator (??) |
| BoxingNullables.cs | int? maybe = 42; | Boxing and Unboxing Nullable Types |
| NullableDifficulty.cs | string? middleName = null; | Practical Examples |
| NullableAttributesExample.cs | using System.Diagnostics.CodeAnalysis; | Nullable Reference Types Deep-Dive |
| CSharp12NullableAnalysis.cs | public class Product | Nullable Analysis in C# 12/13 |
| DBNullVsNullExample.cs | using System; | DBNull vs null vs Nullable |
Key takeaways
Interview Questions on This Topic
What is Nullable
Frequently Asked Questions
20+ years shipping production .NET services in enterprise systems. Lessons pulled from things that broke in production.
That's C# Basics. Mark it forged?
8 min read · try the examples if you haven't