Two weeks ago .NET Core 3.0 was officially published. Together with the new framework version, Visual Studio 2019 got support for a long-awaited C# 8.0. The complete list of the new language features is available here on the MSDN, but the one that deserves special attention is Nullable reference types. This is a very important change in the language semantic, because from now we will be able to eliminate certain class of errors related to reference
nullability on the compilation stage. In this blog post, I will show you how to use this new language feature and how to achieve similar benefits if you still cannot use .NET Core 3.0 in your projects.
If our project is targeting .NET Core 3.0 or higher, we can start using non-nullable references. However, when we add
? annotation to the reference type we get the following warning:
CS8632: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
Our code will compile but all the rules related to the non-nullable references will be ignored (our code will be interpreted as it was before C# 8). Because this feature is a breaking change in the language, it should be explicitly enabled. To test it in a single file, add
#nullable enable directive. In order to enable it for the whole project, add
<Nullable>enable</Nullable> to your
csproj definition. If you are serious about using non-nullable references, I would recommend enabling it for the whole solution by adding
Directory.Build.props file in the root directory of your source code with the following content:
Since now all the references to the reference types are treated as
non-nullable unless they are explicitly marked as nullable with
? annotation. These new rules are verified by the Roslyn analyzers from
Microsoft.CodeAnalysis.CSharp package (which is added implicitly to all C# projects). There are over 40 rules related to the
nullability and the complete list is presented below:
|The result of the expression is always the same since a value of this type is never equal to ’null'
|Thrown value may be null.
|Converting null literal or possible null value to non-nullable type.
|Possible null reference assignment.
|Dereference of a possibly null reference.
|Possible null reference return.
|Possible null reference argument.
|Unboxing a possibly null value.
|Possible null reference assignment to iteration variable
|A possible null value may not be passed to a target marked with the [DisallowNull] attribute
|Nullability of reference types in type doesn’t match overridden member.
|Nullability of reference types in return type doesn’t match overridden member.
|Nullability of reference types in type of parameter doesn’t match overridden member.
|Nullability of reference types in type of parameter doesn’t match partial method declaration.
|Nullability of reference types in type doesn’t match implicitly implemented member.
|Nullability of reference types in return type doesn’t match implicitly implemented member.
|Nullability of reference types in type of parameter doesn’t match implicitly implemented member.
|Nullability of reference types in type doesn’t match implemented member.
|Nullability of reference types in return type doesn’t match implemented member.
|Nullability of reference types in type of parameter doesn’t match implemented member.
|Non-nullable field is uninitialized. Consider declaring as nullable.
|Nullability of reference types in value doesn’t match target type.
|Argument cannot be used for parameter due to differences in the nullability of reference types.
|Nullability of reference types in return type doesn’t match the target delegate.
|Nullability of reference types in type of parameter doesn’t match the target delegate.
|Argument cannot be used as an output for parameter due to differences in the nullability of reference types.
|Cannot convert null literal to non-nullable reference type.
|The ‘as’ operator may produce a null value for a type parameter.
|Nullable value type may be null.
|The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn’t match constraint type.
|The annotation for nullable reference types should only be used in code within a ‘#nullable’ annotations context.
|Nullability in constraints for type parameter doesn’t match the constraints for type parameter in implicitly implemented interface method’.
|The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn’t match ‘class’ constraint.
|Conditional access may produce a null value for a type parameter.
|Nullability of reference types in explicit interface specifier doesn’t match interface implemented by the type.
|Type does not implement interface member. Nullability of reference types in interface implemented by the base type doesn’t match.
|Interface is already listed in the interface list with different nullability of reference types.
|A default expression introduces a null value for a type parameter.
|A null literal introduces a null value for a type parameter.
|The switch expression does not handle some null inputs.
|Partial method declarations have inconsistent nullability in constraints for type parameter
|The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn’t match ’notnull’ constraint.
By default, all these rules are reported as
WARNING, which means even if they are violated the code will compile and work. My advice is to go through this list carefully and increase the severity to
ERROR level for most of them. In order to change the way these rules are treated, you have to at first add
*.ruleset file to your project/solution by clicking on the project’s (or solution’s) context menu
Add -> New Item and creating a file from
Code Analysis Rule Set template:
In the next step, you have to reference this ruleset file in your
Directory.Build.props file with the following entry:
Do not forget to add this file to your source control system in order to enforce all these rules across your teammates and CI.
Now you can start configuring severity level for individual rules. After double clicking on your ruleset file, Visual Studio should open it in
Ruleset editor. As I’ve mentioned before, entries related to
Nullable reference types should be available under
Microsoft.CodeAnalysis.CSharp branch. You can find them all by writing a
null keyword in the search field.
If you are using
JetBrains Rider you have to configure ruleset by manually editing xml inside
*.ruleset file - there is no graphical editor for this. The file’s format looks as follows:
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="New Rule Set" Description=" " ToolsVersion="16.0">
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp" RuleNamespace="Microsoft.CodeAnalysis.CSharp">
<Rule Id="CS8618" Action="Error" />
After increasing the severity level to
ERROR, all the violations of a given rule will be reported in a more noticeable manner and will block the compilation.
Nullable reference types are a very new language feature, I’ve been working with this concept for quite some time. To simulate similar behavior in the previous language versions, I’ve been using the following attributes from JetBrains.Annotations package:
In order to start using these attributes you need to perform the following steps:
- Install aforementioned JetBrains.Annotations nuget package
- Increase severity level for rules related to these attributes in Resharper’s configuration. Go to
Resharper -> Options -> Code Inspection -> Inspection Severityand review rules associated with nullability.
- Enable Solution Wide Analysis
Since now you can start annotating your code with
These inspections work only in Visual Studio and rule violations don’t affect the compilation process. However, you can enforce all the checks in your
CI pipelines using Resharper’s InspectCode Command-Line Tool. If the
design time check is not enough for you, you can bring these validations to runtime by using Fody together with NullGuard plugin. I tried it once and everything worked as expected but there were some issues with code coverage.
All the attributes from
JetBrains.Annotations package are conditional, which means they are not compiled into output assemblies unless you defined
JETBRAINS_ANNOTATIONS build constant. You can read more about usage of these attributes in my blog post Hunt your bugs in design time or in the official documentation
I’ve played a little with
Nullable reference types from C# 8.0 to compare it with
JetBrains.Annotations and I’ve spotted a few missing bits. Let’s take the following code with C# 8.0 annotations
- Line 7: Reported as ERROR - OK
- Line 12: This condition is always evaluated to
falseso should be somehow reported
- Line 17: This code is OK but the previous condition mislead the analyses and now is reported as ERROR.
UPDATE 2020-02-27: What we observe in
Line 17 it’s the compiler’s intentional behavior. Bill Wagner explained it in his presentation Change your habits: Modern techniques for modern C# (Similar example is discussed from 29:00)
The same code with
JetBrains.Annotations is reported according to expectations:
UPDATE 2019-10-08 (I): If you are developing libraries with .NET Core 3.0, there is still a possibility that someone can use it in the project without
Null reference types. It would be a good idea to add runtime null-checks in your library API methods. For contract enforcement, I would recommend using Synergy.Contracts package:
public void DoSomething(UserEntity user)
UPDATE 2019-10-08 (II): The return value of
ToString() method was marked as nullable. This is quite surprising because according to Notes to Inheritors at MSDN nobody should return null from this method:
“Your ToString() override should not return Empty or a null string.”
Here’s a PR #23510 that add this annotation to
ToString() method in coreclr repository. Right now there is a discrepancy between .NET Core and Resharper annotation for this method which can cause an even more confusion:
UPDATE 2019-10-17: I found an interesting article about the possibilities of usage of non-nullable references in projects targeting
netstandard2.0: C# 8.0 and .NET Standard 2.0 - Doing Unsupported Things
UPDATE 2019-11-24: Just discovered, that together with dotnet core 3.0 a couple of other things supporting nullability have been introduced, such as ! (null-forgiving) operator and extra System.Diagnostics.CodeAnalysis attributes.
UPDATE 2020-02-23: I’ve prepared a code snippet for
.editorconfig that sets severity level to error for different rules related to the reference nullability. Snippet available as gist. You can read more about configuring Roslyn analyzers rules via
.editorconfig at MSDN Set rule severity in an EditorConfig file