C# 8以降では型パラメータにnotnull制約が使用できる。 コンパイルの際、notnull制約が指定されている型パラメータと、その型パラメータを持つジェネリック型・ジェネリックメソッドには、nullabilityに関するメタデータ(属性)が付与される。

このとき付与されるメタデータには、NullableAttributeNullableContextAttributeの二種類がある。 このメタデータは、プロジェクトのNullableプロパティの値、あるいはコード中の#nullableディレクティブの指定によって以下のように付与される種類や値が変わる。 また、付与される対象が変わる場合もある。

ちなみに、NullableAttributeNullableContextAttributeはともにコンパイラが生成する属性で、フレームワークライブラリには含まれない。 そのためこの属性をコード中で使用することはできない。

型パラメータに付与されるメタデータ

notnull制約が指定されている型パラメータに付与されるメタデータと、nullableコンテキスト(プロジェクトのNullableプロパティの値、あるいは#nullableディレクティブの指定)によって付与されるメタデータの変化は次のようになる。

型パラメータT、およびその型パラメータを宣言している型C<T>またはジェネリックメソッドM<T>に付与されるメタデータ
型パラメータTの制約 nullableコンテキスト メタデータ
型パラメータT C<T>(DeclaringType)もしくは
メソッドM<T>(DeclaringMethod)
なし enable [NullableAttribute(2)] [NullableContextAttribute(1)]
disable (なし) (なし)
notnull enable (なし) [NullableContextAttribute(1)]
disable [NullableAttribute(1)] (なし)

ここで、NullableContextAttributeは型(DeclaringType)もしくはメソッド(DeclaringMethod)のどちらかに付与されるが、メソッドの場合でもコンパイル時の最適化によって(?)DeclaringTypeのみへの付与となる場合がある模様。

また、次のようにNullableContextAttributeは付与されず、型パラメータに対するNullableAttributeの付与のみとなる場合もある模様。

ジェネリック型GC<T>の型パラメータTに付与されるメタデータ
型パラメータTの制約 nullableコンテキスト 型パラメータTに付与されるメタデータ
なし enable [NullableAttribute(2)]
disable (なし)
notnull enable [NullableAttribute(1)]
disable [NullableAttribute(1)]

検証に使ったコード

型パラメータに付与されるメタデータを取得するコード 
using System;
using System.Linq;
using System.Reflection;

public class C0 {
#nullable enable
  public void MNEC<TNoConstraints, TNotNull>(TNoConstraints x, TNotNull y) where TNotNull : notnull {}
#nullable restore
}
public class C1 {
#nullable disable
  public void MNDC<TNoConstraints, TNotNull>(TNoConstraints x, TNotNull y) where TNotNull : notnull {}
#nullable restore
}

#nullable enable
public class CNEC {
  public void M<TNoConstraints, TNotNull>(TNoConstraints x, TNotNull y) where TNotNull : notnull {}
}
#nullable restore

#nullable disable
public class CNDC {
  public void M<TNoConstraints, TNotNull>(TNoConstraints x, TNotNull y) where TNotNull : notnull {}
}
#nullable restore

#nullable enable
public class GCNEC<TNoConstraints, TNotNull> where TNotNull : notnull {}
#nullable restore

#nullable disable
public class GCNDC<TNoConstraints, TNotNull> where TNotNull : notnull {}
#nullable restore

class Program {
  static void Main(string[] args)
  {
    PrintMetadata(typeof(C0).GetMethod(nameof(C0.MNEC)).GetGenericArguments());
    PrintMetadata(typeof(C1).GetMethod(nameof(C1.MNDC)).GetGenericArguments());
    PrintMetadata(typeof(CNEC).GetMethod(nameof(CNEC.M)).GetGenericArguments());
    PrintMetadata(typeof(CNDC).GetMethod(nameof(CNDC.M)).GetGenericArguments());

    PrintMetadata(typeof(GCNEC<,>).GetGenericArguments());
    PrintMetadata(typeof(GCNDC<,>).GetGenericArguments());
  }

  static void PrintMetadata(Type[] genericParameters)
  {
    foreach (var genericParameter in genericParameters) {
      PrintMetadata(genericParameter);
    }
  }

  static void PrintNullabilityAttribute(MemberInfo member)
  {
    foreach (var attrNullabilityMetadata in member.CustomAttributes.Where(
      attr => attr.AttributeType.Namespace == "System.Runtime.CompilerServices" && attr.AttributeType.Name.StartsWith("Nullable")
    )) {
      Console.Write(
        $"{attrNullabilityMetadata.AttributeType.Name}({string.Join(", ", attrNullabilityMetadata.ConstructorArguments)})"
      );
      Console.Write(", ");
    }
    Console.WriteLine();
  }

  static void PrintMetadata(Type genericParameter)
  {
    Console.Write($"[{genericParameter}:");
    if (genericParameter.DeclaringType is not null)
      Console.Write($" Type={genericParameter.DeclaringType}");
    if (genericParameter.DeclaringMethod is not null)
      Console.Write($" Method={genericParameter.DeclaringMethod}");
    Console.WriteLine("]");

    Console.Write($"{genericParameter}: ");
    PrintNullabilityAttribute(genericParameter);

    Console.Write($"{genericParameter.DeclaringType}: ");
    PrintNullabilityAttribute(genericParameter.DeclaringType);

    if (genericParameter.DeclaringMethod is not null) {
      Console.Write($"{genericParameter.DeclaringMethod}: ");
      PrintNullabilityAttribute(genericParameter.DeclaringMethod);
    }

    Console.WriteLine();
  }
}
実行結果
[TNoConstraints: Type=C0 Method=Void MNEC[TNoConstraints,TNotNull](TNoConstraints, TNotNull)]
TNoConstraints: NullableAttribute((Byte)2), 
C0: 
Void MNEC[TNoConstraints,TNotNull](TNoConstraints, TNotNull): NullableContextAttribute((Byte)1), 

[TNotNull: Type=C0 Method=Void MNEC[TNoConstraints,TNotNull](TNoConstraints, TNotNull)]
TNotNull: 
C0: 
Void MNEC[TNoConstraints,TNotNull](TNoConstraints, TNotNull): NullableContextAttribute((Byte)1), 

[TNoConstraints: Type=C1 Method=Void MNDC[TNoConstraints,TNotNull](TNoConstraints, TNotNull)]
TNoConstraints: 
C1: 
Void MNDC[TNoConstraints,TNotNull](TNoConstraints, TNotNull): 

[TNotNull: Type=C1 Method=Void MNDC[TNoConstraints,TNotNull](TNoConstraints, TNotNull)]
TNotNull: NullableAttribute((Byte)1), 
C1: 
Void MNDC[TNoConstraints,TNotNull](TNoConstraints, TNotNull): 

[TNoConstraints: Type=CNEC Method=Void M[TNoConstraints,TNotNull](TNoConstraints, TNotNull)]
TNoConstraints: NullableAttribute((Byte)2), 
CNEC: 
Void M[TNoConstraints,TNotNull](TNoConstraints, TNotNull): NullableContextAttribute((Byte)1), 

[TNotNull: Type=CNEC Method=Void M[TNoConstraints,TNotNull](TNoConstraints, TNotNull)]
TNotNull: 
CNEC: 
Void M[TNoConstraints,TNotNull](TNoConstraints, TNotNull): NullableContextAttribute((Byte)1), 

[TNoConstraints: Type=CNDC Method=Void M[TNoConstraints,TNotNull](TNoConstraints, TNotNull)]
TNoConstraints: 
CNDC: 
Void M[TNoConstraints,TNotNull](TNoConstraints, TNotNull): 

[TNotNull: Type=CNDC Method=Void M[TNoConstraints,TNotNull](TNoConstraints, TNotNull)]
TNotNull: NullableAttribute((Byte)1), 
CNDC: 
Void M[TNoConstraints,TNotNull](TNoConstraints, TNotNull): 

[TNoConstraints: Type=GCNEC`2[TNoConstraints,TNotNull]]
TNoConstraints: NullableAttribute((Byte)2), 
GCNEC`2[TNoConstraints,TNotNull]: 

[TNotNull: Type=GCNEC`2[TNoConstraints,TNotNull]]
TNotNull: NullableAttribute((Byte)1), 
GCNEC`2[TNoConstraints,TNotNull]: 

[TNoConstraints: Type=GCNDC`2[TNoConstraints,TNotNull]]
TNoConstraints: 
GCNDC`2[TNoConstraints,TNotNull]: 

[TNotNull: Type=GCNDC`2[TNoConstraints,TNotNull]]
TNotNull: NullableAttribute((Byte)1), 
GCNDC`2[TNoConstraints,TNotNull]: 

リフレクションによってnotnull制約かどうかを調べる

リフレクションによって型パラメータにnotnull制約が指定されているかどうかを調べるには、次のようにする。

まずは、型パラメータを表すTypeGenericParameterAttributesGenericParameterAttributes.SpecialConstraintMaskでマスクし、その値がGenericParameterAttributes.Noneであればnotnull制約が指定されている可能性がある。

その上で、次のように条件分岐する。

C#コードでは次のようになる。

型パラメータがnotnull制約を持つかどうか調べる 
static bool HasNotNullConstraint(Type genericParameter)
{
  static bool IsNullableAttribute(CustomAttributeData attr)
    => attr.AttributeType.FullName.Equals("System.Runtime.CompilerServices.NullableAttribute", StringComparison.Ordinal);

  static bool IsNullableContextAttribute(CustomAttributeData attr)
    => attr.AttributeType.FullName.Equals("System.Runtime.CompilerServices.NullableContextAttribute", StringComparison.Ordinal);

  var attrNullable = genericParameter.CustomAttributes.FirstOrDefault(IsNullableAttribute);
  var attrNullableContext =
    genericParameter.DeclaringMethod?.CustomAttributes?.FirstOrDefault(IsNullableContextAttribute) ??
    genericParameter.DeclaringType   .CustomAttributes .FirstOrDefault(IsNullableContextAttribute);

  if (attrNullableContext is not null && (byte)attrNullableContext.ConstructorArguments[0].Value == 1)
    // `#nullable enable` context
    return attrNullable is null;
  else
    // `#nullable disable` context
    return (attrNullable is not null && (byte)attrNullable.ConstructorArguments[0].Value == 1);
}