Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C# & .NET (C# 14, .NET 10 LTS)  Module 9 – Null safety

Nullability attributes, guards and migration

Describe null contracts with NotNullWhen and other nullability attributes, refuse null arguments with ThrowIfNull, and move C# code to nullable file by file.

  • Intermediate
  • 20 minutes
  • Examples run with .NET 10.0.401 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: Nullability attributes describe what a method promises · Guard clauses refuse null at run time, where annotations only warn · Migration moves a code base to nullable one file at a time. With a pass: 3 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Express nullability contracts with NotNullWhen, MaybeNullWhen and MemberNotNull
  • Validate arguments with ArgumentNullException.ThrowIfNull and the related guard helpers
  • Plan the migration of an existing code base to nullable reference types, file by file
  • Predict which nullability promises the compiler checks inside a method and which it trusts
  • Measure the warnings of a migration in each nullable context and choose the order of the work

Before you start

What you’ll learn · Before you start

What you will learn

  • Express nullability contracts with NotNullWhen, MaybeNullWhen and MemberNotNull
  • Validate arguments with ArgumentNullException.ThrowIfNull and the related guard helpers
  • Plan the migration of an existing code base to nullable reference types, file by file
  • Predict which nullability promises the compiler checks inside a method and which it trusts
  • Measure the warnings of a migration in each nullable context and choose the order of the work

Before you start

On this page

if (timetable.TryFind("NDLS", out Station? first)) checks the result, and the next line, first.Name, still draws warning CS8602. The compiler does not look inside TryFind, so it cannot know that true means a station was found, and one attribute on the parameter tells it.

You will write such contracts with nullability attributes, refuse bad arguments at run time with the guard helpers, and plan how an existing code base moves to nullable reference types while other work goes on.

Nullability attributes describe what a method promises

stations has two Try methods that do the same work. Only TryFindStation puts [NotNullWhen(true)] on its out parameter:

stations/Program.cs C# · stations/Program.cs
using System.Diagnostics.CodeAnalysis;

// Two Try methods that do the same work. Only TryFindStation tells the compiler what true means.
var timetable = new Timetable();

if (timetable.TryFind("NDLS", out Station? first))
{
    Console.WriteLine(first.Name);
}

if (timetable.TryFindStation("SBC", out Station? second))
{
    Console.WriteLine(second.Name);
}

if (!timetable.TryFindStation("XYZ", out Station? third))
{
    Console.WriteLine($"XYZ: not found, station is null: {third is null}");
}

class Station
{
    public Station(string name) => Name = name;

    public string Name { get; }
}

class Timetable
{
    private readonly Dictionary<string, Station> _byCode = new()
    {
        ["NDLS"] = new Station("New Delhi"),
        ["SBC"] = new Station("KSR Bengaluru"),
    };

    public bool TryFind(string code, out Station? station) => _byCode.TryGetValue(code, out station);

    public bool TryFindStation(string code, [NotNullWhen(true)] out Station? station) =>
        _byCode.TryGetValue(code, out station);
}
Building and running stations Python · stations_session.py
from session import run

# stations calls two Try methods that differ only in [NotNullWhen(true)]. It is built once and run once.
run("""
cd stations
dotnet build -v q
dotnet bin/Debug/net10.0/App.dll
""")

Output

$ cd stations
$ dotnet build -v q
stations/Program.cs(8,23): warning CS8602: Dereference of a possibly null reference. [stations/App.csproj]

Build succeeded.

stations/Program.cs(8,23): warning CS8602: Dereference of a possibly null reference. [stations/App.csproj]
    1 Warning(s)
    0 Error(s)

Time Elapsed …
$ dotnet bin/Debug/net10.0/App.dll
New Delhi
KSR Bengaluru
XYZ: not found, station is null: True

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stations_session.py

The build drew one warning, CS8602 at line 8, the read after TryFind. Step through what the compiler knew after each call:

Interactive · message by message What the call site knows after a Try method Step through the two calls of stations/Program.cs and watch what the compiler knows about the station when true comes back.
What the call site knows after a Try method, step 5 of 5. Messages so far: The Try method to The call site: true, first; The Try method to The call site: true, second.The call siteProgram.csThe Try methodTryFindThe compilerthird: maybe-nulltrue, firsttrue, second

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. TryFind returns true. timetable.TryFind("NDLS", out Station? first) finds New Delhi and returns true. Its signature says only that first is a Station?.
  2. The compiler does not know. The analysis does not look inside TryFind, so for the compiler first is still maybe-null, and first.Name drew CS8602 at line 8.
  3. TryFindStation returns true. TryFindStation("SBC", out Station? second) does the same work, but its parameter carries [NotNullWhen(true)].
  4. The attribute is the promise. The attribute says that true means second is not null, so inside the if the compiler treats it as not-null, and second.Name needed no check.
  5. false keeps the old state. For XYZ the method returned false. The attribute says nothing about false, so third stays maybe-null, and the run confirmed that it was null.

nullability attribute: An attribute from System.Diagnostics.CodeAnalysis, such as NotNullWhen or MemberNotNull, that tells the compiler a null-state promise that ? alone cannot express, such as what holds when a method returns true. live in System.Diagnostics.CodeAnalysis and add what ? alone cannot say: a promise that depends on the result, or on the state of an object. Microsoft’s reference groups them by what they promise:

Group Attributes What they tell the compiler
Preconditions AllowNull, DisallowNull what a caller may pass in
Postconditions MaybeNull, NotNull what holds when the method returns
Conditional postconditions MaybeNullWhen, NotNullWhen, NotNullIfNotNull what holds for one result or one argument
Helper methods MemberNotNull, MemberNotNullWhen which fields a helper sets
Unreachable code DoesNotReturn, DoesNotReturnIf that a call never comes back

The compiler reads an attribute from the method’s signature, never from its body, so it works for methods in other assemblies just as well. That is how the annotations of the .NET libraries reach your code: Dictionary.TryGetValue and string.IsNullOrEmpty already work with flow analysis, and your own Try methods can do the same with one attribute each. Beside NotNullWhen, the two you will meet most are MemberNotNull, for a private helper that sets fields, and MaybeNullWhen(false), for the out value of a generic Try method.

Checkpoint

Check: Nullability attributes and Try methods

  1. Question 1 of 2 bool TryParseCode(string text, out string? code) sets code whenever it returns true. Which attribute lets callers read code.Length inside if (TryParseCode(…)) without a warning?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: [NotNullWhen(true)] on code

    A Try method's result decides the state of its out value, which is a conditional postcondition: NotNullWhen(true) on a nullable out parameter.

  2. Question 2 of 2 TryFindStation("XYZ", out Station? third) returned false. What does the compiler assume about third afterwards?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: maybe-null

    With NotNullWhen(true), the false branch is treated like any read of a Station?: check it before use.

Guard clauses refuse null at run time, where annotations only warn

Nullability attributes cost nothing at run time, and so they stop nothing at run time either: a caller can pass null!, compile with nullable switched off, or call from another .NET language. A public method that must not get null checks its arguments as well. guards.cs sends five orders to Shipping.Label, a public method that starts with three guard clause: A check at the start of a method that returns or throws at once when an input is bad, so the code after it can assume the input is good and stays unindented. More in “if, switch statements and switch expressions”:

guards.cs: three guards at a public method C# · guards/guards.cs
// Guard clauses at a public method: each helper names the argument it refused.
var shipping = new Shipping();
var asha = new Customer("Asha Rao");

Report("a null order", () => shipping.Label(null!, "Pune"));
Report("an order with no customer", () => shipping.Label(new Order("A-101", null), "Pune"));
Report("an empty city", () => shipping.Label(new Order("A-102", asha), ""));
Report("a blank city", () => shipping.Label(new Order("A-103", asha), "   "));
Report("a complete order", () => shipping.Label(new Order("A-104", asha), "Pune"));

static void Report(string what, Func<string> label)
{
    try
    {
        Console.WriteLine($"{what}: {label()}");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"{what}: {e.GetType().Name}: {e.Message}");
    }
}

class Customer
{
    public Customer(string name) => Name = name;

    public string Name { get; }
}

class Order
{
    public Order(string id, Customer? customer)
    {
        Id = id;
        Customer = customer;
    }

    public string Id { get; }

    public Customer? Customer { get; }
}

class Shipping
{
    public string Label(Order order, string city)
    {
        ArgumentNullException.ThrowIfNull(order);
        ArgumentNullException.ThrowIfNull(order.Customer);
        ArgumentException.ThrowIfNullOrWhiteSpace(city);
        return $"{order.Id} for {order.Customer.Name}, {city}";
    }
}

Output

a null order: ArgumentNullException: Value cannot be null. (Parameter 'order')
an order with no customer: ArgumentNullException: Value cannot be null. (Parameter 'order.Customer')
an empty city: ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'city')
a blank city: ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'city')
a complete order: A-104 for Asha Rao, Pune

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run guards.cs

ArgumentNullException.ThrowIfNull(order.Customer) named the parameter order.Customer, although the call passed no name. The helper’s second parameter carries [CallerArgumentExpression], so the compiler fills in the text of the argument. ArgumentException.ThrowIfNullOrWhiteSpace(city) refused the empty and the blank city with the same message.

Helper Throws for Since
ArgumentNullException.ThrowIfNull null .NET 6
ArgumentException.ThrowIfNullOrEmpty null or "" .NET 7
ArgumentException.ThrowIfNullOrWhiteSpace null, "" or only white space .NET 8

After a guard, flow analysis knows the value is there: order.Customer.Name on the last line needed no check.

Key idea

Annotations warn the callers you compile; guards stop the ones you do not. A public method has both: the annotations say what it accepts, and the guards enforce it when the program runs.

Checkpoint

Check: Guard clauses and the argument they name

  1. Question 1 of 2 ArgumentNullException.ThrowIfNull(order.Customer) throws. What is the exception's ParamName?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: order.Customer

    The helper's paramName parameter carries [CallerArgumentExpression("argument")], so a guard names its argument without nameof.

  2. Question 2 of 2 A library method takes string name, and the library has nullable reference types on. Why keep ArgumentNullException.ThrowIfNull(name)?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Callers may compile with nullable off, pass null!, or use another .NET language, and none of them is stopped by an annotation

    Annotations warn the callers you compile, and guards stop the rest; a public method has both.

Migration moves a code base to nullable one file at a time

Turning <Nullable>enable</Nullable> on for an old project can bring hundreds of warnings at once. Microsoft’s nullable migration: Moving code written without nullable reference types to <Nullable>enable</Nullable> in steps: file by file, from the leaf types outward, until no #nullable directive is left. guide splits the work into steps instead, and since nullable warnings do not stop a build, the code keeps building and shipping between them. Walk through the phases:

Timeline · 5 events in order Moving a code base to nullable, phase by phase Walk through the phases in order. Each one leaves the code building, so other work can go on between them.
  1. Phase 1

    Choose a default for the project project file

    Set <Nullable>enable</Nullable> and put #nullable disable at the top of each file not migrated yet, so new files start nullable-aware; or keep the project disabled and add #nullable enable to each file as you migrate it.

  2. Phase 2

    Start with the leaf types order of work

    Begin with the types that depend on no other type of yours, then the types that use them, and so on outward. A caller taken after the types it calls has its warnings fixed once, against declarations that will not change again.

  3. Phase 3

    Warnings or annotations first two passes

    Fix the dereferences that the warnings context reports while the types stay as they are, or annotate the public API in the annotations context first and fix the warnings after it. Each pass then changes only one kind of thing.

  4. Phase 4

    Fix the callers callers

    Each annotation moves warnings to the code that uses the value. Fix them with checks, ? and the attributes of this lesson, and keep ! for the few places where you can show a value is there.

  5. Phase 5

    Finish the migration end state

    The project file keeps <Nullable>enable</Nullable>, and the code keeps no #nullable directive at all. Any = null! or default! that only quieted a warning during the work gets a real value, or its member becomes nullable.

The timeline has 5 events, from Phase 1 to Phase 5.

Whichever order you choose, the work ends in one place: enable in the project file and not one #nullable directive in the code. The phases also explain why migrations stall: every type you annotate moves its warnings to the code that calls it.

Leaf types first

Start with the types that depend on nothing else of yours. Their warnings stay inside them, and once they are annotated, the warnings they cause in their callers are the next files to take.

Checkpoint

Check: Planning a migration to nullable

  1. Question 1 of 2 A Billing app uses a Customers library, which uses a Core library. Which do you migrate first?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Core

    Work bottom-up: each type you annotate moves warnings into its callers, so annotate the callees first.

  2. Question 2 of 2 What marks a finished migration?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: <Nullable>enable</Nullable> in the project file and no #nullable directive left in the code

    At the end the directives go, enable stays as the project setting, and any null! that only quieted a warning gets a real value.

Each attribute with the warning it removes, compiled Pro: opens with a Pro, Premium or Ultimate pass

Every attribute changes what the compiler reports, at the call site or inside the method. Each one below was built on the .NET 10 SDK, so you can see the warnings it removes and the one promise the compiler checks for you.

In this part:

  • MemberNotNull removes four CS8618 warnings, measured
  • Four more attributes, and the promise the compiler checks
  • MaybeNullWhen for generic Try methods
  • Choosing the attribute: what you want to say, and where this lesson said it

6 runnable examples (C#, Python) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Guards in depth: what they name, when they fire, where they belong Pro: opens with a Pro, Premium or Ultimate pass

A guard has two jobs: to stop a bad value before any work is done, and to name it so that the report leads straight to the bug. Both are measured below, together with the places where a guard pays for itself and where it does not.

In this part:

  • What ParamName holds, measured
  • A guard at the entry, against a crash deep inside
  • Where guards belong: a decision table

2 runnable examples (C#) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

A migration measured: one library in every nullable context Pro: opens with a Pro, Premium or Ultimate pass

Advice about migration is easy to give and hard to judge. This part builds one small library in every nullable context and through two steps of its migration, and counts the warnings at each step.

In this part:

  • The library, built six ways, with every warning counted
  • What the counts say about the order of work

9 runnable examples (C#, CSPROJ, Python) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Practise: annotate a name cache

cache treats nullable warnings as errors. NameCache.cs is the helper to annotate and guard, and Program.cs holds the call sites and six checks; leave it as it is. As it starts, the build fails with five errors:

cache/App.csproj CSPROJ · cache/App.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <WarningsAsErrors>nullable</WarningsAsErrors>
  </PropertyGroup>

</Project>
cache/NameCache.cs C# · cache/NameCache.cs
using System.Diagnostics.CodeAnalysis;

// Practice: annotate NameCache and add guard clauses, without changing what it does, until the project builds
// with nullable warnings as errors and Program.cs prints "6 of 6 checks pass".
public class NameCache
{
    private Dictionary<string, string> _names;

    public NameCache() => Initialize();

    public NameCache(Dictionary<string, string> seed)
    {
        Initialize();
        foreach (var (code, name) in seed)
        {
            _names[code] = name;
        }
    }

    public int Hits { get; private set; }

    public bool TryGet(string code, out string? name) => _names.TryGetValue(code, out name);

    public string GetOrAdd(string code, Func<string, string> load)
    {
        if (_names.TryGetValue(code, out string? name))
        {
            Hits++;
            return name;
        }

        name = load(code);
        _names[code] = name;
        return name;
    }

    private void Initialize()
    {
        _names = [];
        Hits = 0;
    }
}
cache/Program.cs C# · cache/Program.cs
// The call sites and checks of the practice. Leave this file as it is.
var cache = new NameCache(new() { ["P-1"] = "Masala tea", ["P-2"] = "Filter coffee" });

(string Check, Func<string> Run, string Expected)[] checks =
[
    ("TryGet finds P-1", () => cache.TryGet("P-1", out string? name) ? $"{name.Length} characters" : "missing", "10 characters"),
    ("TryGet misses P-9", () => cache.TryGet("P-9", out string? name) ? name : "missing", "missing"),
    ("GetOrAdd loads P-3 once", () => $"{cache.GetOrAdd("P-3", Load)} / {cache.GetOrAdd("P-3", Load)} / hits {cache.Hits}", "Lemon soda / Lemon soda / hits 1"),
    ("a null code", () => cache.GetOrAdd(null!, Load), "throws ArgumentNullException for code"),
    ("a blank code", () => cache.GetOrAdd("  ", Load), "throws ArgumentException for code"),
    ("a null loader", () => cache.GetOrAdd("P-4", null!), "throws ArgumentNullException for load"),
];

int passed = 0;
foreach (var (check, run, expected) in checks)
{
    string got;
    try
    {
        got = run();
    }
    catch (ArgumentException e)
    {
        got = $"throws {e.GetType().Name} for {e.ParamName}";
    }
    catch (Exception e)
    {
        got = $"throws {e.GetType().Name}";
    }

    bool ok = got == expected;
    if (ok)
    {
        passed++;
    }

    Console.WriteLine(ok ? $"{check}: ok" : $"{check}: FAIL, {got} instead of {expected}");
}

Console.WriteLine($"{passed} of {checks.Length} checks pass");
return passed == checks.Length ? 0 : 1;

static string Load(string code) => code == "P-3" ? "Lemon soda" : "Unknown";
Building cache Python · cache_session.py
from session import run

# The practice project as it starts: built with nullable warnings as errors. Its errors are sorted, each shown once,
# because the compiler can report them in a different order from one build to the next.
run("""
cd cache
dotnet build -v q | grep -E "error CS|Error\\(s\\)" | sort -u
""")

Output

$ cd cache
$ dotnet build -v q | grep -E "error CS|Error\(s\)" | sort -u
    5 Error(s)
cache/NameCache.cs(11,12): error CS8618: Non-nullable field '_names' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. [cache/App.csproj]
cache/NameCache.cs(16,13): error CS8602: Dereference of a possibly null reference. [cache/App.csproj]
cache/NameCache.cs(9,12): error CS8618: Non-nullable field '_names' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. [cache/App.csproj]
cache/Program.cs(6,75): error CS8602: Dereference of a possibly null reference. [cache/App.csproj]
cache/Program.cs(7,33): error CS8603: Possible null reference return. [cache/App.csproj]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cache_session.py

Make cache build and pass its checks

Copy the cache folder. Tell the compiler which field Initialize sets (the helper-methods row of the table of attributes) and what true from TryGet promises, and add guards to GetOrAdd for a missing or blank code and a missing loader. Run dotnet build until it reports 0 errors, then dotnet run until it prints 6 of 6 checks pass.

Then try the self-check, which covers the attributes, the guards and the migration plan and picks the topic you have answered least well first.

Practice that adapts to you

Practise this lesson

9 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 9 Which namespace holds NotNullWhen, MaybeNullWhen and MemberNotNull?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: System.Diagnostics.CodeAnalysis

    The attributes of null-state analysis are in System.Diagnostics.CodeAnalysis, beside other analysis attributes.

  2. Question 2 of 9 Read returns string?. Does the read of input.Length draw a warning?

    Read the code, then choose one answer.

    string? input = Read();
    if (!string.IsNullOrEmpty(input))
    {
        Console.WriteLine(input.Length);
    }
    Show the answer: Question 2 of 9

    Answer: No: IsNullOrEmpty is annotated, so a false result proves input is not null

    Annotated library methods work with flow analysis out of the box, which is what your own helpers gain from the same attributes.

  3. Question 3 of 9 A private void EnsureLoaded() assigns the field _items and is called from three methods. Which attribute group fits it?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: Helper methods: MemberNotNull

    A helper that initializes fields belongs to the helper-method group of the table: MemberNotNull and MemberNotNullWhen.

  4. Question 4 of 9 Which of the three guard helpers arrived last, in .NET 8?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: ArgumentException.ThrowIfNullOrWhiteSpace

    All three exist on .NET 10, the version this track uses; code for older runtimes uses only the ones they have.

  5. Question 5 of 9 What does ArgumentException.ThrowIfNullOrWhiteSpace(city) do when city is " "?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: It throws ArgumentException

    ThrowIfNullOrWhiteSpace throws ArgumentNullException for null and ArgumentException for an empty or blank string.

  6. Question 6 of 9 After ArgumentNullException.ThrowIfNull(order.Customer); does order.Customer.Name draw CS8602?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: No: once the guard returns, the compiler treats the argument as not-null

    A guard both stops null at run time and removes the warning at compile time: after it returns, the argument counts as not-null.

  7. Question 7 of 9 Why does annotating a type create new warnings in other files?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: Its callers can now see which values may be null, and the compiler checks their reads

    That is the reason to migrate from the leaf types outward, so that each caller is fixed once.

  8. Question 8 of 9 A library team wants to publish annotated signatures before it fixes the warnings inside. Which nullable context does it set first?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: annotations

    Annotations first suits libraries, whose callers then see the right contracts; warnings first suits hardening running code.

  9. Question 9 of 9 A team sets <Nullable>enable</Nullable> and puts #nullable disable at the top of every file not yet migrated. What does that choice guarantee?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: Every new file starts nullable-aware, so the files left to migrate can only become fewer

    With enable as the default, the number of files that still carry #nullable disable is a progress measure that never goes up.

Interview questions

Fresher · Why validate arguments at run time when nullable reference types already warn?

The warnings exist only at compile time, and only for code compiled with nullable reference types on. A caller in a project with nullable switched off sees no warning, a caller can pass null!, and code in another .NET language or reached through reflection is not checked at all. The method itself still runs whatever arrives. A guard clause such as ArgumentNullException.ThrowIfNull(order) stops a null at the first line, before any work is done, and names the argument in the exception. Annotations and guards answer different questions: the annotation tells callers what is allowed, and the guard enforces it.

  1. Fresher to mid

    Callers of your method bool TryGetZone(string pin, out Zone? zone) check zone for null again after a true result, because the compiler warns CS8602 otherwise. How do you change the API so that callers need no extra check, and how do you make sure the new promise is true?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Senior to staff

    A 40-project solution written before C# 8 is to move to nullable reference types without stopping feature work. Lay out the plan: the order of projects, the project and file settings, how you measure progress, and when nullable warnings become errors.

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    A colleague asks you to delete the ArgumentNullException.ThrowIfNull calls in a library, because nullable reference types already prevent nulls. Which guards would you keep, which would you remove, and why?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Nullability attributes from System.Diagnostics.CodeAnalysis tell the compiler what ? cannot say, such as [NotNullWhen(true)] on the out parameter of a Try method, so its callers need no extra check.
  • Guards such as ArgumentNullException.ThrowIfNull stop null when the program runs, name the argument through CallerArgumentExpression, and let flow analysis treat the value as not-null afterwards.
  • A migration goes file by file from the leaf types outward, in the warnings or the annotations context first, and ends with <Nullable>enable</Nullable> and no #nullable directives.

Terms in this lesson

nullability attribute
An attribute from System.Diagnostics.CodeAnalysis, such as NotNullWhen or MemberNotNull, that tells the compiler a null-state promise that ? alone cannot express, such as what holds when a method returns true.
nullable migration
Moving code written without nullable reference types to <Nullable>enable</Nullable> in steps: file by file, from the leaf types outward, until no #nullable directive is left.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.