Skip to content

Commit a54423c

Browse files
committed
Phase 2: tutorial
4. `fundamentals/tutorials/nullable-reference-types.md` — apply `template-tutorial.md`. Source: [docs/csharp/tutorials/nullable-reference-types.md](docs/csharp/tutorials/nullable-reference-types.md). Restructure to template (checklist, Prerequisites, numbered task H2s, "Get the code", "Next step"). Trim concept-duplicating prose; replace with cross-links.
1 parent c2ac169 commit a54423c

1 file changed

Lines changed: 180 additions & 0 deletions

File tree

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
---
2+
title: "Tutorial: Express your design intent with nullable and non-nullable reference types"
3+
description: Build a small survey app that uses nullable and non-nullable reference types to declare which references can be null and have the compiler enforce that intent.
4+
ms.date: 05/04/2026
5+
ms.topic: tutorial
6+
ms.subservice: null-safety
7+
ai-usage: ai-assisted
8+
9+
#customer intent: As a C# developer, I want to use nullable and non-nullable reference types in a real design so that the compiler catches null-handling mistakes for me.
10+
---
11+
# Tutorial: Express your design intent with nullable and non-nullable reference types
12+
13+
> [!TIP]
14+
> **New to nullable reference types?** Read [Nullable reference types](../null-safety/nullable-reference-types.md) first. This tutorial assumes you understand the difference between non-nullable and nullable reference types and how the compiler tracks null-state.
15+
>
16+
> **Coming from another language?** If you've used Kotlin's nullable types, TypeScript's `strictNullChecks`, or Swift's optionals, the conceptual model maps directly. The exercise here is about *expressing design intent*, not learning the syntax.
17+
18+
In this tutorial, you build a small library that models running a survey. The data has two kinds of "missing" values that nullable reference types let you distinguish:
19+
20+
- A *survey question* must always be present. The list of questions and the text of each question can never be `null`.
21+
- A *response to a question* might be missing. Respondents can decline to answer some or all questions, and the model should make that explicit.
22+
23+
You declare those rules with non-nullable and nullable reference types. The compiler then warns whenever the code's behavior doesn't match the design.
24+
25+
In this tutorial, you:
26+
27+
> [!div class="checklist"]
28+
>
29+
> - Create a console app that has nullable reference types enabled.
30+
> - Build the survey with non-nullable reference types for required values.
31+
> - Generate respondents that use nullable reference types for missing answers.
32+
> - Read the survey results without writing any null checks the compiler hasn't asked for.
33+
34+
## Prerequisites
35+
36+
[!INCLUDE [Prerequisites](../../../../includes/prerequisites-basic.md)]
37+
38+
This tutorial assumes you're familiar with C# and either Visual Studio or the .NET CLI.
39+
40+
## Create the application and enable nullable reference types
41+
42+
Create a new console application named `NullableIntroduction`:
43+
44+
```dotnetcli
45+
dotnet new console -n NullableIntroduction
46+
cd NullableIntroduction
47+
```
48+
49+
Open `NullableIntroduction.csproj` and confirm the `<Nullable>enable</Nullable>` element is set in the `PropertyGroup`. Templates from .NET 6 onward include it by default; add it manually if it's missing:
50+
51+
:::code language="xml" source="snippets/NullableIntroduction/NullableIntroduction.csproj":::
52+
53+
With the feature enabled, every reference type variable is non-nullable unless you append `?`. The compiler issues warnings when your code's null-handling doesn't match those declarations.
54+
55+
## Design the survey types
56+
57+
Three classes model the survey:
58+
59+
- `SurveyQuestion` — one question. The text and question type are required.
60+
- `SurveyRun` — the collection of questions plus the list of respondents.
61+
- `SurveyResponse` — one respondent's answers, which might be missing.
62+
63+
Each type uses non-nullable reference types for required values and nullable reference types where missing values are part of the design.
64+
65+
## Build the survey questions
66+
67+
Replace the contents of `SurveyQuestion.cs` with the following code. The text and the question type are non-nullable, so the compiler requires the constructor to initialize both:
68+
69+
:::code language="csharp" source="snippets/NullableIntroduction/SurveyQuestion.cs":::
70+
71+
The constructor parameters are non-nullable reference types, so the compiler warns the caller if either argument might be `null`.
72+
73+
Next, add a `SurveyRun` class to hold the list of questions:
74+
75+
```csharp
76+
namespace NullableIntroduction;
77+
78+
public class SurveyRun
79+
{
80+
private List<SurveyQuestion> surveyQuestions = new();
81+
82+
public void AddQuestion(QuestionType type, string question) =>
83+
AddQuestion(new SurveyQuestion(type, question));
84+
85+
public void AddQuestion(SurveyQuestion surveyQuestion) =>
86+
surveyQuestions.Add(surveyQuestion);
87+
}
88+
```
89+
90+
The `surveyQuestions` field is a non-nullable `List<SurveyQuestion>`. Initializing it at the declaration satisfies the non-nullable contract. Both `AddQuestion` overloads accept non-nullable parameters, so the compiler enforces that callers don't pass `null`.
91+
92+
In `Program.cs`, create a `SurveyRun` and add three questions:
93+
94+
:::code language="csharp" source="snippets/NullableIntroduction/Program.cs" id="AddQuestions":::
95+
96+
To see how the compiler enforces non-nullable parameters, try adding the following line and rebuilding:
97+
98+
```csharp
99+
surveyRun.AddQuestion(QuestionType.Text, default);
100+
```
101+
102+
The compiler issues warning *CS8625* because `default` evaluates to `null` for a reference type, and `AddQuestion` expects a non-nullable `string`. Remove the line before continuing.
103+
104+
## Create respondents and capture answers
105+
106+
A respondent's answers are different from a survey's questions: any individual answer might be missing, and a respondent might decline to participate at all. Both are valid states, and both are expressed with `null`.
107+
108+
Add a `SurveyResponse` class. Start with the always-required `Id` property and a constructor that initializes it:
109+
110+
```csharp
111+
namespace NullableIntroduction;
112+
113+
public class SurveyResponse
114+
{
115+
public int Id { get; }
116+
117+
public SurveyResponse(int id) => Id = id;
118+
}
119+
```
120+
121+
Add a static factory method that creates respondents with a random ID:
122+
123+
:::code language="csharp" source="snippets/NullableIntroduction/SurveyResponse.cs" id="Random":::
124+
125+
Next, add the method that asks the survey to a respondent. Store the answers in a nullable dictionary so the type itself communicates that the respondent might decline:
126+
127+
:::code language="csharp" source="snippets/NullableIntroduction/SurveyResponse.cs" id="AnswerSurvey":::
128+
129+
The `surveyResponses` field is `Dictionary<int, string>?`. Anywhere the field is dereferenced without first checking against `null`, the compiler issues a warning. Inside `AnswerSurvey`, the compiler tracks that `surveyResponses` is *not-null* immediately after the `new` expression, so the loop body needs no extra check.
130+
131+
Add a method on `SurveyRun` that builds up a list of respondents until enough consent to participate:
132+
133+
:::code language="csharp" source="snippets/NullableIntroduction/SurveyRun.cs" id="PerformSurvey":::
134+
135+
The `respondents` field is `List<SurveyResponse>?`—it's `null` until the survey runs.
136+
137+
Call `PerformSurvey` from `Main`:
138+
139+
:::code language="csharp" source="snippets/NullableIntroduction/Program.cs" id="RunSurvey":::
140+
141+
## Examine the survey results
142+
143+
To report results, expose a few helpers from `SurveyResponse` and `SurveyRun`. On `SurveyResponse`, add expression-bodied members that handle the nullable dictionary:
144+
145+
:::code language="csharp" source="snippets/NullableIntroduction/SurveyResponse.cs" id="SurveyStatus":::
146+
147+
`AnsweredSurvey` checks the field against `null`. `Answer` uses the `?.` operator to dereference safely and the `??` operator to substitute a non-null fallback. The method's return type is non-nullable `string`, so callers don't need null checks.
148+
149+
On `SurveyRun`, add expression-bodied members that expose the list of participants and questions:
150+
151+
:::code language="csharp" source="snippets/NullableIntroduction/SurveyRun.cs" id="RunReport":::
152+
153+
`AllParticipants` returns a non-nullable sequence even though `respondents` might be `null`. The `??` operator substitutes `Enumerable.Empty<SurveyResponse>()` when the field hasn't been populated yet. If you remove the `??` clause, the compiler warns that the method might return `null` despite a non-nullable return type.
154+
155+
Finally, write the report at the bottom of `Main`:
156+
157+
:::code language="csharp" source="snippets/NullableIntroduction/Program.cs" id="WriteAnswers":::
158+
159+
Notice that no null check is needed for `participant`, `surveyRun.Questions`, or `surveyRun.GetQuestion(i)`. The types declare those values as non-nullable, so the compiler treats them as *not-null* throughout the loop.
160+
161+
Run the application:
162+
163+
```dotnetcli
164+
dotnet run
165+
```
166+
167+
The output is different on each run because respondents are generated randomly, but every line either reports a participant's answers or notes that they declined.
168+
169+
## Get the code
170+
171+
The finished sample is in the [csharp/NullableIntroduction](https://github.com/dotnet/samples/tree/main/csharp/NullableIntroduction) folder of the [dotnet/samples](https://github.com/dotnet/samples) repository.
172+
173+
Experiment by changing types between nullable and non-nullable. Removing a `?` where the design allows missing values produces compiler warnings that point to every place the missing value matters.
174+
175+
## Related content
176+
177+
- [Nullable reference types](../null-safety/nullable-reference-types.md)
178+
- [Resolve nullable warnings](../null-safety/resolve-warnings.md)
179+
- [Nullable migration strategies](../null-safety/migration-strategies.md)
180+
- [Working with nullable reference types in EF Core](/ef/core/miscellaneous/nullable-reference-types)

0 commit comments

Comments
 (0)