Skip to content

Instantly share code, notes, and snippets.

@habib-sadullaev
Last active September 10, 2023 09:14
Show Gist options
  • Save habib-sadullaev/1e4397c302aaaf83b5d5f4698bc94cd6 to your computer and use it in GitHub Desktop.
Save habib-sadullaev/1e4397c302aaaf83b5d5f4698bc94cd6 to your computer and use it in GitHub Desktop.
### Nullable Reference Types
https://devblogs.microsoft.com/dotnet/nullable-reference-types-in-csharp/
https://learn.microsoft.com/en-us/ef/core/miscellaneous/nullable-reference-types#non-nullable-properties-and-initialization
The purpose of nullable warnings is to minimize the chance that your application throws a `System.NullReferenceException` when run.
To enable Nullable Reference Types for all code in a project, you can add the following to its `.csproj` file:
```csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
...
<Nullable>enable</Nullable>
<WarningsAsErrors>Nullable</WarningsAsErrors>
...
</PropertyGroup>
</Project>
```
You'll address almost all warnings using one of four techniques:
- Adding necessary null checks.
- Adding `?` or `!` nullable annotations.
- Adding [Attributes for null-state static analysis](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/nullable-analysis).
- Initializing variables correctly.
***The null-forgiving operator `!` should be used with caution and only in situations where you’re certain that a nullable expression will never evaluate to `null` at runtime. It’s used to suppress nullable warnings when the compiler can’t determine that an expression is not `null`.
This operator doesn’t change the runtime behavior of your code. It only suppresses nullable warnings at compile-time. If an expression that’s been marked with `!` evaluates to `null` at runtime, a null reference exception will still be thrown.***
### Bad
[link](https://sharplab.io/#v2:C4LgTgrgdgNAJiA1AHwAICYCMBYAUHgWQAooBTAdyIEoqBuPPVTANgAJUAWV4gBVLADOAeyisADlTwBvPKzntMATiJiAdADEAloOAA5AIYBbUqoAypKAHNgACzqtZ8psrWn9AvUZPmrtug1x5Vk0AM1YVVQJNODgAG1IDY1YAQgBeVigIWNjJQPkZPKC5ZwiomPjE7wtrO1oHQtYAXzxm/FxUAGZ2dFY+QRFpR2KupgAGVi0dStYpVktSYDqBBbrG1nTM7OT6Bs6FUYB+bmi4hK8ZuZXWZcWmofYRzHG3D2nZ+dub1fWMrNjtlpAA===)
```C#
using System;
M(new()); // can't detect that `FistName` and `LastName` are not initialized
static void M(Person p)
{
Console.WriteLine(p.FirstName.Length);
Console.WriteLine(p.LastName.Length);
if (p.MiddleName != null)
{
Console.WriteLine(p.MiddleName.Length); // ok
}
}
public class Person
{
public string FirstName { get; set; } = null!; // NullReferenceException
public string? MiddleName { get; set; }
public string LastName { get; set; } = null!;
}
```
### Good
[link](https://sharplab.io/#v2:C4LgTgrgdgNAJiA1AHwAICYCMBYAUHgWQAooBTAdyIEoACAbxoDEBLMAZ2ADkBDAW1JoBeGgCJGAe3EiYNADLcOPfkNEAhbmBE0AvlQDcNPHlSYAbDVQAWGsQAKpduKg0ADlTx08NbxcwBOIhcAOhZ2Lj5SINlSKABzYAALfUNcH18A4PlFCKiY+KSDAHpCmk4IABtygCVSADMHGIBjUgBRAA9ml2BmJy8fPu9mWppAoIJmODhy0iUBAEJhKAry91SfTzW07xMMsYmpmZzouMTk4ppxAGsUtO08O/xcVABmC3Qae0coDwGLV5MAAxMVjZZQMWKkYAGNiQgzaFRLSpzPS/F6+AEAfhs+2ms3oNAhUJoMKJDzSaMBcgU4TBBNhxPp8MWy2R9yAA===)
```C#
using System;
M(new() { FirstName = "Foo", LastName = "Bar" }); // ok
static void M(Person p)
{
Console.WriteLine(p.FirstName.Length);
Console.WriteLine(p.LastName.Length);
if (p.MiddleName != null)
{
Console.WriteLine(p.MiddleName.Length); // ok
}
}
public class Person
{
public string FirstName { get; set; } = null!;
public string? MiddleName { get; set; }
public string LastName { get; set; } = null!;
}
```
- declare property as nonnullable and use `null!` to supress the warnings.
It's a preferable way to use
- in an existing project to minimize changes.
- in a new project when there is no other options described below. e.g. types are utilized by IOptions<>, Dapper, serializators, EF (sometimes) etc.
```csharp
public class Foo
{
public Bar Bar { get; set; } = null!;
}
```
In this example, we’ve used the null-forgiving operator `!` to suppress the nullable warning for the `Bar` property. This tells the compiler that we’re aware that the `Bar` property is being assigned a `null` value and that we’re taking responsibility for any null reference exceptions that may occur as a result.
Keep in mind that this should be done with _caution_ as it can hide _potential issues_ in your code. It’s generally _better_ to ensure that non-nullable properties are _always_ assigned a non-null value.
- declare a propery as nullable if it can be `null`
```csharp
public class Foo
{
public Bar? Bar { get; set; }
}
```
This will allow you to assign a null value to the `Bar` property without receiving a nullable warning. You’ll _need_ to check if the `Bar` property is `null` before accessing its members to _avoid_ null reference exceptions.
- add a constructor with a parameter of a property type:
```csharp
public class Foo
{
public Foo(Bar bar) => Bar = bar;
public Bar Bar { get; set; }
}
```
##### Bad
[link](https://sharplab.io/#v2:C4LgTgrgdgNAJiA1AHwAICYCMBYAUHgfQAIBeIqAUwHciAxAe3oAooIAbNgQgEoBuPPKgDMRDHUZ4A3niKzRIhswBCAQzBEARmphFVAL00q93GXOm45logEsAZkSZb11gM7l2bExas/gACzB6GkoaAEEwAHMIAFsKKGAAOQ8AUQAPAGMKAAdga3ooFhVY+ltHNW4+Ux87By0DV3cOLx9fAKDyaiJwqNj4pI40zJy8gqgiihKy40rvWQBfAVn5cWU1QzBuIhAif1cysB0nADp9TaqiSSIF3GvBETFVdSlz4V0jN4NLiIpgXiIXH5/OakRpcfg3RavB5GZ43IA)
```C#
using System;
_ = new Foo(null!);
public class Foo
{
public Foo(Bar bar, Baz baz)
{
if (bar is null)
throw new ArgumentNullException(nameof(bar));
if (baz is null)
throw new ArgumentNullException(nameof(baz));
}
public Foo(Bar bar) : this(bar, bar.Baz) // not ok. NullReferenceException on bar.Baz because bar is null
{ }
}
public class Bar
{
public Baz Baz { get; set; } = null!;
}
public class Baz
{
}
```
##### Good
[link](https://sharplab.io/#v2:C4LgTgrgdgNAJiA1AHwAICYCMBYAUHgfQAIBeIqAUwHciAxAe3oAooIAbNgQgEoBuPPKgDMRDHUZ4A3niKzRIhswBCAQzBEARmphFVAL00q93GXOm45logEsAZkSZb11gM7l2bExas/gACzB6GkoaAEEwAHMIAFsKKGAAOQ8AUQAPAGMKAAdga3ooFhVY+ltHNW4+Ux87By0DV3cOLx9fAKDyaiJwqNj4pI40zJy8gqgiihKy40rvWQBfAVn5cWU1QzBuIhAif1cysB0nAH4AOn0eIgB6S6J6AGsqokkiBdxXwRExVXUpR+FdIwAgzPCIUYC8IguMEQuakRpcfhvRb/L5GX5vIA=)
```C#
using System;
_ = new Foo(null!);
public class Foo
{
public Foo(Bar bar, Baz baz)
{
if (bar is null)
throw new ArgumentNullException(nameof(bar)); // ArgumentNullException because bar is null
if (baz is null)
throw new ArgumentNullException(nameof(baz));
}
public Foo(Bar bar) : this(bar, bar?.Baz!) // ok
{ }
}
public class Bar
{
public Baz Baz { get; set; } = null!;
}
public class Baz
{
}
```
##### Bad
[link](https://sharplab.io/#v2:CYLg1APgAgzABFATHACgUwE4GcD2A7AWACgBvYuChedbfACgEpzKyjL24BJPASwBdGAbmYUAviLgTYCAIwAGOADEe2PgDkAhgFs0cEnADmaPoLhZjp0ZLaVpUeQH44AWR7BgAGzSadewxbMA8RsKO3k4ABkNLHVtXX0jE0CkqwkpABYuXgEGa3ZWDkplVR9dAF44ACJFHBxK0wl2KJjSuArKgCENAC96iWDRIA==)
```C#
public class Person
{
public Person() // warning CS8618: Non-nullable property 'FirstName' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
{ // warning CS8618: Non-nullable property 'LasName' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
Init();
}
public string FirstName { get; set; }
public string? MiddleName { get; set; }
public string LastName { get; set; }
void Init()
{
FirstName = "Foo";
LastName = "Baz";
}
}
```
##### Good
[link](https://sharplab.io/#v2:CYLg1APgAgDABFAjAOgCIEsCGBzAdgewGcAXdAY0OQGF9gBTAQV0wBsBPQ9QgbgFgAoAVADMCAExwACnQBOhfLgEBvAXDUJR0uQoAUASlXqV/dabgBJXOmL6+J9QF9Da5xoSJ4AMXRziAOUwAWzo4JThsOmJuOEJI6Ic4VxF3GAB+OABZdGBgFjoA4NDwuJiSp3s1ZKR4ABlMEgKQsIio0taEgVcAbQy6QIAjWT98fwBXFhYdZmD8ADMdb19GvQAaOGm6OZ06hqC6PT0AXSSAFgsrGz1EitDXU0XdwoBeOAAiT3x8V+i79R3/PZwF6vABCmAAXt9XOUHEA==)
```C#
using System.Diagnostics.CodeAnalysis;
public class Person
{
public Person() // ok
{
Init();
}
public string FirstName { get; set; }
public string? MiddleName { get; set; }
public string LastName { get; set; }
[MemberNotNull(nameof(FirstName), nameof(LastName))]
void Init()
{
FirstName = "Foo";
LastName = "Baz";
}
}
```
##### Bad
[link](https://sharplab.io/#v2:C4LgTgrgdgNAJiA1AHwAICYCMBYAUHgWQAooBTAdyIEoACAbxoDEBLMAZ2ADkBDAW1JoBeGgCJGAe3EiYNAszhwANqR78hogELcAXtJoAZbh1UDhIrWBE0AvlQDcePKkwA2GqgAssogAVS7cSgaAAcqPDo8GiiaZgAzGiJggDo5BWUTGgBCYSgIRUUw3Gj6SOLo5IAlUjZSYABBfJZSRTg2agcisqjnAE5ElPklFT5SJP1SKABzYAALe1Ko6zwl/FxUAGZ3dBo/AKhwhfdN5wAGJlZjEfoaSdq7Ghrge+t1XPzMjuKN90wTgH5ZIN0lcGLcng87jZDt9TgYjFwQTdIY9nq88ooPodoccvFVHg1FE0Wm1aIIAHyAtLDNQ5dEdFZAA)
```C#
using System;
M(new() { FirstName = "Foo", MiddleName = "Baz", LastName = "Bar" });
static void M(Person p)
{
if (p.MiddleName != null)
{
p.ResetAllFields(); // can't detect the change!
Console.WriteLine(p.MiddleName.Length); // NullReferenceException
}
}
public class Person
{
public string FirstName { get; set; } = null!;
public string? MiddleName { get; set; }
public string LastName { get; set; } = null!;
public void ResetAllFields() => MiddleName = null;
```
##### Good
[link](https://sharplab.io/#v2:C4LgTgrgdgNAJiA1AHwAICYCMBYAUHgWQAooBTAdyIEoACAbxoDEBLMAZ2ADkBDAW1JoBeGgCJGAe3EiYNAszhwANqR78hogELcAXtJoAZbh1UDhIrWBE0AvlQDcePKkwA2GqgAssogAVS7cSgaAAcqPDo8GiiaZgAzGiJggDo5BWUTGgBCYSgIRUUw3Gj6SOLo4PUAcVJgAEEocWAAC38/AKhqOzLu6NLu5wBORJT5JRU+UiT9UigAc2b7Pps8a0dcZzc2tkCaarqG5tb/bY7aQQA+GjJyGi2d8JollnYuCfUxSWklw2M3swsRCsHPh1gBmdzoW7HQLhJaocHOAAMTFYvzUDFmNS6bCxNnUuXymWBxXh7kwiIA/LJRuk3hjcTjgF1VkVoqSkQYjK90TRMUyaIzmfi8ooiSs8EA=)
```C#
using System;
M(new() { FirstName = "Foo", MiddleName = "Baz", LastName = "Bar" });
static void M(Person p)
{
if (p.MiddleName != null)
{
p = GetAnotherPerson(); // detects the change!
Console.WriteLine(p.MiddleName.Length); // warning CS8602: Dereference of a possibly null reference.
}
}
static Person GetAnotherPerson() => new Person
{
FirstName = "Foo",
LastName = "Bar"
};
public class Person
{
public string FirstName { get; set; } = null!;
public string? MiddleName { get; set; }
public string LastName { get; set; } = null!;
public void ResetAllFields() => MiddleName = null;
```
@habib-sadullaev
Copy link
Author

habib-sadullaev commented Jun 8, 2023

Enable Nullable Reference Types for .NET6 C# projects

The purpose of nullable warnings is to minimize the chance that your application throws a System.NullReferenceException when run.

We think that the default meaning of unannotated reference types such as string should be non-nullable reference types, for a couple of reasons:

  1. We believe that it is more common to want a reference not to be null. Nullable reference types would be the rarer kind (though we don’t have good data to tell us by how much), so they are the ones that should require a new annotation.
  2. The language already has a notion of – and a syntax for – nullable value types. The analogy between the two would make the language addition conceptually easier, and linguistically simpler.
  3. It seems right that you shouldn’t burden yourself or your consumer with cumbersome null values unless you’ve actively decided that you want them. Nulls, not the absence of them, should be the thing that you explicitly have to opt in to.

Thus we get to the reason we call this language feature “nullable reference types”: Those are the ones that get added to the language. The nonnullable ones are already there, at least syntactically.

To enable Nullable Reference Types for all code in a project, you can add the following to its .csproj file:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    ...
    <Nullable>enable</Nullable>
    <WarningsAsErrors>Nullable</WarningsAsErrors>
    ...
  </PropertyGroup>
</Project>

You'll address almost all warnings using one of four techniques:

The null-forgiving operator ! should be used with caution and only in situations where you’re certain that a nullable expression will never evaluate to null at runtime. It’s used to suppress nullable warnings when the compiler can’t determine that an expression is not null.
This operator doesn’t change the runtime behavior of your code. It only suppresses nullable warnings at compile-time. If an expression that’s been marked with ! evaluates to null at runtime, a null reference exception will still be thrown.

Non-nullable properties and initialization

link

When nullable reference types are enabled, the C# compiler emits warnings for any uninitialized non-nullable property, as these would contain null. As a result, the following, common way of writing entity types cannot be used:

public class Customer
{
    public int Id { get; set; }

    // Generates CS8618, uninitialized non-nullable property:
    public string Name { get; set; }
}

If you're using C# 11 or above, required members provide the perfect solution to this problem:

public required string Name { get; set; }

The compiler now guarantees that when your code instantiates a Customer, it always initializes its Name property. And since the database column mapped to the property is non-nullable, any instances loaded by EF always contain a non-null Name as well.

If you're using an older version of C#, Constructor binding is an alternative technique to ensure that your non-nullable properties are initialized:

public class CustomerWithConstructorBinding
{
    public int Id { get; set; }
    public string Name { get; set; }

    public CustomerWithConstructorBinding(string name)
    {
        Name = name;
    }
}

Unfortunately, in some scenarios constructor binding isn't an option; navigation properties, for example, cannot be initialized in this way. In those cases, you can simply initialize the property to null with the help of the null-forgiving operator (but see below for more details):

public Product Product { get; set; } = null!;
The Best
public record Person(string FirstName, string MiddleName, string LastName)
{
    public Person Default = new("", "");
    public Person Empty = new("", "");

    public Person(string FirstName, string LastName) : this(FirstName, null!, LastName)
    {
    }
    
    public string? MiddleName { get; init; } = MiddleName;
}

Pros:

  • The compiler helps to initialize all required properties.
  • Properties are immutable by default.
  • with expression can help to create a copy of an instance with/without modification.
  • Simple to declare.
  • Mostly without duplication.
  • Sometimes Default/Empty can be used to reduce boilerplate code.

Cons:

  • Using this on an existing project can result in a lot of changes.
  • Sometimes there are duplicates but this is tolerable.
  • Sometimes Default/Empty needs to be implemented.

When to use:

  • New types declared and explicitly instatiated. They should be simple enough.
Just Great
public class Person
{
    public Person(string firstName, string lastName)
        => (FirstName, LastName) = (firstName, lastName);

    public Person(string firstName, string middleName, string lastName)
        : this(firstName, lastName) => MiddleName = middleName;

    public string FirstName { get; }
    public string? MiddleName { get; }
    public string LastName { get; }
}

Pros:

  • The compiler helps to initialize all required properties.
  • Properties are immutable by default.

Cons:

  • Using this on an existing project can result in a lot of changes.
  • Not possible to use with expression.
  • Not possible to use Default/Empty to reduce boilerplate code because not possible to use with expression.
  • A lot of constructors and duplication.

When to use:

  • New types declared and explicitly instatiated. They should be simple enough.
Good
public record Person
{
    public string FirstName { get; init; } = null!;
    public string? MiddleName { get; init; }
    public string LastName { get; init; } = null!;
}

Pros:

  • Recommended to use on an existing project to minimize changes.
  • Can be used for scenarios where it is difficult to use records/classes with constructors.
  • Properties are immutable by default.
  • with expression can help you create a copy of instance with/without modification.
  • Sometimes Default/Empty can be used to reduce boilerplate code.

Cons:

  • Easy to forget to initialize required properties.
  • Properties are optional by default.

When to use:

  • IOptions<>.
  • Dapper.
  • Serializations.
  • Mappings.
  • Existing code.
  • Complex types that have a lot of properties and nesting types.
Not Bad Yet
public class Person
{
    public string FirstName { get; init; } = null!;
    public string? MiddleName { get; init; }
    public string LastName { get; init; } = null!;
}

Pros:

  • Recommended to use on an existing project to minimize changes.
  • Can be used for scenarios where it is difficult to use records/classes with constructors.
  • Properties are immutable by default.

Cons:

  • Easy to forget to initialize a required property.
  • Not possible to use with expression.
  • Properties are optional by default.

When to use:

  • IOptions<>.
  • Dapper.
  • Serializations.
  • Mappings.
  • Existing code.
  • Complex types that have a lot of properties and nesting types.
Not So Bad
public class Person
{
    public string FirstName { get; set; } = null!;
    public string? MiddleName { get; set; }
    public string LastName { get; set; } = null!;
}

Pros:

  • Recommended to use on an existing project to minimize changes.
  • Can be used in scenarios where it is difficult to use records/classes with constructors.
  • Can be used where properties need to be mutable.

Cons:

  • Easy to forget to initialize a required property.
  • Properties are mutable by default.
  • Properties are optional by default.

When to use:

  • IOptions<>.
  • Dapper.
  • EF Core.
  • Serializations.
  • Mappings.
  • Circular references.
  • Existing code.
  • Instantiation and initialization should be done separately.
  • Complex types that have a lot of properties and nesting types.
  • Complex initialization logic.

Other Examples

Bad

link

using System;

_ = new Foo(null!);

public class Foo
{
    public Foo(Bar bar, Baz baz)
    {
        if (bar is null)
            throw new ArgumentNullException(nameof(bar));
        if (baz is null)
            throw new ArgumentNullException(nameof(baz));
    }

    public Foo(Bar bar) : this(bar, bar.Baz) // not ok. NullReferenceException on bar.Baz because bar is null
    { }
}

public class Bar 
{
    public Baz Baz { get; set; } = null!;
}

public class Baz
{
}
Good

link

using System;

_ = new Foo(null!);

public class Foo
{
    public Foo(Bar bar, Baz baz)
    {
        if (bar is null)
            throw new ArgumentNullException(nameof(bar)); // ArgumentNullException because bar is null
        if (baz is null)
            throw new ArgumentNullException(nameof(baz));
    }

    public Foo(Bar bar) : this(bar, bar?.Baz!) // ok 
    { }
}

public class Bar 
{
    public Baz Baz { get; set; } = null!;
}

public class Baz
{
}
Bad

link

public class Person
{
    public Person() // warning CS8618: Non-nullable property 'FirstName' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
    {               // warning CS8618: Non-nullable property 'LastName' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
        Init();
    }
    
    public string FirstName { get; set; } 
    public string? MiddleName { get; set; }
    public string LastName { get; set; } 
    
    void Init() 
    {
        FirstName = "Foo"; 
        LastName = "Baz";
    }
}
Good

link

using System.Diagnostics.CodeAnalysis;

public class Person
{
    public Person() // ok
    {
        Init();
    }
    
    public string FirstName { get; set; } 
    public string? MiddleName { get; set; }
    public string LastName { get; set; } 

    [MemberNotNull(nameof(FirstName), nameof(LastName))]
    void Init() 
    {
        FirstName = "Foo"; 
        LastName = "Baz";
    }
}
Bad

link

using System;

M(new() { FirstName = "Foo", MiddleName = "Baz", LastName = "Bar" });

static void M(Person p)
{
    if (p.MiddleName != null)
    {
        p.ResetAllFields();                     // can't detect the change!
        Console.WriteLine(p.MiddleName.Length); // NullReferenceException
    }
}

public class Person
{
    public string FirstName { get; set; } = null!;
    public string? MiddleName { get; set; }
    public string LastName { get; set; } = null!;
    
    public void ResetAllFields() => MiddleName = null;
Good

link

using System;

M(new() { FirstName = "Foo", MiddleName = "Baz", LastName = "Bar" });

static void M(Person p)
{
    if (p.MiddleName != null)
    {
        p = GetAnotherPerson();                 // detects the change!
        Console.WriteLine(p.MiddleName.Length); // warning CS8602: Dereference of a possibly null reference.
    }
}

static Person GetAnotherPerson() => new Person 
{ 
    FirstName = "Foo",
    LastName = "Bar"
};

public class Person
{
    public string FirstName { get; set; } = null!;
    public string? MiddleName { get; set; }
    public string LastName { get; set; } = null!;
    
    public void ResetAllFields() => MiddleName = null;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment