C# 8 introduced a new feature called nullable reference types, allowing reference types to be annotated, indicating whether it is valid for them to contain null or not.
A property is considered optional if it is valid for it to contain null. If null is not a valid value to be assigned to a property then it is considered to be a required property. When mapping to a relational database schema, the required properties are created as non-nullable columns, and optional properties are created as nullable columns.
int
, decimal
, bool
, etc.) are configured as required, and all properties with nullable .NET value types (int?
, decimal?
, bool?
, etc.) are configured as optional.This feature is disabled by default, and if enabled, it modifies EF Core's behavior in the following way:
string
.string?
will be configured as optional, but the string
will be configured as required.The following example shows an entity type with required and optional properties, with the nullable reference feature disabled.
public class AuthorWithoutNullableReferenceTypes
{
public int Id { get; set; }
[Required] // Data annotations needed to configure as required
public string FirstName { get; set; }
[Required]
public string LastName { get; set; } // Data annotations needed to configure as required
public string MiddleName { get; set; } // Optional by convention
}
The following example shows an entity type with required and optional properties, with the nullable reference feature enabled.
public class Author
{
public int Id { get; set; }
public string FirstName { get; set; } // Required by convention
public string LastName { get; set; } // Required by convention
public string? MiddleName { get; set; } // Optional by convention
public Author(string firstName, string lastName, string? middleName = null)
{
FirstName = firstName;
LastName = lastName;
MiddleName = middleName;
}
}
Using nullable reference types is recommended since it flows the nullability expressed in C# code to EF Core's model and the database, and removes the use of the Fluent API or Data Annotations to express the same concept twice.
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 common practice of having uninitialized DbSet
properties on a context type will now generate a warning. To fix this, make your DbSet
properties read-only and initialize them as follows:
public class NullableReferenceTypesContext : DbContext
{
public DbSet<Author> Authors => Set<Author>();
public DbSet<Book> Books => Set<Book>();
}
Compiler warnings for uninitialized non-nullable reference types are also a problem for regular properties on your entity types.
One way to deal with these scenarios is to have a non-nullable property with a nullable backing field.
private Address? _address;
public Address Address
{
set => _address = value;
get => _address ?? throw new InvalidOperationException("Uninitialized property: " + nameof(Address));
}
InvalidOperationException
.As a terser alternative, it is possible to simply initialize the property to null with the help of the null-forgiving operator (!
).
public Address Address { get; set; } = null!;
When dealing with optional relationships, it's possible to encounter compiler warnings where an actual null reference exception would be impossible.
!
) to inform the compiler that an actual null value isn't possible.Console.WriteLine(author.OptionalInfo!.ExtraAdditionalInfo!.SomeExtraAdditionalInfo);
A similar issue occurs when including multiple levels of relationships across optional navigations.
var author = context.Authors
.Include(o => o.OptionalInfo!)
.ThenInclude(op => op.ExtraAdditionalInfo)
.FirstOrDefault();