Skip to content

Commit

Permalink
Document C# 8 nullable reference types
Browse files Browse the repository at this point in the history
Closes #1654
Closes #1655
  • Loading branch information
roji committed Sep 23, 2019
1 parent df80085 commit e00d498
Show file tree
Hide file tree
Showing 14 changed files with 300 additions and 16 deletions.
11 changes: 5 additions & 6 deletions entity-framework/core/managing-schemas/scaffolding.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,12 @@ Next, it uses the schema information to create an EF Core model. Tables are used

Finally, the model is used to generate code. The corresponding entity type classes, Fluent API, and data annotations are scaffolded in order to re-create the same model from your app.

## What doesn't work
## Limitations

Not everything about a model can be represented using a database schema. For example, information about [**inheritance hierarchies**](../modeling/inheritance.md), [**owned types**](../modeling/owned-entities.md), and [**table splitting**](../modeling/table-splitting.md) are not present in the database schema. Because of this, these constructs will never be reverse engineered.

In addition, **some column types** may not be supported by the EF Core provider. These columns won't be included in the model.

You can define [**concurrency tokens**](../modeling/concurrency.md), in an EF Core model to prevent two users from updating the same entity at the same time. Some databases have a special type to represent this type of column (for example, rowversion in SQL Server) in which case we can reverse engineer this information; however, other concurrency tokens will not be reverse engineered.
* Not everything about a model can be represented using a database schema. For example, information about [**inheritance hierarchies**](../modeling/inheritance.md), [**owned types**](../modeling/owned-entities.md), and [**table splitting**](../modeling/table-splitting.md) are not present in the database schema. Because of this, these constructs will never be reverse engineered.
* In addition, **some column types** may not be supported by the EF Core provider. These columns won't be included in the model.
* You can define [**concurrency tokens**](../modeling/concurrency.md), in an EF Core model to prevent two users from updating the same entity at the same time. Some databases have a special type to represent this type of column (for example, rowversion in SQL Server) in which case we can reverse engineer this information; however, other concurrency tokens will not be reverse engineered.
* [The C# 8 nullable reference type feature](/dotnet/csharp/tutorials/nullable-reference-types) is currently unsupported in reverse engineering: EF Core always generates C# code that assumes the feature is disabled. For example, nullable text columns will be scaffolded as a property with type `string` , not `string?`, with either the Fluent API or Data Annotations used to configure whether a property is required or not. You can edit the scaffolded code and replace these with C# nullability annotations. Scaffolding support for nullable reference types is tracked by issue [#15520](https://github.com/aspnet/EntityFrameworkCore/issues/15520).

## Customizing the model

Expand Down
62 changes: 62 additions & 0 deletions entity-framework/core/miscellaneous/nullable-reference-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: Working with nullable reference types - EF Core
author: roji
ms.date: 9/9/2019
ms.assetid: bde4e0ee-fba3-4813-a849-27049323d301
uid: core/miscellaneous/nullable-reference-types
---
# Working with Nullable Reference Types

C# 8 introduced a new feature called [nullable reference types](/dotnet/csharp/tutorials/nullable-reference-types), allowing reference types to be annotated, indicating whether it is valid for them to contain null or not. If you are new to this feature, it is recommended that make yourself familiar with it by reading the C# docs.

This page introduces EF Core's support fo nullable reference types, and describes best practices for working with them.

## Required and optional properties

The main documentation on required and optional properties and their interaction with nullable reference types is the [Required and Optional Properties](xref:core/modeling/required-optional) page. It is recommended you start out by reading that page first.

> [!NOTE]
> 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.
## DbContext and DbSet

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 defining a non-nullable `DbSet` on a context will now generate a warning. However, EF Core always initializes all `DbSet` properties on DbContext-derived types, so they are guaranteed to never be null, even if the compiler is unaware of this. Therefore, it is recommended to keep your `DbSet` properties non-nullable - allowing you to access them without null checks - and to silence the compiler warnings by explicitly setting them to null with the help of the null-forgiving operator (!):

[!code-csharp[Main](../../../samples/core/Miscellaneous/NullableReferenceTypes/NullableReferenceTypesContext.cs?name=Context&highlight=3-4)]

## Non-nullable properties and initialization

Compiler warnings for uninitialized non-nullable reference types are also a problem for regular properties on your entity types. In our example above, we avoided these warnings by using [constructor binding](xref:core/modeling/constructors), a feature which works perfectly with non-nullable properties, ensuring they are always initialized. However, in some scenarios constructor binding isn't an option: navigation properties, for example, cannot be initialized in this way.

Required navigation properties present an additional difficulty: although a dependent will always exist for a given principal, it may or may not be loaded by a particular query, depending on the needs at that point in the program ([see the different patterns for loading data](xref:core/querying/related-data)). At the same time, it is undesirable to make these properties nullable, since that would force all access to them to check for null, even if they are required.

One way to deal with these scenarios, is to have a non-nullable property with a nullable [backing field](xref:core/modeling/backing-field):

[!code-csharp[Main](../../../samples/core/Miscellaneous/NullableReferenceTypes/Order.cs?range=12-17)]

Since the navigation property is non-nullable, a required navigation is configured; and as long as the navigation is properly loaded, the dependent will be accessible via the property. If, however, the property is accessed without first properly loading the related entity, an InvalidOperationException is thrown, since the API contract has been used incorrectly.

As a terser alternative, it is possible to simply initialize the property to null with the help of the null-forgiving operator (!):

[!code-csharp[Main](../../../samples/core/Miscellaneous/NullableReferenceTypes/Order.cs?range=19)]

An actual null value will never be observed except as a result of a programming bug, e.g. accessing the navigation property without properly loading the related entity beforehand.

> [!NOTE]
> Collection navigations, which contain references to multiple related entities, should always be non-nullable. An empty collection means that no related entities exist, but the list itself should never be null.
## Navigating and including nullable relationships

When dealing with optional relationships, it's possible to encounter compiler warnings where an actual null reference exception would be impossible. When translating and executing your LINQ queries, EF Core guarantees that if an optional related entity does not exist, any navigation to it will simply be ignored, rather than throwing. However, the compiler is unaware of this EF Core guarantee, and produces warnings as if the LINQ query were executed in memory, with LINQ to Objects. As a result, it is necessary to use the null-forgiving operator (!) to inform the compiler that an actual null value isn't possible:

[!code-csharp[Main](../../../samples/core/Miscellaneous/NullableReferenceTypes/Program.cs?range=46)]

A similar issue occurs when including multiple levels of relationships across optional navigations:

[!code-csharp[Main](../../../samples/core/Miscellaneous/NullableReferenceTypes/Program.cs?range=36-39&highlight=2)]

If you find yourself doing this a lot, and the entity types in question are predominantly (or exclusively) used in EF Core queries, consider making the navigation properties non-nullable, and to configure them as optional via the Fluent API or Data Annotations. This will remove all compiler warnings while keeping the relationship optional; however, if your entities are traversed outside of EF Core, you may observe null values although the properties are annotated as non-nullable.

## Scaffolding

[The C# 8 nullable reference type feature](/dotnet/csharp/tutorials/nullable-reference-types) is currently unsupported in reverse engineering: EF Core always generates C# code that assumes the feature is off. For example, nullable text columns will be scaffolded as a property with type `string` , not `string?`, with either the Fluent API or Data Annotations used to configure whether a property is required or not. You can edit the scaffolded code and replace these with C# nullability annotations. Scaffolding support for nullable reference types is tracked by issue [#15520](https://github.com/aspnet/EntityFrameworkCore/issues/15520).
42 changes: 33 additions & 9 deletions entity-framework/core/modeling/required-optional.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Required/optional properties - EF Core
title: Required and Optional Properties - EF Core
author: rowanmiller
ms.date: 10/27/2016
ms.assetid: ddaa0a54-9f43-4c34-aae3-f95c96c69842
Expand All @@ -9,22 +9,46 @@ uid: core/modeling/required-optional

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, required properties are created as non-nullable columns, and optional properties are created as nullable columns.

## Conventions

By convention, a property whose .NET type can contain null will be configured as optional (`string`, `int?`, `byte[]`, etc.). Properties whose CLR type cannot contain null will be configured as required (`int`, `decimal`, `bool`, etc.).
By convention, a property whose .NET type can contain null will be configured as optional, whereas properties whose .NET type cannot contain null will be configured as required. For example, all properties with .NET value types (`int`, `decimal`, `bool`, etc.) are configured as required, and all properties with nullable .NET value types (`int?`, `decimal?`, `bool?`, etc.) are configured as optional.

> [!NOTE]
> A property whose .NET type cannot contain null cannot be configured as optional. The property will always be considered required by Entity Framework.
C# 8 introduced a new feature called [nullable reference types](/dotnet/csharp/tutorials/nullable-reference-types), which allows reference types to be annotated, indicating whether it is valid for them to contain null or not. This feature is disabled by default, and if enabled, it modifies EF Core's behavior in the following way:

## Data Annotations
* If nullable reference types are disabled (the default), all properties with .NET reference types are configured as optional by convention (e.g. `string`).
* If nullable reference types are enabled, properties will be configured based on the C# nullability of their .NET type: `string?` will be configured as optional, whereas `string` will be configured as required.

You can use Data Annotations to indicate that a property is required.
The following example shows an entity type with required and optional properties, with the nullable reference feature disabled (the default) and enabled:

[!code-csharp[Main](../../../samples/core/Modeling/DataAnnotations/Required.cs?highlight=14)]
# [Without nullable reference types (default)](#tab/without-nrt)

[!code-csharp[Main](../../../samples/core/Miscellaneous/NullableReferenceTypes/CustomerWithoutNullableReferenceTypes.cs?name=Customer&highlight=4-8)]

# [With nullable reference types](#tab/with-nrt)

[!code-csharp[Main](../../../samples/core/Miscellaneous/NullableReferenceTypes/Customer.cs?name=Customer&highlight=4-6)]

***

Using nullable reference types is recommended since it flows the nullability expressed in C# code to EF Core's model and to the database, and obviates the use of the Fluent API or Data Annotations to express the same concept twice.

## Fluent API
> [!NOTE]
> 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.
For more information on nullable reference types and how to use them with EF Core, [see the dedicated documentation page for this feature](xref:core/miscellaneous/nullable-reference-types).

## Configuration

A property that would be optional by convention can be configured to be required as follows:

# [Data Annotations](#tab/data-annotations)

[!code-csharp[Main](../../../samples/core/Modeling/DataAnnotations/Required.cs?highlight=14)]

You can use the Fluent API to indicate that a property is required.
# [Fluent API](#tab/fluent-api)

[!code-csharp[Main](../../../samples/core/Modeling/FluentAPI/Required.cs?highlight=11-13)]

***
2 changes: 1 addition & 1 deletion entity-framework/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
href: core/modeling/keys.md
- name: Generated Values
href: core/modeling/generated-properties.md
- name: Required/optional properties
- name: Required and Optional Properties
href: core/modeling/required-optional.md
- name: Maximum Length
href: core/modeling/max-length.md
Expand Down
17 changes: 17 additions & 0 deletions samples/core/Miscellaneous/NullableReferenceTypes/Address.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace NullableReferenceTypes
{
#region OrderDetails
public class Address
{
public int Id { get; set; }
public string City { get; set; }
public string Street { get; set; }

public Address(string city, string street)
{
City = city;
Street = street;
}
}
#endregion
}
19 changes: 19 additions & 0 deletions samples/core/Miscellaneous/NullableReferenceTypes/Customer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace NullableReferenceTypes
{
#region Customer
public class Customer
{
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 Customer(string firstName, string lastName, string? middleName = null)
{
FirstName = firstName;
LastName = lastName;
MiddleName = middleName;
}
}
#endregion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;

#nullable disable

namespace NullableReferenceTypes
{
#region Customer
public class CustomerWithoutNullableReferenceTypes
{
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
}
#endregion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace NullableReferenceTypes
{
public class ExtraOptionalOrderInfo
{
public int Id { get; set; }
public string SomeExtraAdditionalInfo { get; set; }

public ExtraOptionalOrderInfo(string someExtraAdditionalInfo)
{
SomeExtraAdditionalInfo = someExtraAdditionalInfo;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.0.0-preview9.19423.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.0.0-preview9.19423.6" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.EntityFrameworkCore;

namespace NullableReferenceTypes
{
#region Context
public class NullableReferenceTypesContext : DbContext
{
public DbSet<Customer> Customers { get; set; } = null!;
public DbSet<Order> Orders { get; set; } = null!;

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFNullableReferenceTypes;Trusted_Connection=True;ConnectRetryCount=0");
}
#endregion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace NullableReferenceTypes
{
public class OptionalOrderInfo
{
public int Id { get; set; }
public string AdditionalInfo { get; set; }
public ExtraOptionalOrderInfo? ExtraAdditionalInfo { get; set; }

public OptionalOrderInfo(string additionalInfo)
{
AdditionalInfo = additionalInfo;
}
}
}
24 changes: 24 additions & 0 deletions samples/core/Miscellaneous/NullableReferenceTypes/Order.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;

namespace NullableReferenceTypes
{
#region Order
public class Order
{
public int Id { get; set; }

private Address? _shippingAddress;

public Address ShippingAddress
{
set => _shippingAddress = value;
get => _shippingAddress
?? throw new InvalidOperationException("Uninitialized property: " + nameof(ShippingAddress));
}

public Product Product { get; set; } = null!;

public OptionalOrderInfo? OptionalInfo { get; set; }
}
#endregion
}
13 changes: 13 additions & 0 deletions samples/core/Miscellaneous/NullableReferenceTypes/Product.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace NullableReferenceTypes
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }

public Product(string name)
{
Name = name;
}
}
}
50 changes: 50 additions & 0 deletions samples/core/Miscellaneous/NullableReferenceTypes/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;

namespace NullableReferenceTypes
{
public static class Program
{
static void Main(string[] args)
{
using (var context = new NullableReferenceTypesContext())
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();

context.Add(new Customer("John", "Doe"));

context.Add(new Order
{
ShippingAddress = new Address("London", "Downing"),
Product = new Product("Cooking stove"),
OptionalInfo = new OptionalOrderInfo("Some additional info")
{
ExtraAdditionalInfo = new ExtraOptionalOrderInfo("Some extra additional info")
}
});

context.SaveChanges();
}

using (var context = new NullableReferenceTypesContext())
{
var john = context.Customers.First(c => c.FirstName == "John");
Console.WriteLine("John's last name: " + john.LastName);

var order = context.Orders
.Include(o => o.OptionalInfo!)
.ThenInclude(op => op.ExtraAdditionalInfo)
.Single();

// The following would be a programming bug: we forgot to include ShippingAddress above. It would throw InvalidOperationException.
// Console.WriteLine(order.ShippingAddress.City);
// The following would be a programming bug: we forgot to include Product above; will throw NullReferenceException. It would throw NullReferenceException.
// Console.WriteLine(order.Product.Name);

Console.WriteLine(order.OptionalInfo!.ExtraAdditionalInfo!.SomeExtraAdditionalInfo);
}
}
}
}

0 comments on commit e00d498

Please sign in to comment.