|
| 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