// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; namespace AntDesign.core.Helpers { public static class PropertyAccessHelper { public const string DefaultPathSeparator = "."; private const string Nullable_HasValue = "HasValue"; private const string Nullable_Value = "Value"; private const string CountPropertyName = "Count"; private const string GetItemMethodName = "get_Item"; #region Build not Nullable delegate public static LambdaExpression BuildAccessPropertyLambdaExpression([NotNull] this Type type, [NotNull] string propertyPath) { return BuildAccessPropertyLambdaExpression(type, propertyPath.Split(DefaultPathSeparator)); } public static LambdaExpression BuildAccessPropertyLambdaExpression([NotNull] this Type type, [NotNull] string[] properties) { ArgumentNotNull(type, nameof(type)); ArgumentNotEmpty(properties, nameof(properties)); var parameterExpression = Expression.Parameter(type); var expression = AccessProperty(parameterExpression, properties); return Expression.Lambda(expression, parameterExpression); } public static Expression> BuildAccessPropertyLambdaExpression([NotNull] this Type type, [NotNull] string propertyPath) { return BuildAccessPropertyLambdaExpression(type, propertyPath.Split(DefaultPathSeparator)); } public static Expression> BuildAccessPropertyLambdaExpression([NotNull] this Type type, [NotNull] string[] properties) { ArgumentNotNull(type, nameof(type)); ArgumentNotEmpty(properties, nameof(properties)); var parameterExpression = Expression.Parameter(type); var expression = AccessProperty(parameterExpression, properties); return Expression.Lambda>(expression, parameterExpression); } public static Expression> BuildAccessPropertyLambdaExpression( [NotNull] this ParameterExpression parameterExpression, [NotNull] string[] properties) { ArgumentNotNull(parameterExpression, nameof(parameterExpression)); var expression = AccessProperty(parameterExpression, properties); return Expression.Lambda>(expression, parameterExpression); } #endregion Build not Nullable delegate #region Build Nullable delegate public static LambdaExpression BuildAccessNullablePropertyLambdaExpression([NotNull] this Type type, [NotNull] string propertyPath) { return BuildAccessNullablePropertyLambdaExpression(type, propertyPath.Split(DefaultPathSeparator)); } public static LambdaExpression BuildAccessNullablePropertyLambdaExpression([NotNull] this Type type, [NotNull] string[] properties) { ArgumentNotNull(type, nameof(type)); ArgumentNotEmpty(properties, nameof(properties)); var parameterExpression = Expression.Parameter(type); var expression = AccessNullableProperty(parameterExpression, properties); return Expression.Lambda(expression, parameterExpression); } public static Expression> BuildAccessNullablePropertyLambdaExpression([NotNull] this Type type, [NotNull] string propertyPath) { return BuildAccessNullablePropertyLambdaExpression(type, propertyPath.Split(DefaultPathSeparator)); } public static Expression> BuildAccessNullablePropertyLambdaExpression([NotNull] this Type type, [NotNull] string[] properties) { ArgumentNotNull(type, nameof(type)); ArgumentNotEmpty(properties, nameof(properties)); var parameterExpression = Expression.Parameter(type); var expression = AccessNullableProperty(parameterExpression, properties); return Expression.Lambda>(expression, parameterExpression); } public static Expression> BuildAccessNullablePropertyLambdaExpression( [NotNull] this ParameterExpression parameterExpression, [NotNull] string[] properties) { ArgumentNotNull(parameterExpression, nameof(parameterExpression)); var expression = AccessNullableProperty(parameterExpression, properties); return Expression.Lambda>(expression, parameterExpression); } #endregion Build Nullable delegate #region Extension method public static Delegate ToDelegate([NotNull] this Expression expression) { ArgumentNotNull(expression, nameof(expression)); return ToLambdaExpression(expression).Compile(); } public static LambdaExpression ToLambdaExpression([NotNull] this Expression expression) { ArgumentNotNull(expression, nameof(expression)); return Expression.Lambda(expression, GetRootParameterExpression(expression)); } public static Expression> ToFuncExpression([NotNull] this Expression expression) { ArgumentNotNull(expression, nameof(expression)); return Expression.Lambda>(expression, GetRootParameterExpression(expression)); } #endregion Extension method #region DefaultValue handle public static Expression AccessPropertyDefaultIfNull([NotNull] this Type type, [NotNull] string properties, [NotNull] string separator, TValue defaultValue) { return AccessPropertyDefaultIfNull(type, properties.Split(separator), defaultValue); } public static Expression AccessPropertyDefaultIfNull([NotNull] this Type type, [NotNull] string properties, TValue defaultValue) { return AccessPropertyDefaultIfNull(type, properties.Split(DefaultPathSeparator), defaultValue); } public static Expression AccessPropertyDefaultIfNull([NotNull] this Type type, [NotNull] string[] properties, TValue defaultValue) { ArgumentNotNull(type, nameof(type)); ArgumentNotEmpty(properties, nameof(properties)); var valueType = typeof(TValue); var propertyExp = AccessNullableProperty(type, properties); // will get Nullable or class if (propertyExp.Type.IsValueType) { // Nullable Value Type var defaultValueUnderlyingType = Nullable.GetUnderlyingType(valueType); var defaultValueTypeIsNullable = defaultValueUnderlyingType != null; if (defaultValueTypeIsNullable) { var propertyUnderlyingType = Nullable.GetUnderlyingType(propertyExp.Type); if (propertyUnderlyingType != defaultValueUnderlyingType) { throw new InvalidOperationException($"default value type doesn't match the property type: property type '{propertyUnderlyingType?.Name}', default value type '{defaultValueUnderlyingType?.Name}'"); } } if (propertyExp is UnaryExpression ue) { var test = Expression.IsTrue(Expression.Property(ue, Nullable_HasValue)); Expression trueResult = defaultValueTypeIsNullable ? ue : Expression.Property(ue, Nullable_Value); var falseResult = Expression.Constant(defaultValue, valueType); return Expression.Condition(test, trueResult, falseResult); } else if (propertyExp is ConditionalExpression ce) { var test = Expression.IsTrue(Expression.Property(ce, Nullable_HasValue)); var trueResult = defaultValueTypeIsNullable ? ce.IfTrue : Expression.Property(ce.IfTrue, Nullable_Value); var falseResult = defaultValueTypeIsNullable ? ce.IfFalse : Expression.Constant(defaultValue, valueType); return Expression.Condition(test, trueResult, falseResult); } else if (propertyExp is MemberExpression me) { var test = Expression.IsTrue(Expression.Property(me, Nullable_HasValue)); Expression trueResult = defaultValueTypeIsNullable ? me : Expression.Property(me, Nullable_Value); var falseResult = Expression.Constant(defaultValue, valueType); return Expression.Condition(test, trueResult, falseResult); } else { throw new NotImplementedException($"Unexpected expression type {propertyExp.GetType().Name}"); } } else { // Class var defaultValueType = typeof(TValue); var propertyType = propertyExp.Type; if (defaultValueType != propertyType) { throw new InvalidOperationException($"default value type doesn't match the property type: property type '{propertyType?.Name}', default value type '{defaultValueType.Name}'"); } var test = Expression.NotEqual(propertyExp, Expression.Constant(null, propertyType)); var trueResult = propertyExp; var falseResult = Expression.Constant(defaultValue, defaultValueType); return Expression.Condition(test, trueResult, falseResult); } } #endregion DefaultValue handle #region Access nullable property public static Expression AccessNullableProperty([NotNull] this Type type, [NotNull] string propertyPath) { return AccessNullableProperty(type, propertyPath.Split(DefaultPathSeparator)); } public static Expression AccessNullableProperty([NotNull] this Type type, [NotNull] string propertyPath, [NotNull] string separator) { return AccessNullableProperty(type, propertyPath.Split(separator)); } public static Expression AccessNullableProperty([NotNull] this Type type, [NotNull] string[] properties) { ArgumentNotNull(type, nameof(type)); ArgumentNotEmpty(properties, nameof(properties)); var paramExp = Expression.Parameter(type); return AccessNullableProperty(paramExp, properties); } public static Expression AccessNullableProperty([NotNull] this Expression parameterExpression, [NotNull] string propertyPath) { return AccessNullableProperty(parameterExpression, propertyPath.Split(DefaultPathSeparator)); } public static Expression AccessNullableProperty([NotNull] this Expression parameterExpression, [NotNull] string propertyPath, [NotNull] string separator) { return AccessNullableProperty(parameterExpression, propertyPath.Split(separator)); } public static Expression AccessNullableProperty([NotNull] this Expression parameterExpression, [NotNull] string[] properties) { ArgumentNotNull(parameterExpression, nameof(parameterExpression)); ArgumentNotEmpty(properties, nameof(properties)); Expression access = parameterExpression; foreach (var property in properties) { var indexAccess = ParseIndexAccess(property); access = indexAccess.HasValue ? AccessIndex(access, indexAccess.Value.propertyName, indexAccess.Value.indexes) : AccessNext(access, property); } static Expression AccessIndex(Expression member, string propertyName, Expression[] indexes) { switch (propertyName) { case {Length: 0}: { foreach (var index in indexes) { member = member.IndexableGetNullableItem(index); } return member; } default: { member = AccessNext(member, propertyName); foreach (var index in indexes) { member = member.IndexableGetNullableItem(index); } return member; } } } static Expression AccessNext(Expression member, string property) { if (member.Type.IsValueType) { if (Nullable.GetUnderlyingType(member.Type) == null) { // Value Type return member.ValueTypeGetProperty(property); } else { // Nullable Value Type return member.NullableTypeGetPropOrNull(property); } } else { // Class return member.ClassGetPropertyOrNull(property); } } return TryConvertToNullable(access); } #endregion Access Nullable property #region Access not null property public static Expression AccessProperty([NotNull] this Type type, [NotNull] string propertyPath) { return AccessProperty(type, propertyPath.Split(DefaultPathSeparator)); } public static Expression AccessProperty([NotNull] this Type type, [NotNull] string propertyPath, [NotNull] string separator) { return AccessProperty(type, propertyPath.Split(separator)); } public static Expression AccessProperty([NotNull] this Type type, [NotNull] string[] properties) { ArgumentNotNull(type, nameof(type)); ArgumentNotEmpty(properties, nameof(properties)); var paramExp = Expression.Parameter(type); return AccessProperty(paramExp, properties); } public static Expression AccessProperty([NotNull] this Expression parameterExpression, [NotNull] string propertyPath) { return AccessProperty(parameterExpression, propertyPath.Split(DefaultPathSeparator)); } public static Expression AccessProperty([NotNull] this Expression parameterExpression, [NotNull] string propertyPath, [NotNull] string separator) { return AccessProperty(parameterExpression, propertyPath.Split(separator)); } public static Expression AccessProperty(this Expression parameterExpression, string[] properties) { ArgumentNotNull(parameterExpression, nameof(parameterExpression)); ArgumentNotEmpty(properties, nameof(properties)); Expression access = parameterExpression; foreach (var property in properties) { var indexAccess = ParseIndexAccess(property); if (!indexAccess.HasValue) { access = AccessNext(access, property); } else { access = AccessIndex(access, indexAccess.Value.propertyName, indexAccess.Value.indexes); } } static Expression AccessIndex(Expression member, string propertyName, Expression[] indexes) { switch (propertyName) { case {Length: 0}: { return indexes.Aggregate(member, (current, index) => current.IndexableGetItem(index)); } default: { member = AccessNext(member, propertyName); return indexes.Aggregate(member, (current, index) => current.IndexableGetItem(index)); } } } static Expression AccessNext(Expression member, string property) { // Not index access if (member.Type.IsValueType) { return Nullable.GetUnderlyingType(member.Type) == null ? member.ValueTypeGetProperty(property) // Not Nullable : member.NullableTypeGetProperty(property); // Is Nullable } else { // Class return member.ClassGetProperty(property); } } return access; } #endregion Access Not Null Property // // Branch // 1. class : C? // 2. not null value type : V // 3. nullable value type : N? // // C!.Prop // C?.Prop // // V.Prop // // N!.Value // N!.Value.Prop // N?.Value // #region Property Access /// /// C.Prop /// /// /// /// private static MemberExpression ClassGetProperty(this Expression expression, string property) { IsClass(expression); if (!expression.Type.IsClass) { throw new InvalidOperationException($"{nameof(expression)} {expression.Type.Name} must be class"); } var exp = Expression.Property(expression, property); // C.Prop return exp; } /// /// C?.Prop /// /// /// /// private static ConditionalExpression ClassGetPropertyOrNull([NotNull] this Expression expression, string property) { ArgumentNotNull(expression, nameof(expression)); IsClass(expression); var test = Expression.NotEqual(expression, Expression.Constant(null, expression.Type)); // E: C == null var propExp = Expression.Property(expression, property); // C.Prop var propIsValueType = propExp.Type.IsValueType && Nullable.GetUnderlyingType(propExp.Type) == null; Expression trueResult = propIsValueType ? Expression.Convert(propExp, typeof(Nullable<>).MakeGenericType(propExp.Type)) // T: Prop is VT: (Nullable)C.Prop : propExp; // T: Prop is class: C.Prop var falseResult = Expression.Constant(null, trueResult.Type); // F: null var exp = Expression.Condition(test, trueResult, falseResult); // E ? T : F; return exp; } /// /// V.Prop /// /// /// /// private static MemberExpression ValueTypeGetProperty([NotNull] this Expression expression, string property) { ArgumentNotNull(expression, nameof(expression)); IsValueType(expression); var exp = Expression.Property(expression, property); // V.Prop return exp; } /// /// NV?.Value /// /// /// private static ConditionalExpression NullableTypeGetValueOrNull([NotNull] this Expression expression) { ArgumentNotNull(expression, nameof(expression)); IsNullableTypeOrThrow(expression); var test = Expression.IsTrue(Expression.Property(expression, Nullable_HasValue)); // E: NV.HasValue == true var trueResult = Expression.Convert(Expression.Property(expression, Nullable_Value), expression.Type); // T: (Nullable)NV.Value var falseResult = Expression.Constant(null, expression.Type); // F: (Nullable)null var exp = Expression.Condition(test, trueResult, falseResult); // E ? T : F return exp; } /// /// NV!.Value, maybe InvalidOperationException for no value /// /// /// private static MemberExpression NullableTypeGetValue([NotNull] this Expression expression) { ArgumentNotNull(expression, nameof(expression)); IsNullableTypeOrThrow(expression); var exp = Expression.Property(expression, nameof(Nullable.Value)); // NV!.Value return exp; } /// /// NV?.Value.Prop /// /// /// /// private static ConditionalExpression NullableTypeGetPropOrNull([NotNull] this Expression expression, string property) { ArgumentNotNull(expression, nameof(expression)); IsNullableTypeOrThrow(expression); var valueExp = NullableTypeGetValueOrNull(expression); // NV?.Value var test = Expression.NotEqual(valueExp, Expression.Constant(null, valueExp.Type)); // E: NV?.Value != null var propExp = NullableTypeGetProperty(valueExp.IfTrue, property); // Expression.Property(Expression.Property(valueExp.IfTrue, VTValue), property); // NV!.Value.Prop var propIsValueType = propExp.Type.IsValueType && Nullable.GetUnderlyingType(propExp.Type) == null; Expression trueResult = propIsValueType ? Expression.Convert(propExp, typeof(Nullable<>).MakeGenericType(propExp.Type)) // T: Prop is VT: (Nullable)NV!.Value.Prop : propExp; // T: Prop is class: NV!.Value.Prop var falseResult = Expression.Constant(null, trueResult.Type); // F: (Nullable)null var exp = Expression.Condition(test, trueResult, falseResult); // NV?.Value != null ? NV!.Value.Prop : null return exp; } /// /// NV!.Value.Prop, maybe InvalidOperationException for no value /// /// /// /// private static MemberExpression NullableTypeGetProperty([NotNull] this Expression expression, string property) { ArgumentNotNull(expression, nameof(expression)); IsNullableTypeOrThrow(expression); var nvValue = expression.NullableTypeGetValue(); // NV!.Value var exp = Expression.Property(nvValue, property); // NV!.Value.Prop return exp; } private static Expression IndexableGetItem([NotNull] this Expression expression, Expression index) { var getItemMethod = expression.Type.GetMethod(GetItemMethodName, BindingFlags.Public | BindingFlags.Instance); if (getItemMethod != null) { var getItemMethodCall = Expression.Call(expression, getItemMethod, index); return getItemMethodCall; } else if (expression.Type.IsArray) { var indexAccess = Expression.ArrayIndex(expression, index); return indexAccess; } throw new InvalidOperationException($"Not supported type '{expression.Type.Name}' for index access"); } private static Expression IndexableGetNullableItem([NotNull] this Expression expression, [NotNull] Expression index) { // Array if (expression.Type.IsArray) { if (index.Type != typeof(int)) { throw new InvalidOperationException($"Array must be indexed by 'int', but get '{expression.Type.Name}'"); } var lengthAccess = Expression.ArrayLength(expression); var test = Expression.AndAlso(Expression.LessThan(index, lengthAccess), Expression.GreaterThanOrEqual(index, Expression.Constant(0, typeof(int)))); var trueResult = Expression.ArrayIndex(expression, index); var falseResult = Expression.Constant(null, trueResult.Type); return Expression.Condition(test, trueResult, falseResult); } // Dictionary<,> like, types that have the 'ContainsKey' method and the 'get_Item' method. { var getItemMethod = expression.Type.GetMethod(GetItemMethodName, BindingFlags.Public | BindingFlags.Instance); var containsKeyMethod = expression.Type.GetMethod(nameof(IDictionary.ContainsKey), BindingFlags.Public | BindingFlags.Instance); if (getItemMethod != null && containsKeyMethod != null) { var containsKeyCall = Expression.Call(expression, containsKeyMethod, index); var test = Expression.IsTrue(containsKeyCall); var trueResult = Expression.Call(expression, getItemMethod, index); var falseResult = Expression.Constant(null, trueResult.Type); return Expression.Condition(test, trueResult, falseResult); } } // Lisk like, types that have the 'get_Item' method and the 'Count' property. { var getItemMethod = expression.Type.GetMethod(GetItemMethodName, BindingFlags.Public | BindingFlags.Instance); var countProperty = expression.Type.GetProperty(CountPropertyName, BindingFlags.Public | BindingFlags.Instance); if (getItemMethod != null && countProperty != null) { if (index.Type != typeof(int)) { throw new InvalidOperationException($"{expression.Type.Name} must be indexable by 'int', but get '{index.Type.Name}'"); } var countAccess = Expression.Property(expression, countProperty); var test = Expression.AndAlso(Expression.LessThan(index, countAccess), Expression.GreaterThanOrEqual(index, Expression.Constant(0, typeof(int)))); var trueResult = Expression.Call(expression, getItemMethod, index); var falseResult = Expression.Constant(null, trueResult.Type); return Expression.Condition(test, trueResult, falseResult); } } throw new InvalidOperationException($"Not supported type '{expression.Type.Name}' for index accessing"); } #endregion Property Access #region Type Validate /// /// Check if expression.Type is class, otherwise throw and exception /// /// /// /// private static void IsClass([NotNull] Expression expression) { ArgumentNotNull(expression, nameof(expression)); if (!expression.Type.IsClass) { throw new InvalidOperationException($"{nameof(expression)} {expression.Type.Name} must be class"); } } /// /// Check if expression.Type is ValueType and not Nullable<T>, otherwise throw and exception /// /// /// /// private static void IsValueType([NotNull] Expression expression) { ArgumentNotNull(expression, nameof(expression)); if (expression.Type.IsValueType && Nullable.GetUnderlyingType(expression.Type) != null) { throw new InvalidOperationException($"{nameof(expression)} {expression.Type.Name} must be value type"); } } /// /// Check if expression.Type is Nullable<T>, otherwise throw and exception /// /// /// /// private static void IsNullableTypeOrThrow([NotNull] Expression expression) { ArgumentNotNull(expression, nameof(expression)); if (Nullable.GetUnderlyingType(expression.Type) == null) { throw new InvalidOperationException($"{nameof(expression)} {expression.Type.Name} must be typeof Nullable"); } } #endregion Type Validate #region Utils private static void ArgumentNotNull(in T arg, string argName) where T : class { if (arg == default) { throw new ArgumentNullException(argName); } } private static void ArgumentNotEmpty(in T[] arg, string argName) where T : class { if (arg == default || arg is {Length: 0}) { throw new ArgumentException("Value cannot be an empty collection or null.", argName); } } /// /// expression should be like: ParameterExpression->MemberExpression1->MemberExpression2... , /// if the root for 'expression' is not ParameterExpression, this will return null. /// /// /// private static ParameterExpression GetRootParameterExpression([NotNull] Expression expression) { return expression switch { MemberExpression {Expression: { }} memberExp => GetRootParameterExpression(memberExp.Expression), ConditionalExpression conditionalExp => GetRootParameterExpression(conditionalExp.IfTrue), UnaryExpression unaryExp => GetRootParameterExpression(unaryExp.Operand), ParameterExpression paramExp => paramExp, _ => throw new NullReferenceException() }; } /// /// Try convert Expression type to Nullable type, only Non-Nullable ValueType can be converted /// /// /// private static Expression TryConvertToNullable([NotNull] Expression expression) { if (expression.Type.IsValueType && Nullable.GetUnderlyingType(expression.Type) == null) { return Expression.Convert(expression, typeof(Nullable<>).MakeGenericType(expression.Type)); } return expression; } /// /// Check if property string has index operation and parse to Expression /// /// /// /// private static (string propertyName, Expression[] indexes)? ParseIndexAccess(string property) { const string IndexAccessErrorTemplate = "Invalid index property: {0}, index must be like 'prop[key]' or 'prop[key][key]...'"; if (!property.EndsWith(']')) { return null; } property = property.Replace(' ', '\0'); var sp = property.AsSpan(); var begin = 0; var end = sp.IndexOf('['); if (end == -1) { throw new InvalidOperationException(string.Format(IndexAccessErrorTemplate, property)); } var propertyName = sp.Slice(begin, end).ToString(); // from 0 to index of first '[' treat as property name sp = sp.Slice(end); var leftCount = 0; var rightCount = 0; for (int i = 0; i < sp.Length; i++) { if (sp[i] == '[') { leftCount++; } else if (sp[i] == ']') { rightCount++; } } if (leftCount != rightCount) { throw new InvalidOperationException(string.Format(IndexAccessErrorTemplate, property)); } var indexes = new Expression[leftCount]; var count = 0; while (sp.Length != 0) { var indexBegin = sp.IndexOf('['); var indexEnd = sp.IndexOf(']'); if (indexBegin == -1 || indexEnd == -1 || indexBegin + 1 == indexEnd) // ']' not found or found "[]" which is incorrect { throw new InvalidOperationException(); } var idxSpan = sp.Slice(indexBegin + 1, indexEnd - indexBegin - 1); Expression indexExp; if (idxSpan[0] == '"' && idxSpan[^1] == '"') { // is string key var key = idxSpan.Slice(1, idxSpan.Length - 2).ToString(); indexExp = Expression.Constant(key, typeof(string)); } else { // is int key if (!int.TryParse(idxSpan, out var indexNum)) { throw new InvalidOperationException(string.Format(IndexAccessErrorTemplate, property)); } indexExp = Expression.Constant(indexNum, typeof(int)); } indexes[count] = indexExp; count++; sp = sp.Slice(indexEnd + 1); } return (propertyName, indexes); } #endregion Utils } }