1
+ using Microsoft . UI . Xaml ;
1
2
using System ;
2
3
using System . Collections ;
3
4
using System . Linq ;
5
+ using System . Linq . Expressions ;
4
6
using System . Reflection ;
5
7
using System . Text . RegularExpressions ;
6
8
@@ -16,146 +18,108 @@ internal static partial class ObjectExtensions
16
18
private static partial Regex PropertyPathRegex ( ) ;
17
19
18
20
/// <summary>
19
- /// Gets the value of a property from an object using a sequence of property info and index pairs .
21
+ /// Creates and returns a compiled lambda expression for accessing the property path on instances, with runtime type checking and casting support .
20
22
/// </summary>
21
- /// <param name="obj ">The object from which to get the value .</param>
22
- /// <param name="pis">An array of property info and index pairs .</param>
23
- /// <returns>The value of the property, or null if the object is null .</returns>
24
- internal static object ? GetValue ( this object ? obj , ( PropertyInfo pi , object ? index ) [ ] pis )
23
+ /// <param name="dataItem ">The data item instance to use for runtime type evaluation .</param>
24
+ /// <param name="bindingPath">The binding path to access, e.g. "[0].SubPropertyArray[0].SubSubProperty" .</param>
25
+ /// <returns>A compiled function that takes an instance and returns the property value , or null if the property path is invalid .</returns>
26
+ public static Func < object , object ? > ? GetFuncCompiledPropertyPath ( this object dataItem , string bindingPath )
25
27
{
26
- foreach ( var ( pi , index ) in pis )
28
+ try
27
29
{
28
- if ( obj is null )
29
- break ;
30
-
31
- if ( pi != null )
32
- {
33
- // Use property getter, with or without index
34
- obj = index is not null ? pi . GetValue ( obj , [ index ] ) : pi . GetValue ( obj ) ;
35
- }
36
- else if ( index is int i )
37
- {
38
- // Array
39
- if ( obj is Array arr )
40
- {
41
- obj = arr . GetValue ( i ) ;
42
- }
43
- // IList
44
- else if ( obj is IList list )
45
- {
46
- obj = list [ i ] ;
47
- }
48
- else
49
- {
50
- // Not a supported indexer type
51
- return null ;
52
- }
53
- }
54
- else
55
- {
56
- // Not a supported path segment
57
- return null ;
58
- }
30
+ // Build the property access expression chain with runtime type checking
31
+ var parameterObj = Expression . Parameter ( typeof ( object ) , "obj" ) ;
32
+ var propertyAccess = BuildPropertyPathExpressionTree ( parameterObj , bindingPath , dataItem ) ;
33
+
34
+ // Compile the lambda expression
35
+ var lambda = Expression . Lambda < Func < object , object ? > > ( Expression . Convert ( propertyAccess , typeof ( object ) ) , parameterObj ) ;
36
+ var _compiledPropertyPath = lambda . Compile ( ) ;
37
+ return _compiledPropertyPath ;
38
+ }
39
+ catch
40
+ {
41
+ // If compilation fails, fall back to reflection-based approach
42
+ return null ;
59
43
}
60
-
61
- return obj ;
62
44
}
63
45
64
46
/// <summary>
65
- /// Gets the value of a property from an object using a type and a property path .
47
+ /// Builds an expression tree for accessing a property path on the given instance expression, with runtime type checking and casting support .
66
48
/// </summary>
67
- /// <param name="obj">The object from which to get the value.</param>
68
- /// <param name="type">The type of the object.</param>
69
- /// <param name="path">The property path.</param>
70
- /// <param name="pis">An array of property info and index pairs.</param>
71
- /// <returns>The value of the property, or null if the object is null.</returns>
72
- internal static object ? GetValue ( this object ? obj , Type ? type , string ? path , out ( PropertyInfo pi , object ? index ) [ ] pis )
49
+ /// <param name="parameterObj">The expression representing the instance parameter for which the binding path will be evaluated.</param>
50
+ /// <param name="bindingPath">The binding path to access.</param>
51
+ /// <param name="dataItem">The actual data item to use for runtime type evaluation, to help with any needed subclass type conversions.</param>
52
+ /// <returns>An expression that accesses the binding path from the </returns>
53
+ private static Expression BuildPropertyPathExpressionTree ( ParameterExpression parameterObj , string bindingPath , object dataItem )
73
54
{
74
- if ( obj == null || string . IsNullOrWhiteSpace ( path ) || type == null )
75
- {
76
- pis = [ ] ;
77
- return obj ;
78
- }
55
+ Expression current = parameterObj ;
79
56
80
- var matches = PropertyPathRegex ( ) . Matches ( path ) ;
81
- if ( matches . Count == 0 )
82
- {
83
- pis = [ ] ;
84
- return obj ;
85
- }
57
+ // The function uses a generic object input parameter to allow for any type of data item,
58
+ // but we need to ensure that the runtime type matches the data item type that is inputted as example to be able to find members
59
+ if ( current . Type != dataItem . GetType ( ) )
60
+ current = Expression . Convert ( current , dataItem . GetType ( ) ) ;
86
61
87
- // Pre-size the steps array to the number of matches
88
- pis = new ( PropertyInfo , object ? ) [ matches . Count ] ;
89
- int i = 0 ;
90
- object ? current = obj ;
91
- Type ? currentType = type ;
62
+ var matches = PropertyPathRegex ( ) . Matches ( bindingPath ) ;
92
63
93
64
foreach ( Match match in matches )
94
65
{
95
66
string part = match . Value ;
96
- object ? index = null ;
97
- PropertyInfo ? pi = null ;
98
67
68
+ // Indexer
99
69
if ( part . StartsWith ( '[' ) && part . EndsWith ( ']' ) )
100
70
{
101
- // Indexer: [int] or [string]
102
- string indexer = part [ 1 ..^ 1 ] ;
103
- if ( int . TryParse ( indexer , out int intIndex ) )
104
- index = intIndex ;
105
- else
106
- index = indexer ;
107
-
108
- // Try array
109
- if ( current is Array arr && index is int idx )
110
- {
111
- current = arr . GetValue ( idx ) ;
112
- pis [ i ++ ] = ( null ! , idx ) ;
113
- currentType = current ? . GetType ( ) ;
114
- continue ;
115
- }
71
+ string stringIndex = part [ 1 ..^ 1 ] ;
116
72
117
- // Try IList
118
- if ( current is IList list && index is int idx2 )
73
+ // See if this is an integer index
74
+ if ( int . TryParse ( stringIndex , out int index ) )
119
75
{
120
- current = list [ idx2 ] ;
121
- pis [ i ++ ] = ( null ! , idx2 ) ;
122
- currentType = current ? . GetType ( ) ;
123
- continue ;
76
+ if ( current . Type . IsArray )
77
+ {
78
+ current = Expression . ArrayIndex ( current , Expression . Constant ( index ) ) ;
79
+ }
80
+ else
81
+ {
82
+ // Try to find an indexer property, with an int parameter
83
+ var indexerProperty = current . Type . GetProperty ( "Item" , [ typeof ( int ) ] )
84
+ ?? throw new ArgumentException ( $ "Type '{ current . Type . Name } ' does not support integer indexing") ;
85
+ current = Expression . Property ( current , indexerProperty , Expression . Constant ( index ) ) ;
86
+ }
124
87
}
125
-
126
- // Try to find a default indexer property "Item" (e.g., this[string]);
127
- // Note that only single argument indexers of type int or string are currently support
128
- pi = currentType ? . GetProperty ( "Item" , [ index . GetType ( ) ] ) ;
129
- if ( pi != null )
88
+ else
130
89
{
131
- current = pi . GetValue ( current , [ index ] ) ;
132
- pis [ i ++ ] = ( pi , index ) ;
133
- currentType = current ? . GetType ( ) ;
134
- continue ;
90
+ // Try to find an indexer property, with an string parameter
91
+ var indexerProperty = current . Type . GetProperty ( "Item" , [ typeof ( string ) ] )
92
+ ?? throw new ArgumentException ( $ "Type ' { current . Type . Name } ' does not support string indexing" ) ;
93
+ current = Expression . Property ( current , indexerProperty , Expression . Constant ( stringIndex ) ) ;
135
94
}
136
-
137
- // Not found
138
- pis = null ! ;
139
- return null ;
140
95
}
96
+ // Simple property access
141
97
else
142
- {
143
- // Property access
144
- pi = currentType ? . GetProperty ( part , BindingFlags . Instance | BindingFlags . Public | BindingFlags . NonPublic ) ;
145
- if ( pi == null )
146
- {
147
- pis = null ! ;
148
- return null ;
149
- }
150
- current = pi . GetValue ( current ) ;
151
- pis [ i ++ ] = ( pi , null ) ;
152
- currentType = current ? . GetType ( ) ;
98
+ {
99
+ var propertyInfo = current . Type . GetProperty ( part , BindingFlags . Instance | BindingFlags . Public | BindingFlags . NonPublic )
100
+ ?? throw new ArgumentException ( $ "Property '{ part } ' not found on type '{ current . Type . Name } '") ;
101
+ current = Expression . Property ( current , propertyInfo ) ;
153
102
}
103
+
104
+ // Compile a lambda of the partial expression thus far (cast to object), to see if we need to add a cast
105
+ var lambdaTemp = Expression . Lambda < Func < object , object ? > > ( EnsureObjectCompatibleResult ( current ) , parameterObj ) ;
106
+ var funcCurrent = lambdaTemp . Compile ( ) ;
107
+ // Evaluate this compiled function, to see if the result type is more specific than the current expression type. If so, cast to it
108
+ var result = funcCurrent ( dataItem ) ;
109
+ var runtimeType = result ? . GetType ( ) ?? current . Type ;
110
+ if ( current . Type != runtimeType && current . Type . IsAssignableFrom ( runtimeType ) )
111
+ current = Expression . Convert ( current , runtimeType ) ;
154
112
}
155
113
156
- return current ;
114
+ return EnsureObjectCompatibleResult ( current ) ;
157
115
}
158
116
117
+ static Expression EnsureObjectCompatibleResult ( Expression expression ) =>
118
+ typeof ( object ) . IsAssignableFrom ( expression . Type ) && ! expression . Type . IsValueType
119
+ ? expression
120
+ : Expression . Convert ( expression , typeof ( object ) ) ;
121
+
122
+
159
123
/// <summary>
160
124
/// Determines whether the specified object is numeric.
161
125
/// </summary>
0 commit comments