mirror of
https://gitee.com/ant-design-blazor/ant-design-blazor.git
synced 2024-11-29 18:48:50 +08:00
feat(core): access object's property by path-based string (#1056)
* fix: DataIndex access maybe null object * feat: access object's property by path-based string * doc: update Table DataIndex demo * doc: correct Table/demo/custom-row-style.md file encoding Co-authored-by: James Yeung <shunjiey@hotmail.com>
This commit is contained in:
parent
2b82f3bdbd
commit
ea412b24ec
@ -3,232 +3,803 @@
|
||||
// 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<Func<TParam, TReturn>> BuildAccessPropertyLambdaExpression<TParam, TReturn>([NotNull] this Type type, [NotNull] string propertyPath)
|
||||
{
|
||||
return BuildAccessPropertyLambdaExpression<TParam, TReturn>(type, propertyPath.Split(DefaultPathSeparator));
|
||||
}
|
||||
|
||||
public static Expression<Func<TParam, TReturn>> BuildAccessPropertyLambdaExpression<TParam, TReturn>([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<Func<TParam, TReturn>>(expression, parameterExpression);
|
||||
}
|
||||
|
||||
public static Expression<Func<TParam, TReturn>> BuildAccessPropertyLambdaExpression<TParam, TReturn>(
|
||||
[NotNull] this ParameterExpression parameterExpression,
|
||||
[NotNull] string[] properties)
|
||||
{
|
||||
ArgumentNotNull(parameterExpression, nameof(parameterExpression));
|
||||
|
||||
var expression = AccessProperty(parameterExpression, properties);
|
||||
return Expression.Lambda<Func<TParam, TReturn>>(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<Func<TParam, TReturn>> BuildAccessNullablePropertyLambdaExpression<TParam, TReturn>([NotNull] this Type type, [NotNull] string propertyPath)
|
||||
{
|
||||
return BuildAccessNullablePropertyLambdaExpression<TParam, TReturn>(type, propertyPath.Split(DefaultPathSeparator));
|
||||
}
|
||||
|
||||
public static Expression<Func<TParam, TReturn>> BuildAccessNullablePropertyLambdaExpression<TParam, TReturn>([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<Func<TParam, TReturn>>(expression, parameterExpression);
|
||||
}
|
||||
|
||||
public static Expression<Func<TParam, TReturn>> BuildAccessNullablePropertyLambdaExpression<TParam, TReturn>(
|
||||
[NotNull] this ParameterExpression parameterExpression,
|
||||
[NotNull] string[] properties)
|
||||
{
|
||||
ArgumentNotNull(parameterExpression, nameof(parameterExpression));
|
||||
|
||||
var expression = AccessNullableProperty(parameterExpression, properties);
|
||||
return Expression.Lambda<Func<TParam, TReturn>>(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<Func<TItem, TProp>> ToFuncExpression<TItem, TProp>([NotNull] this Expression expression)
|
||||
{
|
||||
ArgumentNotNull(expression, nameof(expression));
|
||||
|
||||
return Expression.Lambda<Func<TItem, TProp>>(expression, GetRootParameterExpression(expression));
|
||||
}
|
||||
|
||||
#endregion Extension method
|
||||
|
||||
#region DefaultValue handle
|
||||
|
||||
public static Expression AccessPropertyDefaultIfNull<TValue>([NotNull] this Type type, [NotNull] string properties, [NotNull] string separator, TValue defaultValue)
|
||||
{
|
||||
return AccessPropertyDefaultIfNull(type, properties.Split(separator), defaultValue);
|
||||
}
|
||||
|
||||
public static Expression AccessPropertyDefaultIfNull<TValue>([NotNull] this Type type, [NotNull] string properties, TValue defaultValue)
|
||||
{
|
||||
return AccessPropertyDefaultIfNull(type, properties.Split(DefaultPathSeparator), defaultValue);
|
||||
}
|
||||
|
||||
public static Expression AccessPropertyDefaultIfNull<TValue>([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<T> 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
|
||||
|
||||
/// <summary>
|
||||
/// Build property access expression, using property path string to identify the property.
|
||||
/// C.Prop
|
||||
/// </summary>
|
||||
/// <param name="expression"></param>
|
||||
/// <param name="propertyPath"></param>
|
||||
/// <param name="separator"></param>
|
||||
/// <returns></returns>
|
||||
public static MemberExpression AccessProperty(Expression expression, string propertyPath, string separator)
|
||||
/// <param name="property"></param>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
private static MemberExpression ClassGetProperty(this Expression expression, string property)
|
||||
{
|
||||
if (expression == null)
|
||||
IsClass(expression);
|
||||
|
||||
if (!expression.Type.IsClass)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(expression));
|
||||
throw new InvalidOperationException($"{nameof(expression)} {expression.Type.Name} must be class");
|
||||
}
|
||||
|
||||
if (propertyPath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(propertyPath));
|
||||
}
|
||||
|
||||
if (separator == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(separator));
|
||||
}
|
||||
|
||||
return AccessProperty(expression, propertyPath.Split(separator));
|
||||
}
|
||||
|
||||
public static MemberExpression AccessProperty<TItem>(string propertyPath, string separator)
|
||||
{
|
||||
if (propertyPath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(propertyPath));
|
||||
}
|
||||
|
||||
if (separator == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(separator));
|
||||
}
|
||||
|
||||
return AccessProperty(Expression.Parameter(typeof(TItem)), propertyPath.Split(separator));
|
||||
var exp = Expression.Property(expression, property); // C.Prop
|
||||
return exp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build property access expression, using property path string to identify the property.
|
||||
/// C?.Prop
|
||||
/// </summary>
|
||||
/// <param name="expression"></param>
|
||||
/// <param name="separatedPropertyPath"></param>
|
||||
/// <param name="property"></param>
|
||||
/// <returns></returns>
|
||||
public static MemberExpression AccessProperty(Expression expression, string[] separatedPropertyPath)
|
||||
private static ConditionalExpression ClassGetPropertyOrNull([NotNull] this Expression expression, string property)
|
||||
{
|
||||
if (expression == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(expression));
|
||||
}
|
||||
ArgumentNotNull(expression, nameof(expression));
|
||||
|
||||
if (separatedPropertyPath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(separatedPropertyPath));
|
||||
}
|
||||
|
||||
if (separatedPropertyPath.Length == 0)
|
||||
{
|
||||
throw new ArgumentException($"{nameof(separatedPropertyPath)} should not be empty");
|
||||
}
|
||||
|
||||
return (MemberExpression)separatedPropertyPath.Aggregate(expression, Expression.Property);
|
||||
}
|
||||
|
||||
public static ConditionalExpression AccessNullableProperty(Expression expression, string[] separatedPropertyPath)
|
||||
{
|
||||
if (expression == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(expression));
|
||||
}
|
||||
|
||||
if (separatedPropertyPath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(separatedPropertyPath));
|
||||
}
|
||||
|
||||
if (separatedPropertyPath.Length == 0)
|
||||
{
|
||||
throw new ArgumentException($"{nameof(separatedPropertyPath)} should not be empty");
|
||||
}
|
||||
|
||||
return (ConditionalExpression)separatedPropertyPath.Aggregate(expression, CreateNullConditionalPropertyAccess);
|
||||
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<Prop>)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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// V.Prop
|
||||
/// </summary>
|
||||
/// <param name="itemType"></param>
|
||||
/// <param name="separatedPropertyPath"></param>
|
||||
/// <param name="expression"></param>
|
||||
/// <param name="property"></param>
|
||||
/// <returns></returns>
|
||||
public static LambdaExpression BuildNullablePropertyAccessExpression(Type itemType, string[] separatedPropertyPath)
|
||||
private static MemberExpression ValueTypeGetProperty([NotNull] this Expression expression, string property)
|
||||
{
|
||||
var expression = Expression.Parameter(itemType);
|
||||
var accessExpression = AccessNullableProperty(expression, separatedPropertyPath);
|
||||
return Expression.Lambda(accessExpression, expression);
|
||||
ArgumentNotNull(expression, nameof(expression));
|
||||
|
||||
IsValueType(expression);
|
||||
var exp = Expression.Property(expression, property); // V.Prop
|
||||
return exp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// return itemType t => t.A.B.C.D;
|
||||
/// NV?.Value
|
||||
/// </summary>
|
||||
/// <param name="itemType"></param>
|
||||
/// <param name="separatedPropertyPath"></param>
|
||||
/// <param name="expression"></param>
|
||||
/// <returns></returns>
|
||||
public static LambdaExpression BuildPropertyAccessExpression(Type itemType, string[] separatedPropertyPath)
|
||||
private static ConditionalExpression NullableTypeGetValueOrNull([NotNull] this Expression expression)
|
||||
{
|
||||
var expression = Expression.Parameter(itemType);
|
||||
var accessExpression = AccessProperty(expression, separatedPropertyPath);
|
||||
return Expression.Lambda(accessExpression, 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<T>)NV.Value
|
||||
var falseResult = Expression.Constant(null, expression.Type); // F: (Nullable<T>)null
|
||||
var exp = Expression.Condition(test, trueResult, falseResult); // E ? T : F
|
||||
return exp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// return itemType t => t.A.B.C.D;
|
||||
/// NV!.Value, maybe InvalidOperationException for no value
|
||||
/// </summary>
|
||||
/// <param name="itemType"></param>
|
||||
/// <param name="propertyPath"></param>
|
||||
/// <param name="separator"></param>
|
||||
/// <param name="expression"></param>
|
||||
/// <returns></returns>
|
||||
private static MemberExpression NullableTypeGetValue([NotNull] this Expression expression)
|
||||
{
|
||||
ArgumentNotNull(expression, nameof(expression));
|
||||
|
||||
IsNullableTypeOrThrow(expression);
|
||||
var exp = Expression.Property(expression, nameof(Nullable<bool>.Value)); // NV!.Value
|
||||
return exp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NV?.Value.Prop
|
||||
/// </summary>
|
||||
/// <param name="expression"></param>
|
||||
/// <param name="property"></param>
|
||||
/// <returns></returns>
|
||||
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<Prop>)NV!.Value.Prop
|
||||
: propExp; // T: Prop is class: NV!.Value.Prop
|
||||
var falseResult = Expression.Constant(null, trueResult.Type); // F: (Nullable<Prop>)null
|
||||
var exp = Expression.Condition(test, trueResult, falseResult); // NV?.Value != null ? NV!.Value.Prop : null
|
||||
return exp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NV!.Value.Prop, maybe InvalidOperationException for no value
|
||||
/// </summary>
|
||||
/// <param name="expression"></param>
|
||||
/// <param name="property"></param>
|
||||
/// <returns></returns>
|
||||
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<int, int>.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
|
||||
|
||||
/// <summary>
|
||||
/// Check if expression.Type is class, otherwise throw and exception
|
||||
/// </summary>
|
||||
/// <param name="expression"></param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public static LambdaExpression BuildPropertyAccessExpression(Type itemType, string propertyPath, string separator)
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
private static void IsClass([NotNull] Expression expression)
|
||||
{
|
||||
if (propertyPath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(propertyPath));
|
||||
}
|
||||
ArgumentNotNull(expression, nameof(expression));
|
||||
|
||||
return BuildPropertyAccessExpression(itemType, propertyPath.Split(separator));
|
||||
if (!expression.Type.IsClass)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(expression)} {expression.Type.Name} must be class");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// return TItem t => t.A.B.C.D; D is TProp
|
||||
/// Check if expression.Type is ValueType and not Nullable<T>, otherwise throw and exception
|
||||
/// </summary>
|
||||
/// <param name="separatedPropertyPath"></param>
|
||||
/// <typeparam name="TItem"></typeparam>
|
||||
/// <typeparam name="TProp"></typeparam>
|
||||
/// <returns></returns>
|
||||
/// <param name="expression"></param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public static Expression<Func<TItem, TProp>> BuildPropertyAccessExpression<TItem, TProp>(string[] separatedPropertyPath)
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
private static void IsValueType([NotNull] Expression expression)
|
||||
{
|
||||
if (separatedPropertyPath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(separatedPropertyPath));
|
||||
}
|
||||
ArgumentNotNull(expression, nameof(expression));
|
||||
|
||||
if (separatedPropertyPath.Length == 0)
|
||||
if (expression.Type.IsValueType && Nullable.GetUnderlyingType(expression.Type) != null)
|
||||
{
|
||||
throw new ArgumentException($"{nameof(separatedPropertyPath)} should not be empty");
|
||||
throw new InvalidOperationException($"{nameof(expression)} {expression.Type.Name} must be value type");
|
||||
}
|
||||
|
||||
var expression = Expression.Parameter(typeof(TItem));
|
||||
var accessExpression = AccessProperty(expression, separatedPropertyPath);
|
||||
return Expression.Lambda<Func<TItem, TProp>>(accessExpression, expression);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// return TItem t => t.A.B.C.D; D is TProp
|
||||
/// Check if expression.Type is Nullable<T>, otherwise throw and exception
|
||||
/// </summary>
|
||||
/// <param name="propertyPath"></param>
|
||||
/// <param name="separator"></param>
|
||||
/// <typeparam name="TItem"></typeparam>
|
||||
/// <typeparam name="TProp"></typeparam>
|
||||
/// <returns></returns>
|
||||
/// <param name="expression"></param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public static Expression<Func<TItem, TProp>> BuildPropertyAccessExpression<TItem, TProp>(string propertyPath, string separator)
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
private static void IsNullableTypeOrThrow([NotNull] Expression expression)
|
||||
{
|
||||
if (propertyPath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(propertyPath));
|
||||
}
|
||||
ArgumentNotNull(expression, nameof(expression));
|
||||
|
||||
return BuildPropertyAccessExpression<TItem, TProp>(propertyPath.Split(separator));
|
||||
if (Nullable.GetUnderlyingType(expression.Type) == null)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(expression)} {expression.Type.Name} must be typeof Nullable<T>");
|
||||
}
|
||||
}
|
||||
|
||||
public static ConditionalExpression CreateNullConditionalPropertyAccess(Expression expression, string property)
|
||||
#endregion Type Validate
|
||||
|
||||
#region Utils
|
||||
|
||||
private static void ArgumentNotNull<T>(in T arg, string argName)
|
||||
where T : class
|
||||
{
|
||||
if (expression == null)
|
||||
if (arg == default)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(expression));
|
||||
throw new ArgumentNullException(argName);
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(property))
|
||||
private static void ArgumentNotEmpty<T>(in T[] arg, string argName)
|
||||
where T : class
|
||||
{
|
||||
if (arg == default || arg is {Length: 0})
|
||||
{
|
||||
throw new ArgumentNullException(nameof(property));
|
||||
throw new ArgumentException("Value cannot be an empty collection or null.", argName);
|
||||
}
|
||||
|
||||
Expression propertyAccess = Expression.Property(expression, property);
|
||||
|
||||
var propertyType = propertyAccess.Type;
|
||||
|
||||
if (propertyType.IsValueType && Nullable.GetUnderlyingType(propertyType) == null)
|
||||
{
|
||||
propertyAccess = Expression.Convert(propertyAccess, typeof(Nullable<>).MakeGenericType(propertyType));
|
||||
}
|
||||
|
||||
var nullResult = Expression.Default(propertyAccess.Type);
|
||||
|
||||
var condition = Expression.Equal(expression, Expression.Constant(null, expression.Type));
|
||||
|
||||
return Expression.Condition(condition, nullResult, propertyAccess);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// expression should be like: ParameterExpression->MemberExpression1->MemberExpression2... ,
|
||||
/// it will return root ParameterExpression, otherwise return null.
|
||||
/// if the root for 'expression' is not ParameterExpression, this will return null.
|
||||
/// </summary>
|
||||
/// <param name="expression"></param>
|
||||
/// <returns></returns>
|
||||
public static ParameterExpression GetRootParameterExpression(Expression expression)
|
||||
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,
|
||||
_ => null
|
||||
_ => throw new NullReferenceException()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try convert Expression type to Nullable type, only Non-Nullable ValueType can be converted
|
||||
/// </summary>
|
||||
/// <param name="expression"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if property string has index operation and parse to Expression
|
||||
/// </summary>
|
||||
/// <param name="property"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -31,25 +31,25 @@ namespace AntDesign.Internal
|
||||
Func<RowData, TProp> getValue = null;
|
||||
ITableSortModel sortModel = null;
|
||||
var properties = dataIndex?.Split(".");
|
||||
if (properties is { Length: > 0 })
|
||||
if (properties is {Length: > 0})
|
||||
{
|
||||
var isNullable = propType.IsValueType && Nullable.GetUnderlyingType(propType) != null;
|
||||
var canBeNull = !propType.IsValueType || Nullable.GetUnderlyingType(propType) != null;
|
||||
var rowDataType = typeof(RowData);
|
||||
var rowData1Type = typeof(RowData<>).MakeGenericType(itemType);
|
||||
var rowDataExp = Expression.Parameter(rowDataType);
|
||||
var rowData1Exp = Expression.TypeAs(rowDataExp, rowData1Type);
|
||||
var dataMemberExp = Expression.Property(rowData1Exp, nameof(RowData<object>.Data));
|
||||
|
||||
Expression memberExp = isNullable
|
||||
? PropertyAccessHelper.AccessNullableProperty(dataMemberExp, properties)
|
||||
: PropertyAccessHelper.AccessProperty(dataMemberExp, properties);
|
||||
Expression memberExp = canBeNull
|
||||
? dataMemberExp.AccessNullableProperty(properties)
|
||||
: dataMemberExp.AccessProperty(properties);
|
||||
getValue = Expression.Lambda<Func<RowData, TProp>>(memberExp, rowDataExp).Compile();
|
||||
|
||||
if (sortable)
|
||||
{
|
||||
var propertySelector = isNullable
|
||||
? PropertyAccessHelper.BuildNullablePropertyAccessExpression(itemType, properties)
|
||||
: PropertyAccessHelper.BuildPropertyAccessExpression(itemType, properties);
|
||||
var propertySelector = canBeNull
|
||||
? itemType.BuildAccessNullablePropertyLambdaExpression(properties)
|
||||
: itemType.BuildAccessPropertyLambdaExpression(properties);
|
||||
sortModel = new DataIndexSortModel<TProp>(dataIndex, propertySelector, sorterMultiple, sort, sorterCompare);
|
||||
}
|
||||
}
|
||||
|
59
docs/path-based-property-access.en-US.md
Normal file
59
docs/path-based-property-access.en-US.md
Normal file
@ -0,0 +1,59 @@
|
||||
---
|
||||
order: 5
|
||||
title: Path-based property access
|
||||
---
|
||||
|
||||
Provides `PropertyAccessHelper` to access to an object's properties and their descendants via a property path string.
|
||||
|
||||
## Supported operations
|
||||
|
||||
### 1. Accessing descendant properties
|
||||
|
||||
Example: `"A.B.C"`.
|
||||
|
||||
### 2. Array pattern indexing, and `List-like` types that implement the `Count` property and the `get_Item(int) (i.e. this[int])` method
|
||||
|
||||
Example: `"A.B[1].C"`, `A.B[1][5].C`.
|
||||
|
||||
### 3. Dictionary pattern indexing, and `Dictionary-like` types that implement the `ContainsKey` method and the `get_Item (i.e. this[key])` method.
|
||||
|
||||
Example: `"A.B[\"test\"].C"`, `"A.B[\"test\"][3].C"`.
|
||||
|
||||
### 4. Arrays, dictionary nesting
|
||||
|
||||
Example: `"A.B[\"test\"][3].C"`,`"A.B[1][5].C"`,`"A.B[1][\"user id\"].C"`.
|
||||
|
||||
### 5. Not null and Nullable mode
|
||||
|
||||
#### 5.1. Not null mode
|
||||
|
||||
<span style="font-weight: bold;">⚠Notice: Non-null mode requires the developer to ensure that the properties on the property path are not null, and if array or dictionary pattern is included, to also ensure that the indexed object must exist, otherwise an exception will be thrown. </span>.
|
||||
|
||||
When the result data type is a value type and not Nullable (such as int), a direct access expression is generated, and if there is a Nullable type in the property path, it will be accessed directly without doing a null check.
|
||||
|
||||
For example, when accessing the property `A.B.C`, where `B` is `Nullable<MyStruct>`, an expression like `A.B!.Value.C`
|
||||
|
||||
For array pattern or dictionary pattern, it is accessed directly, e.g., when accessing the array object property `A.B[i].C`, `B.Count > i && i > 0` is not checked. When accessing the dictionary object property `A.D["my data"].C`, `D.ContainsKey("my data")` is not checked.
|
||||
|
||||
#### 5.2. Nullable mode
|
||||
|
||||
<span style="font-weight: bold;">Nullable mode does not require a guarantee that the data is not null, nor does it require a guarantee that array pattern or dictionary pattern must have a value, return null when accessing the property of a non-existent object.</span>.
|
||||
|
||||
When the result data type is a Nullable value type or class (such as int?, string), a conditional expression is generated, and if there is a Nullable or class type in the property path, it will check for non-null before accessing, and will return null when it encounters a null object.
|
||||
|
||||
For example, accessing the property `A.B.C`, where `B` is `Nullable<MyStruct>`, will generate an expression like `A.B.HasValue ? A.B.Value.C : null`.
|
||||
|
||||
For array pattern or dictionary pattern, it will check before accessing, e.g. when accessing the array object property `A.B[i].C`, it will check `B.Count > i && i > 0` , and return `null` if the check result is `false`. When accessing the dictionary object property `A.D["my data"].C`, it checks `D.ContainsKey("my data")`, and returns `null` if the check result is `false`.
|
||||
|
||||
## API
|
||||
|
||||
| Method | Description |
|
||||
| ------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| BuildAccessPropertyLambdaExpression | Create a non-nullable property access Lambda expression, call Compile() and use it |
|
||||
| BuildAccessNullablePropertyLambdaExpression | Creates a nullable property access lambda expression, call Compile() and use it |
|
||||
| AccessProperty | Build AccessNullablePropertyLambdaExpression, you need to concatenate the ToXXX method to create the final expression |
|
||||
| AccessNullableProperty | Generate a nullable property access chain expression, you need to concatenate the ToXXX method to create the final expression |
|
||||
| AccessPropertyDefaultIfNull | Generate a DefaultIfNull property expression that uses the default value passed in when the property access result is null, you need to concatenate the ToXXX methods to create the final expression |
|
||||
| ToDelegate | Create delegate method from AccessXXX method |
|
||||
| ToLambdaExpression | Create a Lambda expression from the AccessXXX method |
|
||||
| ToFuncExpression | Create the Func<,> delegate method from the AccessXXX method |
|
59
docs/path-based-property-access.zh-CN.md
Normal file
59
docs/path-based-property-access.zh-CN.md
Normal file
@ -0,0 +1,59 @@
|
||||
---
|
||||
order: 5
|
||||
title: 路径式属性访问
|
||||
---
|
||||
|
||||
提供 `PropertyAccessHelper` 以实现通过属性路径字符串来访问对象的属性及其后代属性。
|
||||
|
||||
## 支持的操作
|
||||
|
||||
### 1. 访问后代属性
|
||||
|
||||
例:`"A.B.C"`。
|
||||
|
||||
### 2. 数组模式索引,以及实现了 `Count` 属性和 `get_Item(int) (即 this[int]` 方法的 `类似 List 的` 类型
|
||||
|
||||
例:`"A.B[1].C"`。
|
||||
|
||||
### 3. 字典模式索引,以及实现了 `ContainsKey` 方法和 `get_Item (即 this[key])` 方法的 `类似 Dictionary 的` 类型。
|
||||
|
||||
例:`"A.B[\"test\"].C"`。
|
||||
|
||||
### 4. 数组、字典嵌套
|
||||
|
||||
例:`"A.B[\"test\"][3].C"`,`"A.B[1][5].C"`,`"A.B[1][\"user id\"].C"`。
|
||||
|
||||
### 5. 非空模式和可空模式
|
||||
|
||||
#### 5.1. 非空模式
|
||||
|
||||
<span style="font-weight: bold;">⚠注意:非空模式下需要开发者保证属性路径上的属性不为null,如果包含数组模式或字典模式,还需要保证索引对象必须存在,否则访问不存在的对象的属性时会抛出异常。</span>
|
||||
|
||||
当结果数据类型是值类型且不是Nullable时(如int),会生成直接访问表达式,如果属性路径中存在Nullable类型,会不做null检查直接访问。
|
||||
|
||||
如:访问属性`A.B.C` 时,其中 `B` 是 `Nullable<MyStruct>`,会生成类似 `A.B!.Value.C` 的表达式。
|
||||
|
||||
对于数组或字典,会直接访问,如:访问数组对象属性 `A.B[i].C` 时,不会检查 `B.Count > i && i > 0` 。访问字典对象属性 `A.D["my data"].C` 时,不会检查 `D.ContainsKey("my data")`。
|
||||
|
||||
#### 5.2. 可空模式
|
||||
|
||||
<span style="font-weight: bold;">可空模式下不需要保证数据不为null,也不用保证数组模式或字典模式必定有值,访问到不存在对象的属性时会返回null。</span>
|
||||
|
||||
当结果数据类型是Nullable值类型或class时(如int?, string),会生成条件表达式,如果属性路径中存在Nullable或class类型,会先检查非null再访问,遇到null对象会返回null。
|
||||
|
||||
如:访问属性 `A.B.C` 时,其中 `B` 是 `Nullable<MyStruct>`,会生成类似 `A.B.HasValue ? A.B.Value.C : null` 的表达式。
|
||||
|
||||
对于数组或字典,会先检查再访问,如:访问数组对象属性 `A.B[i].C` 时,会检查 `B.Count > i && i > 0` , 结果是 `false` 时返回 `null` 。访问字典对象属性 `A.D["my data"].C` 时,会检查 `D.ContainsKey("my data")`, 结果是 `false` 时返回 `null` 。
|
||||
|
||||
## API
|
||||
|
||||
| 方法 | 说明 |
|
||||
| ------------------------------------------- | :-------------------------------------------------------------------------------------- |
|
||||
| BuildAccessPropertyLambdaExpression | 创建非空属性访问Lambda表达式, 调用Compile()后即可使用 |
|
||||
| BuildAccessNullablePropertyLambdaExpression | 创建可空属性访问Lambda表达式, 调用Compile()后即可使用 |
|
||||
| AccessProperty | 生成非空属性访问链表达式, 需要串联ToXXX方法创建最终表达式 |
|
||||
| AccessNullableProperty | 生成可空属性访问链表达式,需要串联ToXXX方法创建最终表达式 |
|
||||
| AccessPropertyDefaultIfNull | 生成DefaultIfNull属性表达式,当属性访问结果是null时,使用传入的默认值,需要串联ToXXX方法创建最终表达式 |
|
||||
| ToDelegate | 从AccessXXX方法创建委托方法 |
|
||||
| ToLambdaExpression | 从AccessXXX方法创建Lambda表达式 |
|
||||
| ToFuncExpression | 从AccessXXX方法创建Func<,>委托方法 |
|
@ -2,9 +2,9 @@
|
||||
<Column @bind-Field="@context.Id" Sortable/>
|
||||
<Column Title="DayOfWeek" @bind-Field="@context.DayOfWeek" Sortable/>
|
||||
<Column Title="DayOfWeek DataIndex" TData="int" DataIndex="DayOfWeek" Sortable SorterCompare="@((v1, v2) => v1 - v2)"/>
|
||||
<Column Title="N1.N12.N2A DataIndex" TData="string" DataIndex="N1.N12.N2A" Sortable SorterCompare="@((v1, v2) => string.CompareOrdinal(v1, v2))"/>
|
||||
<Column Title="N1.N12.N2A DataIndex" TData="string" DataIndex=@("N1.N12[\"test\"].N2A") Sortable SorterCompare="@((v1, v2) => string.CompareOrdinal(v1, v2))"/>
|
||||
<Column Title="N1.N1A DataIndex Nullable" TData="int?" DataIndex="N1.N1A" Sortable SorterCompare="CompareNullableInt"/>
|
||||
<Column Title="Time" TData="DateTime" DataIndex="N1.N12.Time" Format="yyyy-MM-dd hh:mm:ss" Sortable SorterCompare="@((t1, t2) => t1.CompareTo(t2))"></Column>
|
||||
<Column Title="Time" TData="DateTime?" DataIndex=@(@"N1.N12[""test""].Time") Format="yyyy-MM-dd hh:mm:ss" Sortable SorterCompare="_dateTimeCompare"></Column>
|
||||
</Table>
|
||||
|
||||
@code {
|
||||
@ -30,18 +30,30 @@
|
||||
{
|
||||
public int? N1A { get; set; }
|
||||
|
||||
public Nested2 N12 { get; set; }
|
||||
public Dictionary<string, Nested2> N12 { get; set; }
|
||||
}
|
||||
|
||||
public class Nested2
|
||||
{
|
||||
public string N2A { get; set; }
|
||||
|
||||
public DateTime Time { get; set; }
|
||||
public DateTime? Time { get; set; }
|
||||
}
|
||||
|
||||
public string[] DayName = {"None", "Monday", "Tuesday", "Wednesdays", "Thursday", "Friday", "Saturday", "Sunday"};
|
||||
|
||||
Func<DateTime?, DateTime?, int> _dateTimeCompare = (t1, t2) =>
|
||||
{
|
||||
if (t1.HasValue && t2.HasValue)
|
||||
return DateTime.Compare(t1.Value, t2.Value);
|
||||
if (t1.HasValue)
|
||||
return 1;
|
||||
if (t2.HasValue)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
@ -68,8 +80,14 @@
|
||||
N1A = (index & 1) == 0 ? index : null,
|
||||
N12 = new()
|
||||
{
|
||||
N2A = DayName[data.DayOfWeek] + " - DI",
|
||||
Time = startDate.AddSeconds(rng.Next((int)range))
|
||||
{"no used", null},
|
||||
{
|
||||
"test", new Nested2()
|
||||
{
|
||||
N2A = DayName[data.DayOfWeek] + " - DI",
|
||||
Time = startDate.AddSeconds(rng.Next((int)range))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return data;
|
||||
|
@ -2,7 +2,7 @@
|
||||
order: 22
|
||||
title:
|
||||
en-US: Custom Row Styling
|
||||
zh-CN: 修改每行的样式
|
||||
zh-CN: 修改每行的样式
|
||||
---
|
||||
## en-US
|
||||
|
||||
@ -12,4 +12,4 @@ A solution for displaying custom styling on rows.
|
||||
|
||||
## zh-CN
|
||||
|
||||
可以使用 `RowClassName` 给特定的行设置 class。也可以用 `ExpandedRowClassName` 给特定的展开行设置 class。
|
||||
可以使用 `RowClassName` 给特定的行设置 class。也可以用 `ExpandedRowClassName` 给特定的展开行设置 class。
|
@ -7,16 +7,20 @@ title:
|
||||
|
||||
## zh-CN
|
||||
|
||||
可以不使用 `@bind-Field`,而是通过设置数据类型 `TData` 和数据索引字符串 `DataIndex` 来指定要绑定的属性,可以绑定后代属性。
|
||||
可以不使用 `@bind-Field`,而是通过设置数据类型 `TData` 和数据索引字符串 `DataIndex` 来指定要绑定的属性。列数据的类型与 `TData` 的定义一致。
|
||||
|
||||
当绑定的属性是值类型并且是Nullable时, `TData` 应设为Nullable类型。如: `<Column TData="int?" DataIndex="prop1" />` 或 `<Column TData="Nullable<int>" DataIndex="prop1" />`。
|
||||
当绑定的属性是值类型并且是Nullable时,`TData` 应设为Nullable类型。如:`<Column TData="int?" DataIndex="prop1" />` 或 `<Column TData="Nullable<int>" DataIndex="prop1" />`。
|
||||
|
||||
Column使用DataIndex时, Table的OnChange参数 `QuerModel<TItem>.SortModel[].FieldName` 等于 `DataIndex`。
|
||||
Column使用DataIndex时,Table的OnChange参数 `QuerModel<TItem>.SortModel[].FieldName` 等于 `DataIndex`。
|
||||
|
||||
DataIndex支持的访问操作类型,详见[路径式属性访问](/zh-CN/docs/path-based-property-access)
|
||||
|
||||
## en-US
|
||||
|
||||
Instead of using `@bind-Field`, you can specify the property to be bound by setting the data type `TData` and the data index string `DataIndex`, which can bind descendant properties.
|
||||
Instead of using `@bind-Field`, you can specify the property to be bound by setting the data type `TData` and the data index string `DataIndex`, which can bind descendant properties. The type of the column data is the same as the definition of `TData`.
|
||||
|
||||
When the bound property is ValueType and is Nullable, `TData` should be set to Nullable type. For example: `<Column TData="int?" DataIndex="prop1" />` or `<Column TData="Nullable<int>" DataIndex="prop1" />`.
|
||||
|
||||
When Column uses DataIndex, Table's OnChange event parameter `QuerModel<TItem>.SortModel[].FieldName` is equal to `DataIndex`.
|
||||
When Column uses DataIndex, Table's OnChange event parameter `QuerModel<TItem>.SortModel[].FieldName` is equal to `DataIndex`.
|
||||
|
||||
See [path-based-property-access](/en-US/docs/path-based-property-access) for details of the access modes supported by DataIndex.
|
||||
|
271
tests/Core/PropertyAccessHelperTest.cs
Normal file
271
tests/Core/PropertyAccessHelperTest.cs
Normal file
@ -0,0 +1,271 @@
|
||||
// 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 AntDesign.core.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace AntDesign.Tests.Core
|
||||
{
|
||||
public class PropertyAccessHelperTest
|
||||
{
|
||||
public class CA
|
||||
{
|
||||
public int A1 { get; set; }
|
||||
|
||||
public string A2 { get; set; }
|
||||
|
||||
public CB A3 { get; set; }
|
||||
|
||||
public SA A4 { get; set; }
|
||||
|
||||
public SA? A5 { get; set; }
|
||||
|
||||
public List<SA?[]> A6 { get; set; }
|
||||
}
|
||||
|
||||
public class CB
|
||||
{
|
||||
public int B1 { get; set; }
|
||||
}
|
||||
|
||||
public struct SA
|
||||
{
|
||||
public int A1 { get; set; }
|
||||
|
||||
public string A2 { get; set; }
|
||||
|
||||
public CB A3 { get; set; }
|
||||
|
||||
public SB A4 { get; set; }
|
||||
|
||||
public SB? A5 { get; set; }
|
||||
}
|
||||
|
||||
public struct SB
|
||||
{
|
||||
public string B1 { get; set; }
|
||||
|
||||
public int B2 { get; set; }
|
||||
|
||||
public int? B3 { get; set; }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestIndexAccess()
|
||||
{
|
||||
var cr = new CA[]
|
||||
{
|
||||
new CA() { },
|
||||
new CA()
|
||||
{
|
||||
A1 = 111,
|
||||
A4 = new SA()
|
||||
{
|
||||
A1 = 133,
|
||||
A5 = new SB()
|
||||
{
|
||||
B1 = "CA A5",
|
||||
B2 = 123456
|
||||
}
|
||||
},
|
||||
A6 = new List<SA?[]>
|
||||
{
|
||||
new SA?[3],
|
||||
new SA?[2],
|
||||
new SA?[2]
|
||||
{
|
||||
new SA(),
|
||||
new SA()
|
||||
{
|
||||
A1 = 567,
|
||||
A2 = "222",
|
||||
A5 = new SB() {B1 = "test index"}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
var cl = new List<CA>(cr);
|
||||
|
||||
var arrayFuncExp = typeof(CA[]).BuildAccessNullablePropertyLambdaExpression<CA[], string>("[1].A6[2][1].A5.B1");
|
||||
var listFuncExp = typeof(List<CA>).BuildAccessNullablePropertyLambdaExpression<List<CA>, string>("[1].A6[2][1].A5.B1");
|
||||
var arrayFunc = arrayFuncExp.Compile();
|
||||
var listFunc = listFuncExp.Compile();
|
||||
var arr = arrayFunc.Invoke(cr);
|
||||
var lst = listFunc.Invoke(cl);
|
||||
Assert.Equal(arr, cr[1].A6[2][1].Value.A5.Value.B1);
|
||||
Assert.Equal(lst, cl[1].A6[2][1].Value.A5.Value.B1);
|
||||
|
||||
{
|
||||
var dict = new Dictionary<string, Dictionary<int, string>>();
|
||||
dict["DSa"] = new Dictionary<int, string>()
|
||||
{
|
||||
{1, "B1 Value"},
|
||||
{3, "B2 Value"},
|
||||
};
|
||||
var exp = dict.GetType().BuildAccessPropertyLambdaExpression<Dictionary<string, Dictionary<int, string>>, string>("[\"DSa\"][3]").Compile();
|
||||
var result = exp.Invoke(dict);
|
||||
Assert.Equal(dict["DSa"][3], result);
|
||||
}
|
||||
{
|
||||
var dict = new Dictionary<string, Dictionary<string, string>>();
|
||||
dict["B"] = new Dictionary<string, string>()
|
||||
{
|
||||
{"B1", "B1 Value"},
|
||||
{"B2", "B2 Value"},
|
||||
};
|
||||
|
||||
var exp = dict.GetType().BuildAccessNullablePropertyLambdaExpression<Dictionary<string, Dictionary<string, string>>, string>("[\"B\"][\"B1\"]").Compile();
|
||||
var result = exp.Invoke(dict);
|
||||
Assert.Equal(dict["B"]["B1"], result);
|
||||
}
|
||||
{
|
||||
var dict = new Dictionary<string, Dictionary<string, string>>();
|
||||
dict["A"] = new Dictionary<string, string>()
|
||||
{
|
||||
{"A1", null},
|
||||
};
|
||||
|
||||
var exp = dict.GetType().BuildAccessNullablePropertyLambdaExpression<Dictionary<string, Dictionary<string, string>>, string>("[\"A\"][\"B1\"]").Compile();
|
||||
var result = exp.Invoke(dict);
|
||||
Assert.Null(result);
|
||||
|
||||
var exp2 = dict.GetType().BuildAccessNullablePropertyLambdaExpression<Dictionary<string, Dictionary<string, string>>, string>("[\"A\"][\"B1\"]").Compile();
|
||||
var result2 = exp2.Invoke(dict);
|
||||
Assert.Null(result2);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestDefaultValue()
|
||||
{
|
||||
typeof(CA).TestDefaultValue("A3.B1", 12);
|
||||
typeof(CA).TestDefaultValue("A4.A2", "asdd");
|
||||
typeof(CA).TestDefaultValue("A4.A3", (CB?)null);
|
||||
typeof(CA).TestDefaultValue("A4.A3.B1", 145);
|
||||
typeof(CA).TestDefaultValue("A4.A4.B1", "sfasf");
|
||||
typeof(CA).TestDefaultValue("A4.A4.B3", 5967);
|
||||
typeof(CA).TestDefaultValue("A4.A5.B1", "sadas");
|
||||
typeof(CA).TestDefaultValue("A4.A5.B2", 521);
|
||||
|
||||
typeof(SA).TestDefaultValue("A2", "t232");
|
||||
typeof(SA).TestDefaultValue("A3", new CB() {B1 = 6666});
|
||||
typeof(SA).TestDefaultValue("A3.B1", 5678);
|
||||
|
||||
typeof(SA).TestDefaultValue("A4.B1", "BBBB1");
|
||||
typeof(SA).TestDefaultValue("A5", (SB?)null);
|
||||
typeof(SA).TestDefaultValue(
|
||||
"A5",
|
||||
new SB()
|
||||
{
|
||||
B1 = "A5555",
|
||||
B2 = 5555
|
||||
});
|
||||
typeof(SA).TestDefaultValue("A5.B1", "A5B1...");
|
||||
typeof(SA).TestDefaultValue("A5.B2", 1110);
|
||||
|
||||
typeof(SA?).TestDefaultValue("A1", 5123);
|
||||
typeof(SA?).TestDefaultValue("A2", "?A1");
|
||||
typeof(SA?).TestDefaultValue("A3", (CB?)null);
|
||||
typeof(SA?).TestDefaultValue("A3.B1", -123123);
|
||||
typeof(SA?).TestDefaultValue(
|
||||
"A4",
|
||||
new SB()
|
||||
{
|
||||
B1 = "SA?.A4",
|
||||
B2 = -1
|
||||
});
|
||||
typeof(SA?).TestDefaultValue("A4.B1", "A4.B1111");
|
||||
typeof(SA?).TestDefaultValue("A4.B2", (int?)null);
|
||||
typeof(SA?).TestDefaultValue("A5", (SB?)null);
|
||||
typeof(SA?).TestDefaultValue("A5.B1", (string?)null);
|
||||
typeof(SA?).TestDefaultValue("A5.B2", -1266);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestNotNullPropertyPath()
|
||||
{
|
||||
var x1 = typeof(CA).AccessProperty("A3.B1").ToDelegate();
|
||||
var x2 = typeof(CA).AccessProperty("A4.A1").ToDelegate();
|
||||
var x3 = typeof(CA).AccessProperty("A4.A2").ToDelegate();
|
||||
var x4 = typeof(CA).AccessProperty("A4.A3").ToDelegate();
|
||||
var x5 = typeof(CA).AccessProperty("A4.A3.B1").ToDelegate();
|
||||
var x6 = typeof(CA).AccessProperty("A4.A4.B1").ToDelegate();
|
||||
var x7 = typeof(CA).AccessProperty("A4.A4.B2").ToDelegate();
|
||||
var x8 = typeof(CA).AccessProperty("A4.A5.B1").ToDelegate();
|
||||
var x9 = typeof(CA).AccessProperty("A4.A5.B2").ToDelegate();
|
||||
|
||||
var y1 = typeof(SA).AccessProperty("A1").ToDelegate();
|
||||
var y2 = typeof(SA).AccessProperty("A2").ToDelegate();
|
||||
var y3 = typeof(SA).AccessProperty("A3").ToDelegate();
|
||||
var y3_1 = typeof(SA).AccessProperty("A3.B1").ToDelegate();
|
||||
var y4 = typeof(SA).AccessProperty("A4").ToDelegate();
|
||||
var y4_1 = typeof(SA).AccessProperty("A4.B1").ToDelegate();
|
||||
var y4_2 = typeof(SA).AccessProperty("A4.B2").ToDelegate();
|
||||
var y5 = typeof(SA).AccessProperty("A5").ToDelegate();
|
||||
var y6 = typeof(SA).AccessProperty("A5.B1").ToDelegate();
|
||||
var y7 = typeof(SA).AccessProperty("A5.B2").ToDelegate();
|
||||
|
||||
var z1 = typeof(SA?).AccessProperty("A1").ToDelegate();
|
||||
var z2 = typeof(SA?).AccessProperty("A2").ToDelegate();
|
||||
var z3 = typeof(SA?).AccessProperty("A3").ToDelegate();
|
||||
var z3_1 = typeof(SA?).AccessProperty("A3.B1").ToDelegate();
|
||||
var z4 = typeof(SA?).AccessProperty("A4").ToDelegate();
|
||||
var z4_1 = typeof(SA?).AccessProperty("A4.B1").ToDelegate();
|
||||
var z4_2 = typeof(SA?).AccessProperty("A4.B2").ToDelegate();
|
||||
var z5 = typeof(SA?).AccessProperty("A5").ToDelegate();
|
||||
var z6 = typeof(SA?).AccessProperty("A5.B1").ToDelegate();
|
||||
var z7 = typeof(SA?).AccessProperty("A5.B2").ToDelegate();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestNullablePropertyPath()
|
||||
{
|
||||
var x1 = typeof(CA).AccessNullableProperty("A3.B1").ToDelegate();
|
||||
var x2 = typeof(CA).AccessNullableProperty("A4.A1").ToDelegate();
|
||||
var x3 = typeof(CA).AccessNullableProperty("A4.A2").ToDelegate();
|
||||
var x4 = typeof(CA).AccessNullableProperty("A4.A3").ToDelegate();
|
||||
var x5 = typeof(CA).AccessNullableProperty("A4.A3.B1").ToDelegate();
|
||||
var x6 = typeof(CA).AccessNullableProperty("A4.A4.B1").ToDelegate();
|
||||
var x7 = typeof(CA).AccessNullableProperty("A4.A4.B2").ToDelegate();
|
||||
var x8 = typeof(CA).AccessNullableProperty("A4.A5.B1").ToDelegate();
|
||||
var x9 = typeof(CA).AccessNullableProperty("A4.A5.B2").ToDelegate();
|
||||
|
||||
var y1 = typeof(SA).AccessNullableProperty("A1").ToDelegate();
|
||||
var y2 = typeof(SA).AccessNullableProperty("A2").ToDelegate();
|
||||
var y3 = typeof(SA).AccessNullableProperty("A3").ToDelegate();
|
||||
var y3_1 = typeof(SA).AccessNullableProperty("A3.B1").ToDelegate();
|
||||
var y4 = typeof(SA).AccessNullableProperty("A4").ToDelegate();
|
||||
var y4_1 = typeof(SA).AccessNullableProperty("A4.B1").ToDelegate();
|
||||
var y4_2 = typeof(SA).AccessNullableProperty("A4.B2").ToDelegate();
|
||||
var y5 = typeof(SA).AccessNullableProperty("A5").ToDelegate();
|
||||
var y6 = typeof(SA).AccessNullableProperty("A5.B1").ToDelegate();
|
||||
var y7 = typeof(SA).AccessNullableProperty("A5.B2").ToDelegate();
|
||||
|
||||
var z1 = typeof(SA?).AccessNullableProperty("A1").ToDelegate();
|
||||
var z2 = typeof(SA?).AccessNullableProperty("A2").ToDelegate();
|
||||
var z3 = typeof(SA?).AccessNullableProperty("A3").ToDelegate();
|
||||
var z3_1 = typeof(SA?).AccessNullableProperty("A3.B1").ToDelegate();
|
||||
var z4 = typeof(SA?).AccessNullableProperty("A4").ToDelegate();
|
||||
var z4_1 = typeof(SA?).AccessNullableProperty("A4.B1").ToDelegate();
|
||||
var z4_2 = typeof(SA?).AccessNullableProperty("A4.B2").ToDelegate();
|
||||
var z5 = typeof(SA?).AccessNullableProperty("A5").ToDelegate();
|
||||
var z6 = typeof(SA?).AccessNullableProperty("A5.B1").ToDelegate();
|
||||
var z7 = typeof(SA?).AccessNullableProperty("A5.B2").ToDelegate();
|
||||
}
|
||||
}
|
||||
|
||||
public static class PropertyAccessHelperTestTool
|
||||
{
|
||||
public static void TestDefaultValue<TValue>(this Type type, string propertyPath, TValue value)
|
||||
{
|
||||
var func = type.AccessPropertyDefaultIfNull(propertyPath, value).ToDelegate();
|
||||
var obj = Activator.CreateInstance(type);
|
||||
TValue res = (TValue)func.DynamicInvoke(obj);
|
||||
Assert.Equal(value, res);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user