Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9,516 changes: 4,764 additions & 4,752 deletions .openpublishing.redirection.csharp.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/core/compatibility/sdk/6.0/csharp-template-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Starting in .NET 6, the project templates that ship with the .NET SDK use the la
- [Global using directives](/dotnet/csharp/language-reference/language-specification/namespaces#1452-global-using-alias-directives)
- [File-scoped namespaces](/dotnet/csharp/language-reference/language-specification/namespaces#143-namespace-declarations)
- [Target-typed new expressions](/dotnet/csharp/language-reference/language-specification/expressions#128172-object-creation-expressions)
- [Nullable reference types](../../../../csharp/nullable-references.md)
- [Nullable reference types](../../../../csharp/fundamentals/null-safety/nullable-reference-types.md)
- [Async Main return values](../../../../csharp/fundamentals/program-structure/main-command-line.md#main-return-values)

Some of the latest C# language features are not supported by previous target frameworks, so you might experience issues in the following scenarios:
Expand Down
2 changes: 1 addition & 1 deletion docs/core/extensions/windows-service.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ After successfully adding the packages, your project file should now contain the

## Update project file

This worker project makes use of C#'s [nullable reference types](../../csharp/nullable-references.md). To enable them for the entire project, update the project file accordingly:
This worker project makes use of C#'s [nullable reference types](../../csharp/fundamentals/null-safety/nullable-reference-types.md). To enable them for the entire project, update the project file accordingly:

:::code language="xml" source="snippets/workers/windows-service/App.WindowsService.csproj" range="1-7,12-20" highlight="5":::

Expand Down
2 changes: 1 addition & 1 deletion docs/core/whats-new/dotnet-5.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ ASP.NET Core 5.0 is based on .NET 5 but retains the name "Core" to avoid confusi
- [App trimming](https://devblogs.microsoft.com/dotnet/app-trimming-in-net-5)
- Windows Arm64 and Arm64 intrinsics
- Tooling support for dump debugging
- The runtime libraries are 80% annotated for [nullable reference types](../../csharp/nullable-references.md)
- The runtime libraries are 80% annotated for [nullable reference types](../../csharp/fundamentals/null-safety/nullable-reference-types.md)
- Performance improvements:
- [Garbage Collection (GC)](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-5/#gc)
- [System.Text.Json](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-5/#json)
Expand Down
2 changes: 1 addition & 1 deletion docs/core/whats-new/dotnet-core-3-0.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ C# 8.0 is also part of this release, which includes the [nullable reference type

Tutorials related to C# 8.0 language features:

- [Tutorial: Express your design intent more clearly with nullable and non-nullable reference types](../../csharp/tutorials/nullable-reference-types.md)
- [Tutorial: Express your design intent more clearly with nullable and non-nullable reference types](../../csharp/fundamentals/tutorials/nullable-reference-types.md)
- [Tutorial: Generate and consume async streams using C# 8.0 and .NET Core 3.0](../../csharp/asynchronous-programming/generate-consume-asynchronous-stream.md)
- [Tutorial: Use pattern matching to build type-driven and data-driven algorithms](../../csharp/fundamentals/tutorials/pattern-matching.md)

Expand Down
91 changes: 91 additions & 0 deletions docs/csharp/fundamentals/null-safety/migration-strategies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
title: Nullable migration strategies
description: Learn how to enable nullable reference types in an existing C# codebase. Choose a default context, address warnings progressively, and fully migrate.
ms.date: 05/04/2026
ms.topic: concept-article
ms.subservice: null-safety
ai-usage: ai-assisted
---
# Nullable migration strategies

> [!TIP]
> **Starting a new project?** New projects created from .NET 6 or later templates already have `<Nullable>enable</Nullable>` set. You don't need a migration strategy—skip to [Resolve nullable warnings](resolve-warnings.md).
>
> **Maintaining an existing codebase?** Read [Nullable reference types](nullable-reference-types.md) first to understand contexts, annotations, and null-state. This article assumes you're familiar with those concepts and ready to plan a rollout.

When you turn on nullable reference types in a project that wasn't built for the feature, the compiler produces many warnings at once. Migration is about *sequencing* the work: choosing a default context, exposing warnings file by file or section by section, and converging on `<Nullable>enable</Nullable>` for the whole project. The right sequence depends on how active the codebase is and how much risk you can take in a single pass.

The end state is the same in every case—the project sets `<Nullable>enable</Nullable>` and contains no `#nullable` preprocessor directives.

## Choose a default context

The nullable context has two independent flags: *annotations* (whether `?` declares a nullable reference type) and *warnings* (whether the compiler emits diagnostics). You set them together as a single `<Nullable>` value:

| Default value | Annotations | Warnings | Best for |
| --------------------- | :---------: | :------: | --------------------------------------------------------------------- |
| `disable` *(implicit)* | off | off | Stable libraries that won't take new feature work in this pass. |
| `enable` | on | on | Active codebases with frequent new files. New code starts opted in. |
| `warnings` | off | on | Two-phase migration: address warnings first, annotate later. |
| `annotations` | on | off | Annotate the public API before fixing the internal warnings. |

Pick the strategy that makes the next file you create do the right thing automatically:

- **Disable as the default.** Add `#nullable enable` at the top of each file as you migrate it. Existing files stay nullable-oblivious until you touch them. This is the lowest-friction option for stable libraries because new feature work is rare.
- **Enable as the default.** Set `<Nullable>enable</Nullable>` and add `#nullable disable` at the top of every file you haven't migrated yet. Every new file is nullable-aware from the start, so the migration backlog can only shrink. This is the better choice when development is active.
- **Warnings as the default.** Choose this default for a two-phase migration: address warnings while every reference type is still treated as oblivious, then turn on annotations. The two-phase split keeps each step's diff focused.
- **Annotations as the default.** Start by annotating your public API (`?` on members that allow `null`) before chasing warnings. The compiler doesn't emit warnings yet, so you can settle the API surface without distraction.

Your project file controls the global default. The `#nullable` preprocessor directives override it locally:

:::code language="xml" source="snippets/migration-strategies/project-snippet.xml":::

:::code language="csharp" source="snippets/migration-strategies/Program.cs" id="DirectiveOverrides":::

## Migrate file by file

The most predictable way to migrate a large project is to enable warnings or annotations file by file. The pattern is the same regardless of which default you picked:

1. Pick a file. Start with the deepest leaf types in your dependency graph, then move outward. Annotating a type causes new warnings in its callers, so working bottom-up minimizes rework.
1. Add the `#nullable` directive that opts the file into the new behavior. Use `#nullable enable` if you want both flags. Use `#nullable enable warnings` for warning-only.
1. Address the warnings in the file using the techniques in [Resolve nullable warnings](resolve-warnings.md).
1. Repeat for the next file.
1. When every file in the project has its directive, remove the directives and set `<Nullable>enable</Nullable>` at the project level.

If your codebase already has `<Nullable>enable</Nullable>` and you're driving the *opposite* direction—suppressing warnings in unmigrated files until you're ready—use `#nullable disable` to opt files out, then remove the suppressions one at a time.

## Migrate in two phases

A two-phase migration separates the two kinds of work that nullable reference types involve:

1. **Phase 1 — Address warnings.** Set the project default to `warnings`. Reference types remain nullable-oblivious, so the type system doesn't change yet. The compiler emits warnings everywhere your existing code might already throw a <xref:System.NullReferenceException?displayProperty=nameWithType>. Add null checks, restructure flow, or apply attributes until the project is warning-clean. Each fix makes the production code more resilient even before annotations exist.
1. **Phase 2 — Add annotations.** Switch the project default to `enable`. Reference types are now non-nullable by default, and `var` locals become nullable. New warnings reflect declarations that don't match how the variables are used. Add `?` to types that should allow `null`. Tighten APIs that should require non-null inputs.

The advantage of separating the phases is that each diff is smaller and reviewable. Phase 1 changes only behavior; phase 2 changes only types. The disadvantage is that you visit each file twice. For mature, stable code where every change carries risk, the two passes are usually worth it.

## Generated code is excluded

The compiler treats files marked as generated as if the nullable context were disabled, regardless of the project's setting. A file is considered generated when any of the following is true:

- An `.editorconfig` rule sets `generated_code = true` for the file.
- The first comment in the file contains `<auto-generated>` or `<auto-generated/>`.
- The file name starts with `TemporaryGeneratedFile_`.
- The file name ends with `.designer.cs`, `.generated.cs`, `.g.cs`, or `.g.i.cs`.

Generators that produce nullable-aware output can opt back in by emitting `#nullable enable` at the top of the generated file.

## When you're done

After every file participates in the project default and the `<Nullable>enable</Nullable>` element is set:

- Remove every `#nullable` directive in your source.
- Remove `null!` and `default!` initializers that you added only to silence warnings during migration. Replace them with proper initialization, or change the member type to nullable.
- Spot-check the public API. Every member that returns or accepts `null` should be annotated with `?`. The annotations are part of your contract once the package ships.

You're now in the same state as new projects: nullable reference types are part of the type system, and any new warnings reflect a real mismatch between declarations and code. Use [Resolve nullable warnings](resolve-warnings.md) to address them as they come up.

## Related content

- [Nullable reference types](nullable-reference-types.md)
- [Resolve nullable warnings](resolve-warnings.md)
- [Nullable static analysis attributes](../../language-reference/attributes/nullable-analysis.md)
- [Working with nullable reference types in EF Core](/ef/core/miscellaneous/nullable-reference-types)
126 changes: 126 additions & 0 deletions docs/csharp/fundamentals/null-safety/nullable-reference-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
---
title: Nullable reference types
description: Use nullable reference types to express your intent about which variables can be null. The compiler then warns when your code might dereference null.
ms.date: 05/04/2026
ms.topic: concept-article
ms.subservice: null-safety
ai-usage: ai-assisted
---
# Nullable reference types

> [!TIP]
> **New to developing software?** Start with the [Get started](../../tour-of-csharp/tutorials/index.md) tutorials first. You'll see warnings about possible `null` values once you compile any project that has nullable reference types enabled.
>
> **Experienced in another language?** If you've worked with Kotlin's nullable types, TypeScript's `strictNullChecks`, or Swift's optionals, the model is familiar. C# uses static analysis and warning diagnostics instead of a separate type. Skim [Express intent with annotations](#express-intent-with-annotations) and [Null-state analysis](#null-state-analysis), then jump to the [Tutorial: Express your design intent with nullable and non-nullable reference types](../tutorials/nullable-reference-types.md) to apply the feature.

*Nullable reference types* are a group of features that minimize the chance your code throws <xref:System.NullReferenceException?displayProperty=nameWithType>. You declare which variables are intended to hold `null` and which aren't, and the compiler warns when those declarations don't match how your code uses them. The runtime behavior of your program is unchanged—nullable reference types are entirely a compile-time feature.

Three building blocks work together:

- *Variable annotations* (`string` vs. `string?`) express which references are intended to allow `null`.
- *Null-state analysis* tracks whether the value of an expression is known to be `null` or known not to be `null` at each point in your code.
- *Attributes* on APIs describe more nuanced contracts, such as "this argument can be `null`, but the return value is null only when the argument is null."

The compiler combines these signals to produce diagnostics. Warnings on a non-nullable variable mean the variable might receive `null`. Warnings on a nullable variable mean the code might dereference it without a null check. Either kind of warning is a hint that the code's behavior doesn't match its stated design.

## Enable nullable reference types

For new projects targeting .NET 6 or later, the `<Nullable>enable</Nullable>` element is in the project template by default. To enable the feature in an existing project, add it manually:

:::code language="xml" source="snippets/nullable-reference-types/project-snippet.xml":::

For migration approaches that enable the feature gradually—file by file or warning-only first—see [Nullable migration strategies](migration-strategies.md).

## Express intent with annotations

When the feature is enabled, every reference type variable is *non-nullable* by default. Append `?` to declare a *nullable* reference type:

:::code language="csharp" source="snippets/nullable-reference-types/Program.cs" id="Annotations":::

The annotation doesn't change the runtime type. `string` and `string?` are both <xref:System.String?displayProperty=nameWithType>. The `?` is a hint to the compiler about your intent. That intent shapes the warnings the compiler produces:

- A non-nullable variable has a default *null-state* of *not-null*. The compiler warns if you assign a value that might be `null`.
- A nullable variable has a default *null-state* of *maybe-null*. The compiler warns if you dereference the variable without first checking it.

Use the annotation to make required and optional values visible in the type system. A `Person` whose name must always be set is different from a `Person` whose middle name might be missing:

:::code language="csharp" source="snippets/nullable-reference-types/Program.cs" id="DesignIntent":::

The compiler enforces the difference: code that passes a maybe-null value where a non-nullable one is expected produces a warning. The compiler also warns when a constructor leaves a non-nullable member uninitialized.

## Null-state analysis

The compiler tracks the *null-state* of every expression. The state is one of two values:

- *not-null* — the expression is known to be not `null`.
- *maybe-null* — the expression might be `null`.

Local variables move between these states as your code runs. Assigning a non-null value or checking against `null` makes a variable *not-null*. Assigning a *maybe-null* expression or returning from a method without further information makes it *maybe-null*:

:::code language="csharp" source="snippets/nullable-reference-types/Program.cs" id="NullStateTracking":::

In the preceding example, the first dereference produces a warning because `message` is *maybe-null*. After the assignment to a non-null literal, the compiler knows `message` is *not-null*, so the second dereference is safe. The third statement warns because `originalMessage` was captured while it was still `null`.

Null-state analysis works across `if` checks, pattern matching, and control flow that loops or returns early:

:::code language="csharp" source="snippets/nullable-reference-types/Program.cs" id="FlowAnalysis":::

The analysis doesn't trace into the bodies of methods. If you need a method to communicate null-state to its callers, use [nullable analysis attributes](../../language-reference/attributes/nullable-analysis.md) on its signature.

## Override the warnings with `!`

Sometimes you know more than the compiler. The *null-forgiving operator* `!` declares that an expression is *not-null*, even when the analysis says otherwise:

:::code language="csharp" source="snippets/nullable-reference-types/Program.cs" id="NullForgiving":::

Use `!` sparingly. Each occurrence is a place the compiler can no longer protect you. Prefer adding a null check, restructuring the code, or annotating the relevant API so the compiler reaches the right conclusion on its own.

## Attributes that describe API contracts

Annotations on a parameter or return type aren't always expressive enough. A method might accept a possibly-null argument but guarantee a non-null result. A test method might return `true` only when its argument isn't null. Use the [nullable analysis attributes](../../language-reference/attributes/nullable-analysis.md) to convey these contracts:

:::code language="csharp" source="snippets/nullable-reference-types/Program.cs" id="NullAnalysisAttributes":::

The <xref:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute> tells the compiler that when `IsPresent` returns `true`, the argument is *not-null*. Inside the `if` block, the compiler treats `value` as *not-null* with no null-forgiving operator required. As of .NET 5, all .NET runtime APIs are annotated, so the analysis benefits any code that calls them.

## Generics

Generic type parameters can be reference types or value types, so `T?` follows different rules depending on the type argument:

- If `T` is a non-nullable reference type, `T?` is the corresponding nullable reference type.
- If `T` is a value type, `T?` is the same value type. (Add the `struct` constraint to get <xref:System.Nullable%601> behavior.)
- If `T` is already nullable, `T?` is the same type.

Constraints refine the rules:

- `class` requires a non-nullable reference type.
- `class?` allows either a nullable or a non-nullable reference type.
- `notnull` requires a non-nullable reference or value type.

The constraints help the compiler reason about how a generic type parameter is used:

:::code language="csharp" source="snippets/nullable-reference-types/Program.cs" id="Generics":::

## Known pitfalls

Two patterns can leave a non-nullable reference holding `null` without a warning. Both are limitations of the static analysis, not bugs in your code.

**Default structs.** A struct with non-nullable reference fields can be created with `default` or `new()`, leaving its fields uninitialized:

:::code language="csharp" source="snippets/nullable-reference-types/Program.cs" id="DefaultStructPitfall":::

The fields hold `null` at run time, but the compiler doesn't warn. If you must use a struct, prefer required members or a parameterized constructor that callers must invoke.

**Arrays of references.** A new array of a non-nullable reference type contains all `null` elements until you assign each one:

:::code language="csharp" source="snippets/nullable-reference-types/Program.cs" id="ArrayPitfall":::

Initialize array elements as soon as you create the array. Collection expressions and target-typed `new` make full initialization concise.

## Related content

- [Tutorial: Express your design intent with nullable and non-nullable reference types](../tutorials/nullable-reference-types.md)
- [Resolve nullable warnings](resolve-warnings.md)
- [Nullable migration strategies](migration-strategies.md)
- [Nullable static analysis attributes](../../language-reference/attributes/nullable-analysis.md)
- [Resolve nullable warnings (compiler reference)](../../language-reference/compiler-messages/nullable-warnings.md)
Loading
Loading