使用可以为 Null 的引用类型Working with Nullable Reference Types

C#8引入了一种名为 null 的引用类型的新功能,允许对引用类型进行批注,以指示它是否可用于包含 null。 如果你不熟悉此功能,则建议你通过阅读C#文档来使自己熟悉该功能。

此页介绍 EF Core 对可为 null 的引用类型的支持,并介绍了使用它们的最佳做法。

必需属性和可选属性Required and optional properties

对于必需属性和可选属性及其与可为 null 的引用类型的交互,主要文档是必需的和可选的属性页。 建议首先阅读该页面。

备注

在现有项目上启用可以为 null 的引用类型时要格外小心:现在配置为可选的引用类型属性现在将配置为 “必需”,除非它们显式批注为可为 null。 管理关系数据库架构时,这可能会导致生成更改数据库列的为 null 性的迁移。

DbContext 和 DbSetDbContext and DbSet

如果启用了可以为 null 的引用C#类型,则编译器将为任何未初始化的不可为 null 的属性发出警告,因为这将包含 null。 因此,在上下文中定义不可为 null 的 DbSet 的常见做法现在会生成一个警告。 但是,EF Core 始终初始化 DbContext 派生类型上的所有 DbSet 属性,因此即使编译器不知道此操作,也可以保证它们永远不会为 null。 因此,建议保留不可以为 null 的 DbSet 属性,使你能够在不进行 null 检查的情况下访问这些属性,并通过使用 null 包容性运算符(!)的帮助将其显式设置为 null 来使编译器警告静音:

  1. public class NullableReferenceTypesContext : DbContext
  2. {
  3. public DbSet<Customer> Customers { get; set; } = null!;
  4. public DbSet<Order> Orders { get; set; } = null!;
  5. protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  6. => optionsBuilder
  7. .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFNullableReferenceTypes;Trusted_Connection=True;ConnectRetryCount=0");
  8. }

不可以为 null 的属性和初始化Non-nullable properties and initialization

对于实体类型上的常规属性,未初始化的不可以为 null 的引用类型的编译器警告也是一个问题。 在上面的示例中,我们使用构造函数绑定避免了这些警告,这是一项完美使用不可为 null 的属性的功能,确保它们始终初始化。 但是,在某些情况下,构造函数绑定不是一个选项:例如,不能以这种方式初始化导航属性。

所需的导航属性会带来额外的难度:尽管某个给定主体的依赖项始终存在,但特定查询可能会或不加载该依赖项,具体取决于程序中该点的需求(请参阅不同的加载数据模式)。 同时,不需要将这些属性设置为可以为 null,因为这会强制对它们的所有访问权限,以检查是否有 null,即使它们是必需的。

处理这些方案的一种方法是使用一个可以为 null 的支持字段的不可为 null 的属性:

  1. private Address? _shippingAddress;
  2. public Address ShippingAddress
  3. {
  4. set => _shippingAddress = value;
  5. get => _shippingAddress
  6. ?? throw new InvalidOperationException("Uninitialized property: " + nameof(ShippingAddress));
  7. }

由于导航属性不可为 null,因此配置了必需的导航;只要正确加载导航,就可以通过属性访问依赖项。 但是,如果在未事先正确加载相关实体的情况下访问属性,则会引发 InvalidOperationException,因为 API 协定的使用不正确。 请注意,必须将 EF 配置为始终访问支持字段而不是属性,因为它依赖于即使在未设置时也能读取值;请参阅有关如何执行此操作的支持字段的文档,并考虑指定 PropertyAccessMode.Field 以确保配置正确。

作为 terser 的替代方法,可以使用包容性运算符(!)的帮助简单地将属性初始化为 null:

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

实际的 null 值永远不会被视为除了编程错误之外的情况,例如,访问导航属性时,无需事先正确加载相关实体。

备注

包含对多个相关实体的引用的集合导航应始终不可为 null。 空集合意味着不存在相关实体,但列表本身不应为 null。

导航和包括可以为 null 的关系Navigating and including nullable relationships

当处理可选关系时,可能会遇到编译器警告,但不可能出现实际的 null 引用异常。 在转换和执行 LINQ 查询时,EF Core 确保在一个可选的相关实体不存在的情况下,将忽略对该实体的任何导航,而不是引发。 但是,编译器不知道这 EF Core 确保,并生成警告,就好像 LINQ 查询是在内存中执行的,而 LINQ to Objects。 因此,需要使用包容性运算符(!)来通知编译器无法实现实际的 null 值:

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

在可选导航中包含多个级别的关系时,会发生类似的问题:

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

如果你发现自己执行了很多操作,并且所涉及的实体类型是在 EF Core 查询中主要(或独占)使用的,请考虑使导航属性不可为 null,并将其配置为可通过熟知 API 或数据批注进行选择。 这将删除所有编译器警告,同时保持关系为可选;但是,如果你的实体是在 EF Core 之外遍历的,则你可能会观察到 null 值,尽管这些属性已批注为不可为 null。

限制Limitations

  • 反向工程目前不支持 C# 8 个可以为 null 的引用类型(NRTs): C# EF Core 始终会生成假定该功能已关闭的代码。 例如,可为 null 的文本列将被基架为具有类型 string 的属性,而不是 string?,其中使用的是用于配置是否需要属性的流畅 API 或数据批注。 您可以编辑基架代码并将其替换为C#可为 null 的批注。 #15520的问题跟踪了可为 null 的引用类型的基架支持。
  • EF Core 的公共 API 图面尚未批注为为空性(公共 API 为 “在意”),这使得在 NRT 功能打开时使用它有时会很难使用。 这特别包括 EF Core 公开的异步 LINQ 运算符,如FirstOrDefaultAsync。 我们计划为5.0 版本解决这一情况。