diff --git a/Esiur/Data/NullabilityInfo.cs b/Esiur/Data/NullabilityInfo.cs
new file mode 100644
index 0000000..92d79a1
--- /dev/null
+++ b/Esiur/Data/NullabilityInfo.cs
@@ -0,0 +1,64 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace Esiur.Data
+{
+ ///
+ /// A class that represents nullability info
+ ///
+ public sealed class NullabilityInfo
+ {
+ internal NullabilityInfo(Type type, NullabilityState readState, NullabilityState writeState,
+ NullabilityInfo? elementType, NullabilityInfo[] typeArguments)
+ {
+ Type = type;
+ ReadState = readState;
+ WriteState = writeState;
+ ElementType = elementType;
+ GenericTypeArguments = typeArguments;
+ }
+
+ ///
+ /// The of the member or generic parameter
+ /// to which this NullabilityInfo belongs
+ ///
+ public Type Type { get; }
+ ///
+ /// The nullability read state of the member
+ ///
+ public NullabilityState ReadState { get; internal set; }
+ ///
+ /// The nullability write state of the member
+ ///
+ public NullabilityState WriteState { get; internal set; }
+ ///
+ /// If the member type is an array, gives the of the elements of the array, null otherwise
+ ///
+ public NullabilityInfo? ElementType { get; }
+ ///
+ /// If the member type is a generic type, gives the array of for each type parameter
+ ///
+ public NullabilityInfo[] GenericTypeArguments { get; }
+ }
+
+ ///
+ /// An enum that represents nullability state
+ ///
+ public enum NullabilityState
+ {
+ ///
+ /// Nullability context not enabled (oblivious)
+ ///
+ Unknown,
+ ///
+ /// Non nullable value or reference type
+ ///
+ NotNull,
+ ///
+ /// Nullable value or reference type
+ ///
+ Nullable
+ }
+}
\ No newline at end of file
diff --git a/Esiur/Data/NullabilityInfoContext.cs b/Esiur/Data/NullabilityInfoContext.cs
new file mode 100644
index 0000000..57fc431
--- /dev/null
+++ b/Esiur/Data/NullabilityInfoContext.cs
@@ -0,0 +1,714 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Esiur.Data;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Reflection.Metadata;
+
+namespace Esiur.Data
+{
+ ///
+ /// Provides APIs for populating nullability information/context from reflection members:
+ /// , , and .
+ ///
+ public sealed class NullabilityInfoContext
+ {
+ private const string CompilerServicesNameSpace = "System.Runtime.CompilerServices";
+ private readonly Dictionary _publicOnlyModules = new();
+ private readonly Dictionary _context = new();
+
+ internal static bool IsSupported { get; } =
+ AppContext.TryGetSwitch("System.Reflection.NullabilityInfoContext.IsSupported", out bool isSupported) ? isSupported : true;
+
+ [Flags]
+ private enum NotAnnotatedStatus
+ {
+ None = 0x0, // no restriction, all members annotated
+ Private = 0x1, // private members not annotated
+ Internal = 0x2 // internal members not annotated
+ }
+
+ private NullabilityState? GetNullableContext(MemberInfo? memberInfo)
+ {
+ while (memberInfo != null)
+ {
+ if (_context.TryGetValue(memberInfo, out NullabilityState state))
+ {
+ return state;
+ }
+
+ foreach (CustomAttributeData attribute in memberInfo.GetCustomAttributesData())
+ {
+ if (attribute.AttributeType.Name == "NullableContextAttribute" &&
+ attribute.AttributeType.Namespace == CompilerServicesNameSpace &&
+ attribute.ConstructorArguments.Count == 1)
+ {
+ state = TranslateByte(attribute.ConstructorArguments[0].Value);
+ _context.Add(memberInfo, state);
+ return state;
+ }
+ }
+
+ memberInfo = memberInfo.DeclaringType;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Populates for the given .
+ /// If the nullablePublicOnly feature is set for an assembly, like it does in .NET SDK, the private and/or internal member's
+ /// nullability attributes are omitted, in this case the API will return NullabilityState.Unknown state.
+ ///
+ /// The parameter which nullability info gets populated
+ /// If the parameterInfo parameter is null
+ ///
+ public NullabilityInfo Create(ParameterInfo parameterInfo)
+ {
+ if (parameterInfo == null)
+ throw new ArgumentNullException();
+
+ EnsureIsSupported();
+
+ IList attributes = parameterInfo.GetCustomAttributesData();
+ NullableAttributeStateParser parser = parameterInfo.Member is MethodBase method && IsPrivateOrInternalMethodAndAnnotationDisabled(method)
+ ? NullableAttributeStateParser.Unknown
+ : CreateParser(attributes);
+ NullabilityInfo nullability = GetNullabilityInfo(parameterInfo.Member, parameterInfo.ParameterType, parser);
+
+ if (nullability.ReadState != NullabilityState.Unknown)
+ {
+ CheckParameterMetadataType(parameterInfo, nullability);
+ }
+
+ CheckNullabilityAttributes(nullability, attributes);
+ return nullability;
+ }
+
+ private void CheckParameterMetadataType(ParameterInfo parameter, NullabilityInfo nullability)
+ {
+ if (parameter.Member is MethodInfo method)
+ {
+ MethodInfo metaMethod = GetMethodMetadataDefinition(method);
+ ParameterInfo? metaParameter = null;
+ if (string.IsNullOrEmpty(parameter.Name))
+ {
+ metaParameter = metaMethod.ReturnParameter;
+ }
+ else
+ {
+ ParameterInfo[] parameters = metaMethod.GetParameters();
+ for (int i = 0; i < parameters.Length; i++)
+ {
+ if (parameter.Position == i &&
+ parameter.Name == parameters[i].Name)
+ {
+ metaParameter = parameters[i];
+ break;
+ }
+ }
+ }
+
+ if (metaParameter != null)
+ {
+ CheckGenericParameters(nullability, metaMethod, metaParameter.ParameterType, parameter.Member.ReflectedType);
+ }
+ }
+ }
+
+ private static MethodInfo GetMethodMetadataDefinition(MethodInfo method)
+ {
+ if (method.IsGenericMethod && !method.IsGenericMethodDefinition)
+ {
+ method = method.GetGenericMethodDefinition();
+ }
+
+ return (MethodInfo)GetMemberMetadataDefinition(method);
+ }
+
+ private static void CheckNullabilityAttributes(NullabilityInfo nullability, IList attributes)
+ {
+ var codeAnalysisReadState = NullabilityState.Unknown;
+ var codeAnalysisWriteState = NullabilityState.Unknown;
+
+ foreach (CustomAttributeData attribute in attributes)
+ {
+ if (attribute.AttributeType.Namespace == "System.Diagnostics.CodeAnalysis")
+ {
+ if (attribute.AttributeType.Name == "NotNullAttribute")
+ {
+ codeAnalysisReadState = NullabilityState.NotNull;
+ }
+ else if ((attribute.AttributeType.Name == "MaybeNullAttribute" ||
+ attribute.AttributeType.Name == "MaybeNullWhenAttribute") &&
+ codeAnalysisReadState == NullabilityState.Unknown &&
+ !IsValueTypeOrValueTypeByRef(nullability.Type))
+ {
+ codeAnalysisReadState = NullabilityState.Nullable;
+ }
+ else if (attribute.AttributeType.Name == "DisallowNullAttribute")
+ {
+ codeAnalysisWriteState = NullabilityState.NotNull;
+ }
+ else if (attribute.AttributeType.Name == "AllowNullAttribute" &&
+ codeAnalysisWriteState == NullabilityState.Unknown &&
+ !IsValueTypeOrValueTypeByRef(nullability.Type))
+ {
+ codeAnalysisWriteState = NullabilityState.Nullable;
+ }
+ }
+ }
+
+ if (codeAnalysisReadState != NullabilityState.Unknown)
+ {
+ nullability.ReadState = codeAnalysisReadState;
+ }
+ if (codeAnalysisWriteState != NullabilityState.Unknown)
+ {
+ nullability.WriteState = codeAnalysisWriteState;
+ }
+ }
+
+ ///
+ /// Populates for the given .
+ /// If the nullablePublicOnly feature is set for an assembly, like it does in .NET SDK, the private and/or internal member's
+ /// nullability attributes are omitted, in this case the API will return NullabilityState.Unknown state.
+ ///
+ /// The parameter which nullability info gets populated
+ /// If the propertyInfo parameter is null
+ ///
+ public NullabilityInfo Create(PropertyInfo propertyInfo)
+ {
+ if (propertyInfo == null)
+ throw new ArgumentNullException();
+
+ EnsureIsSupported();
+
+ MethodInfo? getter = propertyInfo.GetGetMethod(true);
+ MethodInfo? setter = propertyInfo.GetSetMethod(true);
+ bool annotationsDisabled = (getter == null || IsPrivateOrInternalMethodAndAnnotationDisabled(getter))
+ && (setter == null || IsPrivateOrInternalMethodAndAnnotationDisabled(setter));
+ NullableAttributeStateParser parser = annotationsDisabled ? NullableAttributeStateParser.Unknown : CreateParser(propertyInfo.GetCustomAttributesData());
+ NullabilityInfo nullability = GetNullabilityInfo(propertyInfo, propertyInfo.PropertyType, parser);
+
+ if (getter != null)
+ {
+ CheckNullabilityAttributes(nullability, getter.ReturnParameter.GetCustomAttributesData());
+ }
+ else
+ {
+ nullability.ReadState = NullabilityState.Unknown;
+ }
+
+ if (setter != null)
+ {
+ CheckNullabilityAttributes(nullability, setter.GetParameters().Last().GetCustomAttributesData());
+ }
+ else
+ {
+ nullability.WriteState = NullabilityState.Unknown;
+ }
+
+ return nullability;
+ }
+
+ /////
+ ///// Populates for the given .
+ ///// If the nullablePublicOnly feature is set for an assembly, like it does in .NET SDK, the private and/or internal member's
+ ///// nullability attributes are omitted, in this case the API will return NullabilityState.Unknown state.
+ /////
+ ///// The parameter which nullability info gets populated
+ ///// If the propertyInfo parameter is null
+ /////
+ //public NullabilityInfo Create(MethodInfo memberInfo)
+ //{
+ // if (memberInfo == null)
+ // throw new ArgumentNullException();
+
+ // EnsureIsSupported();
+
+
+ // bool annotationsDisabled = IsPrivateOrInternalMethodAndAnnotationDisabled(memberInfo);
+
+ // NullableAttributeStateParser parser = annotationsDisabled ? NullableAttributeStateParser.Unknown : CreateParser(memberInfo.GetCustomAttributesData());
+ // NullabilityInfo nullability = GetNullabilityInfo(memberInfo, memberInfo.ReturnType, parser);
+
+
+ // CheckNullabilityAttributes(nullability, memberInfo.ReturnParameter.GetCustomAttributesData());
+
+ // return nullability;
+ //}
+
+ private bool IsPrivateOrInternalMethodAndAnnotationDisabled(MethodBase method)
+ {
+ if ((method.IsPrivate || method.IsFamilyAndAssembly || method.IsAssembly) &&
+ IsPublicOnly(method.IsPrivate, method.IsFamilyAndAssembly, method.IsAssembly, method.Module))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Populates for the given .
+ /// If the nullablePublicOnly feature is set for an assembly, like it does in .NET SDK, the private and/or internal member's
+ /// nullability attributes are omitted, in this case the API will return NullabilityState.Unknown state.
+ ///
+ /// The parameter which nullability info gets populated
+ /// If the eventInfo parameter is null
+ ///
+ public NullabilityInfo Create(EventInfo eventInfo)
+ {
+ if (eventInfo == null)
+ throw new ArgumentNullException();
+
+ EnsureIsSupported();
+
+ return GetNullabilityInfo(eventInfo, eventInfo.EventHandlerType!, CreateParser(eventInfo.GetCustomAttributesData()));
+ }
+
+ ///
+ /// Populates for the given
+ /// If the nullablePublicOnly feature is set for an assembly, like it does in .NET SDK, the private and/or internal member's
+ /// nullability attributes are omitted, in this case the API will return NullabilityState.Unknown state.
+ ///
+ /// The parameter which nullability info gets populated
+ /// If the fieldInfo parameter is null
+ ///
+ public NullabilityInfo Create(FieldInfo fieldInfo)
+ {
+ if (fieldInfo == null)
+ throw new ArgumentNullException();
+
+ EnsureIsSupported();
+
+ IList attributes = fieldInfo.GetCustomAttributesData();
+ NullableAttributeStateParser parser = IsPrivateOrInternalFieldAndAnnotationDisabled(fieldInfo) ? NullableAttributeStateParser.Unknown : CreateParser(attributes);
+ NullabilityInfo nullability = GetNullabilityInfo(fieldInfo, fieldInfo.FieldType, parser);
+ CheckNullabilityAttributes(nullability, attributes);
+ return nullability;
+ }
+
+ private static void EnsureIsSupported()
+ {
+ if (!IsSupported)
+ {
+ throw new InvalidOperationException();
+ }
+ }
+
+ private bool IsPrivateOrInternalFieldAndAnnotationDisabled(FieldInfo fieldInfo)
+ {
+ if ((fieldInfo.IsPrivate || fieldInfo.IsFamilyAndAssembly || fieldInfo.IsAssembly) &&
+ IsPublicOnly(fieldInfo.IsPrivate, fieldInfo.IsFamilyAndAssembly, fieldInfo.IsAssembly, fieldInfo.Module))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ private bool IsPublicOnly(bool isPrivate, bool isFamilyAndAssembly, bool isAssembly, Module module)
+ {
+ if (!_publicOnlyModules.TryGetValue(module, out NotAnnotatedStatus value))
+ {
+ value = PopulateAnnotationInfo(module.GetCustomAttributesData());
+ _publicOnlyModules.Add(module, value);
+ }
+
+ if (value == NotAnnotatedStatus.None)
+ {
+ return false;
+ }
+
+ if ((isPrivate || isFamilyAndAssembly) && value.HasFlag(NotAnnotatedStatus.Private) ||
+ isAssembly && value.HasFlag(NotAnnotatedStatus.Internal))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static NotAnnotatedStatus PopulateAnnotationInfo(IList customAttributes)
+ {
+ foreach (CustomAttributeData attribute in customAttributes)
+ {
+ if (attribute.AttributeType.Name == "NullablePublicOnlyAttribute" &&
+ attribute.AttributeType.Namespace == CompilerServicesNameSpace &&
+ attribute.ConstructorArguments.Count == 1)
+ {
+ if (attribute.ConstructorArguments[0].Value is bool boolValue && boolValue)
+ {
+ return NotAnnotatedStatus.Internal | NotAnnotatedStatus.Private;
+ }
+ else
+ {
+ return NotAnnotatedStatus.Private;
+ }
+ }
+ }
+
+ return NotAnnotatedStatus.None;
+ }
+
+ private NullabilityInfo GetNullabilityInfo(MemberInfo memberInfo, Type type, NullableAttributeStateParser parser)
+ {
+ int index = 0;
+ NullabilityInfo nullability = GetNullabilityInfo(memberInfo, type, parser, ref index);
+
+ if (nullability.ReadState != NullabilityState.Unknown)
+ {
+ TryLoadGenericMetaTypeNullability(memberInfo, nullability);
+ }
+
+ return nullability;
+ }
+
+ private NullabilityInfo GetNullabilityInfo(MemberInfo memberInfo, Type type, NullableAttributeStateParser parser, ref int index)
+ {
+ NullabilityState state = NullabilityState.Unknown;
+ NullabilityInfo? elementState = null;
+ NullabilityInfo[] genericArgumentsState = Array.Empty();
+ Type underlyingType = type;
+
+ if (underlyingType.IsByRef || underlyingType.IsPointer)
+ {
+ underlyingType = underlyingType.GetElementType()!;
+ }
+
+ if (underlyingType.IsValueType)
+ {
+ if (Nullable.GetUnderlyingType(underlyingType) is { } nullableUnderlyingType)
+ {
+ underlyingType = nullableUnderlyingType;
+ state = NullabilityState.Nullable;
+ }
+ else
+ {
+ state = NullabilityState.NotNull;
+ }
+
+ if (underlyingType.IsGenericType)
+ {
+ ++index;
+ }
+ }
+ else
+ {
+ if (!parser.ParseNullableState(index++, ref state)
+ && GetNullableContext(memberInfo) is { } contextState)
+ {
+ state = contextState;
+ }
+
+ if (underlyingType.IsArray)
+ {
+ elementState = GetNullabilityInfo(memberInfo, underlyingType.GetElementType()!, parser, ref index);
+ }
+ }
+
+ if (underlyingType.IsGenericType)
+ {
+ Type[] genericArguments = underlyingType.GetGenericArguments();
+ genericArgumentsState = new NullabilityInfo[genericArguments.Length];
+
+ for (int i = 0; i < genericArguments.Length; i++)
+ {
+ genericArgumentsState[i] = GetNullabilityInfo(memberInfo, genericArguments[i], parser, ref index);
+ }
+ }
+
+ return new NullabilityInfo(type, state, state, elementState, genericArgumentsState);
+ }
+
+ private static NullableAttributeStateParser CreateParser(IList customAttributes)
+ {
+ foreach (CustomAttributeData attribute in customAttributes)
+ {
+ if (attribute.AttributeType.Name == "NullableAttribute" &&
+ attribute.AttributeType.Namespace == CompilerServicesNameSpace &&
+ attribute.ConstructorArguments.Count == 1)
+ {
+ return new NullableAttributeStateParser(attribute.ConstructorArguments[0].Value);
+ }
+ }
+
+ return new NullableAttributeStateParser(null);
+ }
+
+ private void TryLoadGenericMetaTypeNullability(MemberInfo memberInfo, NullabilityInfo nullability)
+ {
+ MemberInfo? metaMember = GetMemberMetadataDefinition(memberInfo);
+ Type? metaType = null;
+ if (metaMember is FieldInfo field)
+ {
+ metaType = field.FieldType;
+ }
+ else if (metaMember is PropertyInfo property)
+ {
+ metaType = GetPropertyMetaType(property);
+ }
+
+ if (metaType != null)
+ {
+ CheckGenericParameters(nullability, metaMember!, metaType, memberInfo.ReflectedType);
+ }
+ }
+
+ private static MemberInfo GetMemberMetadataDefinition(MemberInfo member)
+ {
+ Type? type = member.DeclaringType;
+ if ((type != null) && type.IsGenericType && !type.IsGenericTypeDefinition)
+ {
+ return GetMemberWithSameMetadataDefinitionAs(type.GetGenericTypeDefinition(), member);
+ }
+
+ return member;
+ }
+
+ static bool HasSameMetadataDefinitionAs(MemberInfo mi, MemberInfo other) { throw new NotImplementedException(); }
+
+ static MemberInfo GetMemberWithSameMetadataDefinitionAs(Type type, MemberInfo member)
+ {
+ if (member == null)
+ throw new ArgumentNullException();
+
+ const BindingFlags all = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance;
+ foreach (MemberInfo myMemberInfo in type.GetMembers(all))
+ {
+ if (HasSameMetadataDefinitionAs(myMemberInfo, member))
+ {
+ return myMemberInfo;
+ }
+ }
+
+ throw new Exception();
+ }
+
+ private static Type GetPropertyMetaType(PropertyInfo property)
+ {
+ if (property.GetGetMethod(true) is MethodInfo method)
+ {
+ return method.ReturnType;
+ }
+
+ return property.GetSetMethod(true)!.GetParameters()[0].ParameterType;
+ }
+
+ private void CheckGenericParameters(NullabilityInfo nullability, MemberInfo metaMember, Type metaType, Type? reflectedType)
+ {
+ if (metaType.IsGenericParameter)
+ {
+ if (nullability.ReadState == NullabilityState.NotNull)
+ {
+ TryUpdateGenericParameterNullability(nullability, metaType, reflectedType);
+ }
+ }
+ else if (metaType.ContainsGenericParameters)
+ {
+ if (nullability.GenericTypeArguments.Length > 0)
+ {
+ Type[] genericArguments = metaType.GetGenericArguments();
+
+ for (int i = 0; i < genericArguments.Length; i++)
+ {
+ CheckGenericParameters(nullability.GenericTypeArguments[i], metaMember, genericArguments[i], reflectedType);
+ }
+ }
+ else if (nullability.ElementType is { } elementNullability && metaType.IsArray)
+ {
+ CheckGenericParameters(elementNullability, metaMember, metaType.GetElementType()!, reflectedType);
+ }
+ // We could also follow this branch for metaType.IsPointer, but since pointers must be unmanaged this
+ // will be a no-op regardless
+ else if (metaType.IsByRef)
+ {
+ CheckGenericParameters(nullability, metaMember, metaType.GetElementType()!, reflectedType);
+ }
+ }
+ }
+
+ private bool TryUpdateGenericParameterNullability(NullabilityInfo nullability, Type genericParameter, Type? reflectedType)
+ {
+ Debug.Assert(genericParameter.IsGenericParameter);
+
+ if (reflectedType is not null
+ && !IsGenericMethodParameter(genericParameter)
+ && TryUpdateGenericTypeParameterNullabilityFromReflectedType(nullability, genericParameter, reflectedType, reflectedType))
+ {
+ return true;
+ }
+
+ if (IsValueTypeOrValueTypeByRef(nullability.Type))
+ {
+ return true;
+ }
+
+ var state = NullabilityState.Unknown;
+ if (CreateParser(genericParameter.GetCustomAttributesData()).ParseNullableState(0, ref state))
+ {
+ nullability.ReadState = state;
+ nullability.WriteState = state;
+ return true;
+ }
+
+ if (GetNullableContext(genericParameter) is { } contextState)
+ {
+ nullability.ReadState = contextState;
+ nullability.WriteState = contextState;
+ return true;
+ }
+ return false;
+ }
+
+ bool IsGenericMethodParameter(Type genericParameter) => genericParameter.IsGenericParameter && genericParameter.DeclaringMethod != null;
+
+ private bool TryUpdateGenericTypeParameterNullabilityFromReflectedType(NullabilityInfo nullability, Type genericParameter, Type context, Type reflectedType)
+ {
+ Debug.Assert(genericParameter.IsGenericParameter && !IsGenericMethodParameter(genericParameter));
+
+ Type contextTypeDefinition = context.IsGenericType && !context.IsGenericTypeDefinition ? context.GetGenericTypeDefinition() : context;
+ if (genericParameter.DeclaringType == contextTypeDefinition)
+ {
+ return false;
+ }
+
+ Type? baseType = contextTypeDefinition.BaseType;
+ if (baseType is null)
+ {
+ return false;
+ }
+
+ if (!baseType.IsGenericType
+ || (baseType.IsGenericTypeDefinition ? baseType : baseType.GetGenericTypeDefinition()) != genericParameter.DeclaringType)
+ {
+ return TryUpdateGenericTypeParameterNullabilityFromReflectedType(nullability, genericParameter, baseType, reflectedType);
+ }
+
+ Type[] genericArguments = baseType.GetGenericArguments();
+ Type genericArgument = genericArguments[genericParameter.GenericParameterPosition];
+ if (genericArgument.IsGenericParameter)
+ {
+ return TryUpdateGenericParameterNullability(nullability, genericArgument, reflectedType);
+ }
+
+ NullableAttributeStateParser parser = CreateParser(contextTypeDefinition.GetCustomAttributesData());
+ int nullabilityStateIndex = 1; // start at 1 since index 0 is the type itself
+ for (int i = 0; i < genericParameter.GenericParameterPosition; i++)
+ {
+ nullabilityStateIndex += CountNullabilityStates(genericArguments[i]);
+ }
+ return TryPopulateNullabilityInfo(nullability, parser, ref nullabilityStateIndex);
+
+ static int CountNullabilityStates(Type type)
+ {
+ Type underlyingType = Nullable.GetUnderlyingType(type) ?? type;
+ if (underlyingType.IsGenericType)
+ {
+ int count = 1;
+ foreach (Type genericArgument in underlyingType.GetGenericArguments())
+ {
+ count += CountNullabilityStates(genericArgument);
+ }
+ return count;
+ }
+
+ if (underlyingType.HasElementType)
+ {
+ return (underlyingType.IsArray ? 1 : 0) + CountNullabilityStates(underlyingType.GetElementType()!);
+ }
+
+ return type.IsValueType ? 0 : 1;
+ }
+ }
+
+ private static bool TryPopulateNullabilityInfo(NullabilityInfo nullability, NullableAttributeStateParser parser, ref int index)
+ {
+ bool isValueType = IsValueTypeOrValueTypeByRef(nullability.Type);
+ if (!isValueType)
+ {
+ var state = NullabilityState.Unknown;
+ if (!parser.ParseNullableState(index, ref state))
+ {
+ return false;
+ }
+
+ nullability.ReadState = state;
+ nullability.WriteState = state;
+ }
+
+ if (!isValueType || (Nullable.GetUnderlyingType(nullability.Type) ?? nullability.Type).IsGenericType)
+ {
+ index++;
+ }
+
+ if (nullability.GenericTypeArguments.Length > 0)
+ {
+ foreach (NullabilityInfo genericTypeArgumentNullability in nullability.GenericTypeArguments)
+ {
+ TryPopulateNullabilityInfo(genericTypeArgumentNullability, parser, ref index);
+ }
+ }
+ else if (nullability.ElementType is { } elementTypeNullability)
+ {
+ TryPopulateNullabilityInfo(elementTypeNullability, parser, ref index);
+ }
+
+ return true;
+ }
+
+ private static NullabilityState TranslateByte(object? value)
+ {
+ return value is byte b ? TranslateByte(b) : NullabilityState.Unknown;
+ }
+
+ private static NullabilityState TranslateByte(byte b) =>
+ b switch
+ {
+ 1 => NullabilityState.NotNull,
+ 2 => NullabilityState.Nullable,
+ _ => NullabilityState.Unknown
+ };
+
+ private static bool IsValueTypeOrValueTypeByRef(Type type) =>
+ type.IsValueType || ((type.IsByRef || type.IsPointer) && type.GetElementType()!.IsValueType);
+
+ private readonly struct NullableAttributeStateParser
+ {
+ private static readonly object UnknownByte = (byte)0;
+
+ private readonly object? _nullableAttributeArgument;
+
+ public NullableAttributeStateParser(object? nullableAttributeArgument)
+ {
+ this._nullableAttributeArgument = nullableAttributeArgument;
+ }
+
+ public static NullableAttributeStateParser Unknown => new(UnknownByte);
+
+ public bool ParseNullableState(int index, ref NullabilityState state)
+ {
+ switch (this._nullableAttributeArgument)
+ {
+ case byte b:
+ state = TranslateByte(b);
+ return true;
+ case ReadOnlyCollection args
+ when index < args.Count && args[index].Value is byte elementB:
+ state = TranslateByte(elementB);
+ return true;
+ default:
+ return false;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Esiur/Esiur.csproj b/Esiur/Esiur.csproj
index 7f38f52..b3dbf05 100644
--- a/Esiur/Esiur.csproj
+++ b/Esiur/Esiur.csproj
@@ -56,8 +56,16 @@
+
+
+
+
+
+
+
+
-
+
diff --git a/Esiur/Resource/Template/ConstantTemplate.cs b/Esiur/Resource/Template/ConstantTemplate.cs
index 8395277..ffb68fd 100644
--- a/Esiur/Resource/Template/ConstantTemplate.cs
+++ b/Esiur/Resource/Template/ConstantTemplate.cs
@@ -72,7 +72,6 @@ public class ConstantTemplate : MemberTemplate
public static ConstantTemplate MakeConstantTemplate(Type type, FieldInfo ci, byte index = 0, string customName = null, TypeTemplate typeTemplate = null)
{
var annotationAttr = ci.GetCustomAttribute(true);
- var nullableAttr = ci.GetCustomAttribute(true);
var valueType = RepresentationType.FromType(ci.FieldType);
diff --git a/Esiur/Resource/Template/EventTemplate.cs b/Esiur/Resource/Template/EventTemplate.cs
index 928ae68..15ce70e 100644
--- a/Esiur/Resource/Template/EventTemplate.cs
+++ b/Esiur/Resource/Template/EventTemplate.cs
@@ -82,29 +82,32 @@ public class EventTemplate : MemberTemplate
var annotationAttr = ei.GetCustomAttribute(true);
var listenableAttr = ei.GetCustomAttribute(true);
- var nullableAttr = ei.GetCustomAttribute(true);
- var nullableContextAttr = ei.GetCustomAttribute(true);
- var flags = nullableAttr?.Flags?.ToList() ?? new List();
+ evtType.Nullable = new NullabilityInfoContext().Create(ei).ReadState is NullabilityState.Nullable;
- // skip the eventHandler class
- if (flags.Count > 1)
- flags = flags.Skip(1).ToList();
+ //var nullableAttr = ei.GetCustomAttribute(true);
+ //var nullableContextAttr = ei.GetCustomAttribute(true);
- if (nullableContextAttr?.Flag == 2)
- {
- if (flags.Count == 1)
- evtType.SetNotNull(flags.FirstOrDefault());
- else
- evtType.SetNotNull(flags);
- }
- else
- {
- if (flags.Count == 1)
- evtType.SetNull(flags.FirstOrDefault());
- else
- evtType.SetNull(flags);
- }
+ //var flags = nullableAttr?.Flags?.ToList() ?? new List();
+
+ //// skip the eventHandler class
+ //if (flags.Count > 1)
+ // flags = flags.Skip(1).ToList();
+
+ //if (nullableContextAttr?.Flag == 2)
+ //{
+ // if (flags.Count == 1)
+ // evtType.SetNotNull(flags.FirstOrDefault());
+ // else
+ // evtType.SetNotNull(flags);
+ //}
+ //else
+ //{
+ // if (flags.Count == 1)
+ // evtType.SetNull(flags.FirstOrDefault());
+ // else
+ // evtType.SetNull(flags);
+ //}
var et = new EventTemplate(typeTemplate, index, customName ?? ei.Name, ei.DeclaringType != type, evtType);
et.EventInfo = ei;
diff --git a/Esiur/Resource/Template/FunctionTemplate.cs b/Esiur/Resource/Template/FunctionTemplate.cs
index 7f4e2cf..cd05102 100644
--- a/Esiur/Resource/Template/FunctionTemplate.cs
+++ b/Esiur/Resource/Template/FunctionTemplate.cs
@@ -90,36 +90,42 @@ public class FunctionTemplate : MemberTemplate
throw new Exception($"Unsupported type `{mi.ReturnType}` in method `{type.Name}.{mi.Name}` return");
var annotationAttr = mi.GetCustomAttribute(true);
- var nullableAttr = mi.GetCustomAttribute(true);
- var nullableContextAttr = mi.GetCustomAttribute(true);
- var flags = nullableAttr?.Flags?.ToList() ?? new List();
+ var nullabilityInfoContext = new NullabilityInfoContext();
- var rtNullableAttr = mi.ReturnTypeCustomAttributes.GetCustomAttributes(typeof(NullableAttribute), true).FirstOrDefault() as NullableAttribute;
- var rtNullableContextAttr = mi.ReturnTypeCustomAttributes
- .GetCustomAttributes(typeof(NullableContextAttribute), true)
- .FirstOrDefault() as NullableContextAttribute
- ?? nullableContextAttr;
+ rtType.Nullable = nullabilityInfoContext.Create(mi.ReturnParameter).WriteState is NullabilityState.Nullable;
- var rtFlags = rtNullableAttr?.Flags?.ToList() ?? new List();
- if (rtFlags.Count > 0 && genericRtType == typeof(AsyncReply<>))
- rtFlags.RemoveAt(0);
+ //var nullableAttr = mi.GetCustomAttribute(true);
+ //var nullableContextAttr = mi.GetCustomAttribute(true);
- if (rtNullableContextAttr?.Flag == 2)
- {
- if (rtFlags.Count == 1)
- rtType.SetNotNull(rtFlags.FirstOrDefault());
- else
- rtType.SetNotNull(rtFlags);
- }
- else
- {
- if (rtFlags.Count == 1)
- rtType.SetNull(rtFlags.FirstOrDefault());
- else
- rtType.SetNull(rtFlags);
- }
+ //var flags = nullableAttr?.Flags?.ToList() ?? new List();
+
+ //var rtNullableAttr = mi.ReturnTypeCustomAttributes.GetCustomAttributes(typeof(NullableAttribute), true).FirstOrDefault() as NullableAttribute;
+ //var rtNullableContextAttr = mi.ReturnTypeCustomAttributes
+ // .GetCustomAttributes(typeof(NullableContextAttribute), true)
+ // .FirstOrDefault() as NullableContextAttribute
+ // ?? nullableContextAttr;
+
+ //var rtFlags = rtNullableAttr?.Flags?.ToList() ?? new List();
+
+ //if (rtFlags.Count > 0 && genericRtType == typeof(AsyncReply<>))
+ // rtFlags.RemoveAt(0);
+
+ //if (rtNullableContextAttr?.Flag == 2)
+ //{
+ // if (rtFlags.Count == 1)
+ // rtType.SetNotNull(rtFlags.FirstOrDefault());
+ // else
+ // rtType.SetNotNull(rtFlags);
+ //}
+ //else
+ //{
+ // if (rtFlags.Count == 1)
+ // rtType.SetNull(rtFlags.FirstOrDefault());
+ // else
+ // rtType.SetNull(rtFlags);
+ //}
var args = mi.GetParameters();
@@ -136,27 +142,28 @@ public class FunctionTemplate : MemberTemplate
if (argType == null)
throw new Exception($"Unsupported type `{x.ParameterType}` in method `{type.Name}.{mi.Name}` parameter `{x.Name}`");
+ argType.Nullable = nullabilityInfoContext.Create(x).WriteState is NullabilityState.Nullable;
- var argNullableAttr = x.GetCustomAttribute(true);
- var argNullableContextAttr = x.GetCustomAttribute(true) ?? nullableContextAttr;
+ //var argNullableAttr = x.GetCustomAttribute(true);
+ //var argNullableContextAttr = x.GetCustomAttribute(true) ?? nullableContextAttr;
- var argFlags = argNullableAttr?.Flags?.ToList() ?? new List();
+ //var argFlags = argNullableAttr?.Flags?.ToList() ?? new List();
- if (argNullableContextAttr?.Flag == 2)
- {
- if (argFlags.Count == 1)
- argType.SetNotNull(argFlags.FirstOrDefault());
- else
- argType.SetNotNull(argFlags);
- }
- else
- {
- if (rtFlags.Count == 1)
- argType.SetNull(argFlags.FirstOrDefault());
- else
- argType.SetNull(argFlags);
- }
+ //if (argNullableContextAttr?.Flag == 2)
+ //{
+ // if (argFlags.Count == 1)
+ // argType.SetNotNull(argFlags.FirstOrDefault());
+ // else
+ // argType.SetNotNull(argFlags);
+ //}
+ //else
+ //{
+ // if (rtFlags.Count == 1)
+ // argType.SetNull(argFlags.FirstOrDefault());
+ // else
+ // argType.SetNull(argFlags);
+ //}
return new ArgumentTemplate()
{
diff --git a/Esiur/Resource/Template/PropertyTemplate.cs b/Esiur/Resource/Template/PropertyTemplate.cs
index fd2e38d..028744b 100644
--- a/Esiur/Resource/Template/PropertyTemplate.cs
+++ b/Esiur/Resource/Template/PropertyTemplate.cs
@@ -139,6 +139,9 @@ public class PropertyTemplate : MemberTemplate
RepresentationType valueType, string readAnnotation = null, string writeAnnotation = null, bool recordable = false)
: base(template, index, name, inherited)
{
+ if (name == "RecordNullable")
+ Console.Beep();
+
this.Recordable = recordable;
//this.Storage = storage;
if (readAnnotation != null)
@@ -163,28 +166,32 @@ public class PropertyTemplate : MemberTemplate
var annotationAttr = pi.GetCustomAttribute(true);
var storageAttr = pi.GetCustomAttribute(true);
- var nullableContextAttr = pi.GetCustomAttribute(true);
- var nullableAttr = pi.GetCustomAttribute(true);
+ var nullabilityContext = new NullabilityInfoContext();
+ propType.Nullable = nullabilityContext.Create(pi).ReadState is NullabilityState.Nullable;
- var flags = nullableAttr?.Flags?.ToList() ?? new List();
- if (flags.Count > 0 && genericPropType == typeof(DistributedPropertyContext<>))
- flags.RemoveAt(0);
+ // var nullableContextAttr = pi.GetCustomAttribute(true);
+ // var nullableAttr = pi.GetCustomAttribute(true);
- if (nullableContextAttr?.Flag == 2)
- {
- if (flags.Count == 1)
- propType.SetNotNull(flags.FirstOrDefault());
- else
- propType.SetNotNull(flags);
- }
- else
- {
- if (flags.Count == 1)
- propType.SetNull(flags.FirstOrDefault());
- else
- propType.SetNull(flags);
- }
+ // var flags = nullableAttr?.Flags?.ToList() ?? new List();
+
+ // if (flags.Count > 0 && genericPropType == typeof(DistributedPropertyContext<>))
+ // flags.RemoveAt(0);
+
+ // if (nullableContextAttr?.Flag == 2)
+ // {
+ // if (flags.Count == 1)
+ // propType.SetNotNull(flags.FirstOrDefault());
+ // else
+ // propType.SetNotNull(flags);
+ // }
+ // else
+ // {
+ // if (flags.Count == 1)
+ // propType.SetNull(flags.FirstOrDefault());
+ // else
+ // propType.SetNull(flags);
+ // }
var pt = new PropertyTemplate(typeTemplate, index, customName ?? pi.Name, pi.DeclaringType != type, propType);
diff --git a/Esiur/Resource/Template/TypeTemplate.cs b/Esiur/Resource/Template/TypeTemplate.cs
index 3ad6019..d30961b 100644
--- a/Esiur/Resource/Template/TypeTemplate.cs
+++ b/Esiur/Resource/Template/TypeTemplate.cs
@@ -376,7 +376,6 @@ public class TypeTemplate
public static ConstantTemplate MakeConstantTemplate(Type type, FieldInfo ci, ExportAttribute exportAttr, byte index = 0, TypeTemplate typeTemplate = null)
{
var annotationAttr = ci.GetCustomAttribute(true);
- var nullableAttr = ci.GetCustomAttribute(true);
var valueType = RepresentationType.FromType(ci.FieldType);
diff --git a/Test/MyService.cs b/Test/MyService.cs
index 79123da..80e5e78 100644
--- a/Test/MyService.cs
+++ b/Test/MyService.cs
@@ -120,6 +120,8 @@ public partial class MyService
[Export] public MyRecord Record => new MyRecord() { Id = 33, Name = "Test", Score = 99.33 };
+ [Export] public MyRecord? RecordNullable => new MyRecord() { Id = 33, Name = "Test Nullable", Score = 99.33 };
+
[Export] public List IntList => new List() { 1, 2, 3, 4, 5 };
[Export] public IRecord[] RecordsArray => new IRecord[] { new MyRecord() { Id = 22, Name = "Test", Score = 22.1 } };