C#在启用可空引用(nullable)后获取可空状态的方法(NullabilityInfoContext)
这个问题很早就有个疑问,比如我们有个类:
public class Emp
{
public Emp()
{
Name = "Emp";
}
public int Id { get; set; }
public int? Age { get; set; }
public string Name { get; set; }
public string? NickName { get; set; }
}
当我们使用EFCore将实体映射到数据库后,我们发现Name列和Id在数据库中是非空的,Age列和NickName列是可空的。
对于Id列和Age列,因为一个是int类型,一个是可空int类型,自然可以区分,但是对于Name列和NickName列,他们类型虽然是string和string?,但是按照官方的说法,如果启用了可空引用,那么在运行时环境,string和string?是一样的,它只在编译时生效,会检查空指针引用,在可能出现空指针引用的地方编译时给出warnning,那么在运行时,EFCore又怎么知道Name是非空,而NickName是可空的呢?
比如下面的代码:
public static void Main()
{
var builder = new DbContextOptionsBuilder<MyDbContext>();
builder.UseSqlServer("Server=localhost,1433;User Id=sa;Password=123456;Database=test;Encrypt=True;TrustServerCertificate=True;");
var dbContext = new MyDbContext(builder.Options);
var entityType = dbContext.Model.FindEntityType(typeof(Emp));
var properties = entityType?.GetProperties() ?? Enumerable.Empty<IProperty>();
foreach (var property in properties)
{
Console.WriteLine($"Property Name: {property.Name}, IsNullable: {property.IsNullable}");
}
}
public class MyDbContext : DbContext
{
public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Emp>().HasKey(f => f.Id);
}
}
运行结果:

要寻找答案,需要从源码角度入手(源码:https://github.com/dotnet/efcore)
首先IsNullable在IReadOnlyProperty(源码)类中,它的实现类有两个:Property(源码)和RuntimeProperty(源码),而RuntimeProperty的取值来自IProperty,IProperty的实现类又是Property和RuntimeProperty,所以我们只需要关注Property实现类的IsNullable属性就行了,按它的引用去寻找,最终在NonNullableReferencePropertyConvention(源码)找到了源码:
public class NonNullableReferencePropertyConvention : NonNullableConventionBase,
IPropertyAddedConvention,
IPropertyFieldChangedConvention,
IPropertyElementTypeChangedConvention,
IComplexPropertyAddedConvention,
IComplexPropertyFieldChangedConvention
{
/// <summary>
/// Creates a new instance of <see cref="NonNullableReferencePropertyConvention" />.
/// </summary>
/// <param name="dependencies">Parameter object containing dependencies for this convention.</param>
public NonNullableReferencePropertyConvention(ProviderConventionSetBuilderDependencies dependencies)
: base(dependencies)
{
}
private void Process(IConventionPropertyBuilder propertyBuilder)
{
if (propertyBuilder.Metadata.GetIdentifyingMemberInfo() is { } memberInfo
&& TryGetNullabilityInfo(propertyBuilder.ModelBuilder, memberInfo, out var nullabilityInfo))
{
if (nullabilityInfo.ReadState == NullabilityState.NotNull)
{
propertyBuilder.IsRequired(true);
}
// If there's an element type, this is a primitive collection; check and apply the element's nullability as well.
if (propertyBuilder.Metadata.GetElementType() is { } elementType
&& nullabilityInfo is
{ ElementType.ReadState: NullabilityState.NotNull } or
{ GenericTypeArguments: [{ ReadState: NullabilityState.NotNull }] })
{
elementType.SetIsNullable(false);
}
}
}
// others....
}
这里TryGetNullabilityInfo在父类中:
protected virtual bool TryGetNullabilityInfo(
IConventionModelBuilder modelBuilder,
MemberInfo memberInfo,
[NotNullWhen(true)] out NullabilityInfo? nullabilityInfo)
{
if (memberInfo.GetMemberType().IsValueType)
{
nullabilityInfo = null;
return false;
}
var annotation =
modelBuilder.Metadata.FindAnnotation(CoreAnnotationNames.NonNullableConventionState)
?? modelBuilder.Metadata.AddAnnotation(CoreAnnotationNames.NonNullableConventionState, new NullabilityInfoContext());
var nullabilityInfoContext = (NullabilityInfoContext)annotation.Value!;
nullabilityInfo = memberInfo switch
{
PropertyInfo propertyInfo => nullabilityInfoContext.Create(propertyInfo),
FieldInfo fieldInfo => nullabilityInfoContext.Create(fieldInfo),
_ => null
};
return nullabilityInfo is not null;
}
这里答案就明了了,它用的是:NullabilityInfoContext
NullabilityInfoContext
NullabilityInfoContext 是 .NET 6 引入的一个位于System.Reflection 命名空间下的类,主要用于在运行时通过反射获取 C# 可空引用类型(Nullable Reference Types, NRT) 的元数据信息。
在 C# 8.0 及更高版本中,开发者可以使用 string? 和 string 来区分可空和不可空引用类型。然而,这种区分主要是一个编译时的静态分析特性。在传统的 .NET 反射 API 中,string? 和 string 都被视为相同的 System.String 类型,反射无法直接区分它们。NullabilityInfoContext 填补了这一空白,允许框架和库作者在运行时读取编译器生成的可空性注解。
这个在某些下场景至关重要例如:
1、ORM 框架(如 EF Core):判断实体属性是否应在数据库中映射为 NOT NULL 列
2、序列化库(如 System.Text.Json, Newtonsoft.Json):决定在反序列化时是否允许字段为 null,或在序列化时如何处理 null 值。
3、API 文档生成工具(如:Swagger):准确显示参数或返回值的可空性,以便生成更准确的文档。
4、依赖注入容器:在解析服务时进行更严格的空值检查。
NullabilityInfoContext 提供了 Create 方法的重载,用于从不同的反射成员中提取可空性信息:
Create(PropertyInfo property): 获取属性的可空性信息。
Create(FieldInfo field): 获取字段的可空性信息。
Create(ParameterInfo parameter): 获取方法参数的可空性信息。
Create(EventInfo eventInfo): 获取事件的可空性信息。
这些方法返回一个 NullabilityInfo 对象,该对象包含以下关键状态:
ReadState: 读取该成员时的可空状态(例如,属性的 getter 返回类型是否可空)。
WriteState: 写入该成员时的可空状态(例如,属性的 setter 参数是否可空)。
Element: 如果成员是数组或泛型集合(如 List<string?>),此属性提供元素类型的可空性信息。
可空状态枚举值包括:
NotNull: 明确标记为非可空。
Nullable: 明确标记为可空。
Unknown: 无法确定可空性(通常发生在没有启用可空上下文或缺少元数据时)。
例如,一个简单的使用例子如下:
public static void Main()
{
var nullabilityInfoContext = new NullabilityInfoContext();
var pis = typeof(Emp).GetProperties();
foreach (var pi in pis)
{
var nullabilityInfo = nullabilityInfoContext.Create(pi);
Console.WriteLine($"{pi.Name}: ReadState = {nullabilityInfo?.ReadState},WriteState = {nullabilityInfo?.WriteState}");
}
}

总结
这里记录一下之前的疑惑,其实,像NullabilityInfoContext一样在运行时读取编译器生成时信息的类,还有MetadataLoadContext、StackTrace和StackFrame等。
或者换句话说,这种特性其实我们一直在用,只是我们没有注意罢了,比如Microsoft.CodeAnalysis.Compilation(编译分析)、System.Linq.Expressions (表达式树)、System.Reflection.Emit(IL中间指令)等都会涉及这种获取编译时元数据信息,这些信息通常是从自定义特性(Custom Attributes)、元数据表(Metadata Tables)或PDB符号文件中读取的,例如:


浙公网安备 33010602011771号