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列,他们类型虽然是stringstring?,但是按照官方的说法,如果启用了可空引用,那么在运行时环境,stringstring?是一样的,它只在编译时生效,会检查空指针引用,在可能出现空指针引用的地方编译时给出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);
        }
    }

  运行结果:

  image

  要寻找答案,需要从源码角度入手(源码:https://github.com/dotnet/efcore

  首先IsNullableIReadOnlyProperty源码)类中,它的实现类有两个:Property源码)和RuntimeProperty源码),而RuntimeProperty的取值来自IPropertyIProperty的实现类又是PropertyRuntimeProperty,所以我们只需要关注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}");
        }
    }

  image

  

  总结

  这里记录一下之前的疑惑,其实,像NullabilityInfoContext一样在运行时读取编译器生成时信息的类,还有MetadataLoadContext、StackTrace和StackFrame等。

  或者换句话说,这种特性其实我们一直在用,只是我们没有注意罢了,比如Microsoft.CodeAnalysis.Compilation(编译分析)、System.Linq.Expressions (表达式树)、System.Reflection.Emit(IL中间指令)等都会涉及这种获取编译时元数据信息,这些信息通常是从自定义特性(Custom Attributes)‌、‌元数据表(Metadata Tables)‌或‌PDB符号文件‌中读取的,例如:

  image

 

posted @ 2026-06-20 18:09  没有星星的夏季  阅读(9)  评论(0)    收藏  举报