Nullable reference types and nullability in EF Core

August 12th 2022 EF Core C#

Nullable reference types have a greater impact on projects using Entity Framework Core than other projects. Although this is well documented, I have found that many developers are not aware of this.

The following paragraph from the official documentation is a good summary of what can happen when you enable nullable reference types in an existing project:

Exercise caution when enabling nullable reference types on an existing project: reference type properties which were previously configured as optional will now be configured as required, unless they are explicitly annotated to be nullable. When managing a relational database schema, this may cause migrations to be generated which alter the database column's nullability.

Let us look at what happens to a simple entity with some optional properties and some required properties. This is how you would define it when nullable reference types are disabled:

public class Person
{
  public int PersonId { get; set; }

  [Required]
  public string FirstName { get; set; }

  public string MiddleName { get; set; }

  [Required]
  public string LastName { get; set; }
}

With reference types (string in our example), database columns are nullable by default. To make a column non-nullable, you must annotate its property with the Required attribute. The above entity results in the following migration code:

migrationBuilder.CreateTable(
  name: "Persons",
  columns: table => new
  {
    PersonId = table.Column<int>(type: "int", nullable: false)
      .Annotation("SqlServer:Identity", "1, 1"),
    FirstName = table.Column<string>(type: "nvarchar(max)", nullable: false),
    MiddleName = table.Column<string>(type: "nvarchar(max)", nullable: true),
    LastName = table.Column<string>(type: "nvarchar(max)", nullable: false)
  },
  constraints: table =>
  {
    table.PrimaryKey("PK_Persons", x => x.PersonId);
  });

As you can see, the FirstName and LastName columns are not nullable because the corresponding properties in the entity class are annotated with the Required attribute. The MiddleName column is nullable because there is no attribute on the corresponding property.

After you enable the nullable reference types for the project by adding <Nullable>enable</Nullable> to the project file, the behavior changes. You can verify this by creating a new migration without making any code changes:

Add-Migration NullableReferenceTypes

Since you did not make any intentional changes to the code, you would want the created migration to be empty. But it is not. The migration contains the following code:

migrationBuilder.AlterColumn<string>(
  name: "MiddleName",
  table: "Persons",
  type: "nvarchar(max)",
  nullable: false,
  defaultValue: "",
  oldClrType: typeof(string),
  oldType: "nvarchar(max)",
  oldNullable: true);

The MiddleName column is no longer nullable. Fortunately, the Add-Migration command warns you about this change so you do not miss it:

An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.

How can you fix it? The nullability of a column is now determined based on the nullability of the corresponding property in the entity class. The behavior is described in detail in the documentation. So if you want to keep the column nullable, you need to make the corresponding property type nullable as well:

public string? MiddleName { get; set; }

If you run the Add-Migration command again after this change, the generated migration will be empty.

Since the Required attribute no longer has any effect, you can safely delete it from the entity class:

public class Person
{
  public int PersonId { get; set; }
  public string FirstName { get; set; }
  public string? MiddleName { get; set; }
  public string LastName { get; set; }
}

There will still be other warnings in the code because you enabled nullable reference types.

For each non-nullable property, the following warning is displayed:

CS8618: Non-nullable property 'FirstName' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

The recommended fix is to create a constructor that initializes such properties:

public Person(string firstName, string lastName)
{
  FirstName = firstName;
  LastName = lastName;
}

The same warning is generated for each DbSet<T> property in the DbContext class, e.g.:

public DbSet<Person> Persons { get; set; }

The recommended fix is to make this property a read-only property that returns Set<T>():

public DbSet<Person> Persons => Set<Person>();

For more information about these fixes, see the official documentation.

A sample project that illustrates the described behavior can be found in my GitHub repository.

After enabling nullable reference types for an existing project with Entity Framework core entities, you should always try to create a new migration to make sure you have not accidentally caused database changes. If the migration you create is not empty, review the changes and correct the code by following the steps in this post. When you think you are done, remove the previously created migration and create a new one. If it's not empty, you'll need to fix more code. And while you are at it, you should also fix any warnings related to nullable reference types, as described above.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License