Update a codebase with nullable reference types to improve null diagnostic warnings
Nullable reference types enable you to declare if variables of a reference type should or shouldn't be assigned a
null value. The compiler's static analysis and warnings when your code might dereference
null are the most important benefit of this feature. Once enabled, the compiler generates warnings that help you avoid throwing a System.NullReferenceException when your code runs.
If your codebase is relatively small, you can turn on the feature in your project, address warnings, and enjoy the benefits of the improved diagnostics. Larger codebases may require a more structured approach to address warnings over time, enabling the feature for some as you address warnings in different types or files. This article describes different strategies to update a codebase and the tradeoffs associated with these strategies. Before starting your migration, read the conceptual overview of nullable reference types. It covers the compiler's static analysis, null-state values of maybe-null and not-null and the nullable annotations. Once you're familiar with those concepts and terms, you're ready to migrate your code.
Plan your migration
Regardless of how you update your codebase, the goal is that nullable warnings and nullable annotations are enabled in your project. Once you reach that goal, you'll have the
<nullable>Enable</nullable> setting in your project. You won't need any of the preprocessor directives to adjust settings elsewhere.
The first choice is setting the default for the project. Your choices are:
- Nullable disable as the default: disable is the default if you don't add a
Nullableelement to your project file. Use this default when you're not actively adding new files to the codebase. The main activity is to update the library to use nullable reference types. Using this default means you add a nullable preprocessor directive to each file as you update its code.
- Nullable enable as the default: Set this default when you're actively developing new features. You want all new code to benefit from nullable reference types and nullable static analysis. Using this default means you must add a
#nullable disableto the top of each file. You'll remove these preprocessor directives as you address the warnings in each file.
- Nullable warnings as the default: Choose this default for a two-phase migration. In the first phase, address warnings. In the second phase, turn on annotations for declaring a variable's expected null-state. Using this default means you must add a
#nullable disableto the top of each file.
- Nullable annotations as the default. Annotate code before addressing warnings.
Enabling nullable as the default creates more up-front work to add the preprocessor directives to every file. The advantage is that every new code file added to the project will be nullable enabled. Any new work will be nullable aware; only existing code must be updated. Disabling nullable as the default works better if the library is stable, and the main focus of the development is to adopt nullable reference types. You turn on nullable reference types as you annotate APIs. When you've finished, you enable nullable reference types for the entire project. When you create a new file, you must add the preprocessor directives and make it nullable aware. If any developers on your team forget, that new code is now in the backlog of work to make all code nullable aware.
Which of these strategies you pick depends on how much active development is taking place in your project. The more mature and stable your project, the better the second strategy. The more features being developed, the better the first strategy.
The global nullable context does not apply for generated code files. Under either strategy, the nullable context is disabled for any source file marked as generated. This means any APIs in generated files are not annotated. There are four ways a file is marked as generated:
- In the .editorconfig, specify
generated_code = truein a section that applies to that file.
<auto-generated/>in a comment at the top of the file. It can be on any line in that comment, but the comment block must be the first element in the file.
- Start the file name with TemporaryGeneratedFile_
- End the file name with .designer.cs, .generated.cs, .g.cs, or .g.i.cs.
Generators can opt-in using the
#nullable preprocessor directive.
Understand contexts and warnings
Enabling warnings and annotations control how the compiler views reference types and nullability. Every type has one of three nullabilities:
- oblivious: All reference types are nullable oblivious when the annotation context is disabled.
- nonnullable: An unannotated reference type,
Cis nonnullable when the annotation context is enabled.
- nullable: An annotated reference type,
C?, is nullable, but a warning may be issued when the annotation context is disabled. Variables declared with
varare nullable when the annotation context is enabled.
The compiler generates warnings based on that nullability:
- nonnullable types cause warnings if a potential
nullvalue is assigned to them.
- nullable types cause warnings if they dereferenced when maybe-null.
- oblivious types cause warnings if they're dereferenced when maybe-null and the warning context is enabled.
Each variable has a default nullable state that depends on its nullability:
- Nullable variables have a default null-state of maybe-null.
- Non-nullable variables have a default null-state of not-null.
- Nullable oblivious variables have a default null-state of not-null.
Before you enable nullable reference types, all declarations in your codebase are nullable oblivious. That's important because it means all reference types have a default null-state of not-null.
If your project uses Entity Framework Core, you should read their guidance on Working with nullable reference types.
When you start your migration, you should start by enabling warnings only. All declarations remain nullable oblivious, but you'll see warnings when you dereference a value after its null-state changes to maybe-null. As you address these warnings, you'll be checking against null in more locations, and your codebase becomes more resilient. To learn specific techniques for different situations, see the article on Techniques to resolve nullable warnings.
You can address warnings and enable annotations in each file or class before continuing with other code. However, it's often more efficient to address the warnings generated while the context is warnings before enabling the type annotations. That way, all types are oblivious until you've addressed the first set of warnings.
Enable type annotations
After addressing the first set of warnings, you can enable the annotation context. This changes reference types from oblivious to nonnullable. All variables declared with
var are nullable. This change often introduces new warnings. The first step in addressing the compiler warnings is to use
? annotations on parameter and return types to indicate when arguments or return values may be
null. As you do this task, your goal isn't just to fix warnings. The more important goal is to make the compiler understand your intent for potential null values.
Attributes extend type annotations
Several attributes have been added to express additional information about the null state of variables. The rules for your APIs are likely more complicated than not-null or maybe-null for all parameters and return values. Many of your APIs have more complex rules for when variables can or can't be
null. In these cases, you'll use attributes to express those rules. The attributes that describe the semantics of your API are found in the article on Attributes that affect nullable analysis.
Once you've addressed all warnings after enabling annotations, you can set the default context for your project to enabled. If you added any pragmas in your code for the nullable annotation or warning context, you can remove them. Over time, you may see new warnings. You may write code that introduces warnings. A library dependency may be updated for nullable reference types. Those updates will change the types in that library from nullable oblivious to either nonnullable or nullable.
You can also explore these concepts in our Learn module on Nullable safety in C#.