using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Diagnostics.CodeAnalysis;
public static partial class CodeAnalysisExtensions
public static bool IsNullable(this ITypeSymbol typeSymbol) =>
typeSymbol.NullableAnnotation == NullableAnnotation.Annotated;
public static bool IsNullableValueType(this ITypeSymbol typeSymbol) =>
typeSymbol.IsValueType && typeSymbol.IsNullable();
public static bool TryGetNullableValueUnderlyingType(this ITypeSymbol typeSymbol, [NotNullWhen(returnValue: true)] out ITypeSymbol? underlyingType)
if (typeSymbol is INamedTypeSymbol namedType && typeSymbol.IsNullableValueType() && namedType.IsGenericType)
var typeParameters = namedType.TypeArguments;
Debug.Assert(namedType.ConstructUnboundGenericType() is {} genericType && genericType.Name == "Nullable" && genericType.ContainingNamespace.Name == "System" && genericType.TypeArguments.Length == 1);
Debug.Assert(typeParameters.Length == 1);
underlyingType = typeParameters[0];
return underlyingType.TypeKind == TypeKind.Error ? false : true;
public static bool IsEnum(this ITypeSymbol typeSymbol) =>
typeSymbol is INamedTypeSymbol namedType && namedType.EnumUnderlyingType != null;
public static bool IsNullableEnumType(this ITypeSymbol typeSymbol) =>
typeSymbol.TryGetNullableValueUnderlyingType(out var underlyingType) == true && underlyingType.IsEnum();
public struct Nullable<T> where T : struct
public T Value { get; init; }
public enum MyEnum { Zero, One }
public MyEnum? MyNullableEnum { get; set; }
public Nullable<MyEnum> MyNullableEnum2 { get; set; }
public ValueTuple<MyEnum> MyEnumTuple{ get; set; }
public ValueTuple<MyEnum>? MyEnumNullableTuple{ get; set; }
public ValueTuple<MyEnum?> MyNullableEnumTuple{ get; set; }
public int? MyNullableInt { get; set; }
public string? MyNullableString { get; set; }
public Enum? MyNullableEnumBaseClassType { get; set; }
public Enum MyEnumBaseClassType { get; set; }
public object? MyNullableObject { get; set; }
const string programText =
// Consider this boundary case: what if somebody creates a type with the same fully qualified name as System.Nullable<T>?
// In this case the Nullable<MyEnum> MyNullableEnum2 property type's generic name will be System.Nullable but the property won't actually be nullable.
// So it is better to check IsValueType and NullableAnnotation that to check the generic name.
public struct Nullable<T> where T : struct
public T Value { get; init; }
public enum MyEnum { Zero, One }
public MyEnum? MyNullableEnum { get; set; }
public Nullable<MyEnum> MyNullableEnum2 { get; set; }
public ValueTuple<MyEnum> MyEnumTuple{ get; set; }
public ValueTuple<MyEnum>? MyEnumNullableTuple{ get; set; }
public ValueTuple<MyEnum?> MyNullableEnumTuple{ get; set; }
public int? MyNullableInt { get; set; }
public string? MyNullableString { get; set; }
public Enum? MyNullableEnumBaseClassType { get; set; }
public Enum MyEnumBaseClassType { get; set; }
public object? MyNullableObject { get; set; }
public CompilationError? MyCompilationError { get; set; }
public static void Main()
SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();
var compilation = CSharpCompilation.Create("HelloWorld")
.AddReferences(MetadataReference.CreateFromFile(
typeof(string).Assembly.Location))
var classes = root.DescendantNodes().OfType<ClassDeclarationSyntax>();
foreach (var c in classes)
var typeNodeSymbol = compilation
.GetSemanticModel(c.SyntaxTree)
var properties = typeNodeSymbol
.Where(s => s.Kind == SymbolKind.Property)
Console.WriteLine("{0}: {1}", properties.GetType(), properties.Length);
Console.WriteLine("Properties: {0}", string.Join(",", properties.AsEnumerable()));
var expectedNullableValueProperties = new [] { "MyNullableEnum", "MyNullableEnum2", "MyEnumNullableTuple", "MyNullableInt" }.ToHashSet();
foreach (var classProperty in properties)
var isEnum = classProperty.Type.IsEnum();
var isNullable = classProperty.Type.IsNullable();
var isNullableEnum = classProperty.Type.TryGetNullableValueUnderlyingType(out var underlyingType) && underlyingType.IsEnum();
if (classProperty.Type.TryGetNullableValueUnderlyingType(out var u))
Console.WriteLine(" {0}: TypeKind {1}, Underlying type: {2}, is nullable enum = {3}, underlying TypeKind = {4}", classProperty.Name, classProperty.Type.TypeKind, u.Name, classProperty.Type.IsNullableEnumType(), u.TypeKind );
Console.WriteLine(" classPropertyGenericName = {0}", GetGenericName(classProperty.Type));
NUnit.Framework.Assert.That(expectedNullableValueProperties.Contains(classProperty.Name));
Console.WriteLine(" Not a nullable value: {0} (isEnum={1}, isNullable={2}, Type.Kind={3}, IsValueType={3})", classProperty.Name, isEnum, isNullable, classProperty.Type.Kind, classProperty.Type.IsValueType);
NUnit.Framework.Assert.That(!expectedNullableValueProperties.Contains(classProperty.Name));
static string GetGenericName(ITypeSymbol typeSymbol)
if (typeSymbol is INamedTypeSymbol namedType && namedType.IsGenericType)
var unboundType = namedType.ConstructUnboundGenericType();
return unboundType.ContainingNamespace.Name + "." +unboundType.Name;
static void DumpActualProperty(Type type, string name)
var p = typeof(HelloWorld.MyClass).GetProperty(name)!;
Console.WriteLine("Name: {0}, IsValueType: {1}", p, p.PropertyType.IsValueType);
public static void Assert(bool value) => NUnit.Framework.Assert.That(value);