From 00200218724ca1e1947f93ebe913eb767511745c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Trommel?= Date: Thu, 11 Jun 2026 15:26:04 +0200 Subject: [PATCH 1/4] Fixed code to support mixed collection (and subclass use further down in the BindingPath string). Included test cases for collection of items with the same parent class, on which shared properties and indexers are defined. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniël Trommel --- src/Extensions/ObjectExtensions.cs | 87 ++++++++++++++++++----- tests/ObjectExtensionsTests.cs | 108 +++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 17 deletions(-) diff --git a/src/Extensions/ObjectExtensions.cs b/src/Extensions/ObjectExtensions.cs index 51fabd7..7b4ee85 100644 --- a/src/Extensions/ObjectExtensions.cs +++ b/src/Extensions/ObjectExtensions.cs @@ -519,19 +519,23 @@ private static Expression BuildPropertyPathExpressionTree(ParameterExpression pa { Expression current = parameterObj; + var matches = PropertyPathRegex().Matches(bindingPath); + // The function uses a generic object input parameter to allow for any type of data item, - // 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 + // but we cast only to the root type that actually declares the first path segment. + // This keeps the accessor compatible with sibling subclasses in mixed collections. { - var typeActual = dataItem.GetType(); - if (current.Type != typeActual && !typeActual.IsValueType) - current = Expression.Convert(current, dataItem.GetType()); + var t = dataItem.GetType(); + // Resolve the declaring type for the first segment (property or indexer). + // If we cannot resolve it, keep the original runtime type as fallback. + var typeRoot = matches.Count > 0 ? GetDeclaringTypeForPathSegment(t, matches[0].Value) ?? t : t; + if (current.Type != typeRoot && !typeRoot.IsValueType) + current = Expression.Convert(current, typeRoot); } - var matches = PropertyPathRegex().Matches(bindingPath); - - foreach (Match match in matches) + for (var matchIndex = 0; matchIndex < matches.Count; matchIndex++) { - string part = match.Value; + var part = matches[matchIndex].Value; Expression nextPropertyAccess; // Indexer @@ -577,21 +581,28 @@ private static Expression BuildPropertyPathExpressionTree(ParameterExpression pa ); } - // Only check for type compatibility (i.e.: the need for conversion) if this is not the last match - if (match != matches[^1]) + // Only check for type compatibility (i.e.: the need for conversion) if this is not the last match. + // We only specialize when the expression type is still object, to avoid over-specializing + // to a sample instance subtype and breaking mixed type rows. + if (matchIndex < matches.Count - 1 && current.Type == typeof(object)) { - // Compile a lambda of the partial expression thus far (cast to object), to see if we need to add a cast var lambdaTemp = Expression.Lambda>(EnsureObjectCompatibleResult(current), parameterObj); var funcCurrent = lambdaTemp.Compile(); - // Evaluate this compiled function, to see if the result type is more specific than the current expression type. If so, cast to it var result = funcCurrent(dataItem); - var typeResult = result?.GetType() ?? current.Type; - if (current.Type != typeResult) + // The partial result gives us the runtime container for the NEXT segment. + // Convert to the most general declaring type for that next segment (property/indexer) + // instead of converting directly to the concrete runtime subtype. + var typeResult = result?.GetType(); + if (typeResult != null) { - // Note that we do not need to check for null before we convert, as the null check is already done in the previous condition - // So, we can safely convert the expression to the result type, even for value types (without the null check, a conversion of null to e.g. an int would result in a NullException being thrown) - current = Expression.Convert(current, typeResult); + var nextPart = matches[matchIndex + 1].Value; + var typeCompatible = GetDeclaringTypeForPathSegment(typeResult, nextPart) ?? typeResult; + + if (current.Type != typeCompatible) + { + current = Expression.Convert(current, typeCompatible); + } } } } @@ -607,6 +618,48 @@ private static Expression EnsureObjectCompatibleResult(Expression expression) return expression; } + /// + /// Resolves the declaring type for one binding-path segment on the provided candidate type. + /// + /// The type on which the segment should be resolved. + /// One binding path segment, either a property name or an indexer token like "[0]". + /// The segment declaring type when resolved; otherwise . + private static Type? GetDeclaringTypeForPathSegment(Type candidateType, string segment) + { + if (string.IsNullOrWhiteSpace(segment)) + return null; + + // Indexer segment + if (segment.StartsWith('[') && segment.EndsWith(']')) + { + // Infer CLR argument types from parsed index values. + var indices = GetIndices(segment[1..^1]); + var indexTypes = indices.Select(i => i.GetType()).ToArray(); + + // Find an indexer whose parameter list is assignment-compatible with parsed index types. + // GetProperties includes inherited members, so we can resolve indexers declared on a base class as well. + var indexerInfo = candidateType + .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(p => p.GetIndexParameters().Length == indexTypes.Length) + .FirstOrDefault(p => + { + var indexParameters = p.GetIndexParameters(); + for (var i = 0; i < indexParameters.Length; i++) + { + if (!indexParameters[i].ParameterType.IsAssignableFrom(indexTypes[i])) + return false; + } + return true; + }); + + return indexerInfo?.DeclaringType; + } + + // Property segment + var propertyInfo = candidateType.GetProperty(segment, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + return propertyInfo?.DeclaringType; + } + /// /// Returns the indices from a (possible multi-dimensional) indexer that may be a mixture of integers and strings. /// diff --git a/tests/ObjectExtensionsTests.cs b/tests/ObjectExtensionsTests.cs index 7f79967..fa65475 100644 --- a/tests/ObjectExtensionsTests.cs +++ b/tests/ObjectExtensionsTests.cs @@ -304,6 +304,61 @@ public void GetCompiledValueGetter_ShouldReturnNull_ForEmptyList() Assert.IsNull(result); } + [TestMethod] + public void GetCompiledValueGetter_ShouldSupportMixedSiblingRows_WhenFirstPropertyDeclaredOnBaseType() + { + SharedRow first = new VariantRowA { Group = "AAA" }; + var func = first.GetCompiledValueGetter("Group"); + Assert.IsNotNull(func); + + SharedRow second = new VariantRowB { Group = "BBB" }; + var result = func(second); + + Assert.AreEqual("BBB", result); + } + + [TestMethod] + public void GetCompiledValueGetter_ShouldSupportTwoStepPath_WithNestedSubclassValueFromDifferentSibling() + { + var first = new RootRowA + { + Shared = new SharedContainer + { + Nested = new NestedA { Name = "AAA" } + } + }; + + var func = first.GetCompiledValueGetter("Shared.Nested.Name"); + Assert.IsNotNull(func); + + var second = new RootRowB + { + Shared = new SharedContainer + { + Nested = new NestedB { Name = "BBB" } + } + }; + + var result = func(second); + Assert.AreEqual("BBB", result); + } + + [TestMethod] + public void GetCompiledValueGetter_ShouldSupportIndexerFirstPath_WhenIndexerDeclaredOnBaseType() + { + SharedRow first = new VariantRowA(); + first[1] = "AAA"; + + var func = first.GetCompiledValueGetter("[1]"); + Assert.IsNotNull(func); + + SharedRow second = new VariantRowB(); + second[1] = "BBB"; + + var result = func(second); + Assert.AreEqual("BBB", result); + } + [TestMethod] public void GetCompiledValueSetter_ShouldSetSimpleProperty() { @@ -929,5 +984,58 @@ public struct TestStruct { public int Value { get; set; } } + private abstract class SharedRow + { + public string Group { get; set; } = string.Empty; + + private readonly Dictionary _values = new(); + + public string this[int key] + { + get => _values[key]; + set => _values[key] = value; + } + } + + private class VariantRowA : SharedRow + { + public string Value { get; set; } = string.Empty; + } + + private class VariantRowB : SharedRow + { + public bool Flag { get; set; } + } + + private class RootBase + { + public SharedContainer Shared { get; set; } = new(); + } + + private class RootRowA : RootBase + { + } + + private class RootRowB : RootBase + { + } + + private class SharedContainer + { + public NestedBase Nested { get; set; } = new NestedA(); + } + + private class NestedBase + { + public string Name { get; set; } = string.Empty; + } + + private class NestedA : NestedBase + { + } + + private class NestedB : NestedBase + { + } } From 120e06e70549057bd9a82770ce7555153b70a329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Trommel?= Date: Thu, 11 Jun 2026 20:34:16 +0200 Subject: [PATCH 2/4] Also use the declaring type derivation for the new Setter flow --- src/Extensions/ObjectExtensions.cs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Extensions/ObjectExtensions.cs b/src/Extensions/ObjectExtensions.cs index 7b4ee85..8f9acea 100644 --- a/src/Extensions/ObjectExtensions.cs +++ b/src/Extensions/ObjectExtensions.cs @@ -78,10 +78,17 @@ private static Expression BuildPropertyPathSetterExpressionTree(ParameterExpress Expression current = parameterObj; - var actualType = dataItem.GetType(); - - if (current.Type != actualType && !actualType.IsValueType) - current = Expression.Convert(current, actualType); + // The function uses a generic object input parameter to allow for any type of data item, + // but we cast only to the root type that actually declares the first path segment. + // This keeps the accessor compatible with sibling subclasses in mixed collections. + { + var t = dataItem.GetType(); + // Resolve the declaring type for the first segment (property or indexer). + // If we cannot resolve it, keep the original runtime type as fallback. + var typeRoot = matches.Count > 0 ? GetDeclaringTypeForPathSegment(t, matches[0].Value) ?? t : t; + if (current.Type != typeRoot && !typeRoot.IsValueType) + current = Expression.Convert(current, typeRoot); + } // Navigate to the parent object of the final path segment. for (int i = 0; i < matches.Count - 1; i++) @@ -101,10 +108,13 @@ private static Expression BuildPropertyPathSetterExpressionTree(ParameterExpress if (currentValue is null) throw new ArgumentException($"Cannot build setter. Path segment '{part}' evaluates to null."); - var runtimeType = currentValue.GetType(); + // Use the declaring type of the NEXT segment instead of the sample instance's runtime type. + // This prevents lock-in to a specific sibling subtype and maintains mixed-collection compatibility. + var nextSegment = matches[i + 1].Value; + var typeCompatible = GetDeclaringTypeForPathSegment(currentValue.GetType(), nextSegment) ?? currentValue.GetType(); - if (current.Type != runtimeType) - current = Expression.Convert(current, runtimeType); + if (current.Type != typeCompatible) + current = Expression.Convert(current, typeCompatible); } var finalPart = matches[^1].Value; From 0b80d0e915e09d98b2e281ff9ff63e94c6e2786e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Trommel?= Date: Thu, 11 Jun 2026 20:57:23 +0200 Subject: [PATCH 3/4] Rename; "BindingPath" is the accurate name; "PropertyPath" was an initial naming slip-up from when I first build it (focussed on properties) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniël Trommel --- src/Extensions/ObjectExtensions.cs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Extensions/ObjectExtensions.cs b/src/Extensions/ObjectExtensions.cs index 8f9acea..c0b4758 100644 --- a/src/Extensions/ObjectExtensions.cs +++ b/src/Extensions/ObjectExtensions.cs @@ -15,23 +15,23 @@ namespace WinUI.TableView.Extensions; /// internal static partial class ObjectExtensions { - // Regex to split property paths into property names and indexers (for cases like e.g. "[2].Foo[0].Bar", where Foo might be a Property that returns an array) + // Regex to split binding paths into segments; property names and indexers (for cases like e.g. "[2].Foo[0].Bar", where Foo might be a Property that returns an array) [GeneratedRegex(@"([^.[]+)|(\[[^\]]+\])", RegexOptions.Compiled)] - private static partial Regex PropertyPathRegex(); + private static partial Regex BindingPathRegex(); /// - /// Creates and returns a compiled lambda expression for accessing the property path on instances, with runtime type checking and casting support. + /// Creates and returns a compiled lambda expression for accessing the binding path on instances, with runtime type checking and casting support. /// /// The data item instance to use for runtime type evaluation. /// The binding path to access, e.g. "[0].SubPropertyArray[0].SubSubProperty". - /// A compiled function that takes an instance and returns the property value, or null if the property path is invalid. + /// A compiled function that takes an instance and returns the property value, or null if the binding path is invalid. internal static Func? GetCompiledValueGetter(this object dataItem, string bindingPath) { try { // Build the property access expression chain with runtime type checking var parameterObj = Expression.Parameter(typeof(object), "obj"); - var expressionTree = BuildPropertyPathExpressionTree(parameterObj, bindingPath, dataItem); + var expressionTree = BuildGetterExpressionTree(parameterObj, bindingPath, dataItem); // Compile the lambda expression var lambda = Expression.Lambda>(expressionTree, parameterObj); @@ -50,7 +50,7 @@ internal static partial class ObjectExtensions var parameterObj = Expression.Parameter(typeof(object), "obj"); var parameterValue = Expression.Parameter(typeof(object), "value"); - var expressionTree = BuildPropertyPathSetterExpressionTree( + var expressionTree = BuildSetterExpressionTree( parameterObj, parameterValue, bindingPath, @@ -69,9 +69,9 @@ internal static partial class ObjectExtensions } } - private static Expression BuildPropertyPathSetterExpressionTree(ParameterExpression parameterObj, ParameterExpression parameterValue, string bindingPath, object dataItem) + private static Expression BuildSetterExpressionTree(ParameterExpression parameterObj, ParameterExpression parameterValue, string bindingPath, object dataItem) { - var matches = PropertyPathRegex().Matches(bindingPath); + var matches = BindingPathRegex().Matches(bindingPath); if (matches.Count == 0) throw new ArgumentException("Binding path is empty.", nameof(bindingPath)); @@ -519,17 +519,17 @@ private static Expression ConvertValueExpression(Expression value, Type targetTy } /// - /// Builds an expression tree for accessing a property path on the given instance expression, with runtime type checking and casting support. + /// Builds an expression tree for accessing a binding path on the given instance expression, with runtime type checking and casting support. /// /// The expression representing the instance parameter for which the binding path will be evaluated. /// The binding path to access. /// The actual data item to use for runtime type evaluation, to help with any needed subclass type conversions. /// An expression that accesses the property value specified by the binding path for the provided dataItem instance. - private static Expression BuildPropertyPathExpressionTree(ParameterExpression parameterObj, string bindingPath, object dataItem) + private static Expression BuildGetterExpressionTree(ParameterExpression parameterObj, string bindingPath, object dataItem) { Expression current = parameterObj; - var matches = PropertyPathRegex().Matches(bindingPath); + var matches = BindingPathRegex().Matches(bindingPath); // The function uses a generic object input parameter to allow for any type of data item, // but we cast only to the root type that actually declares the first path segment. From 3fbd4e1395449de002994a3c9b8239860740a840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Trommel?= Date: Thu, 11 Jun 2026 22:02:03 +0200 Subject: [PATCH 4/4] Refactor, so the Setter flow uses as much as possible the same code as the original Getter flow; when navigating to the final segment, the code is exactly the same. Only the final bit of code (get versus set) is different. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This refactor also exposed a subtle bug; GetCompiledValueSetter_ShouldNotSet_ForOutOfBoundsNonGenericListIndex failed with ArgumentOutOfRangeException. This was a latent bug in the setter that the old over-specialization was masking: - For a property typed IList (non-generic ArrayList), the shared navigation correctly leaves current.Type as the interface IList (no over-specialization). - The setter's bounds-check did current.Type.GetProperty("Count"), but Count is declared on the base ICollection interface — and reflection doesn't surface inherited members on interface types. So countProperty was null, the bounds check was skipped, and the raw indexer assignment threw. - The old setter only "worked" by accident: it had converted current to the concrete ArrayList, where Count is found directly. --- src/Extensions/ObjectExtensions.cs | 132 +++++++++-------------------- 1 file changed, 38 insertions(+), 94 deletions(-) diff --git a/src/Extensions/ObjectExtensions.cs b/src/Extensions/ObjectExtensions.cs index c0b4758..2a79d81 100644 --- a/src/Extensions/ObjectExtensions.cs +++ b/src/Extensions/ObjectExtensions.cs @@ -76,46 +76,9 @@ private static Expression BuildSetterExpressionTree(ParameterExpression paramete if (matches.Count == 0) throw new ArgumentException("Binding path is empty.", nameof(bindingPath)); - Expression current = parameterObj; - - // The function uses a generic object input parameter to allow for any type of data item, - // but we cast only to the root type that actually declares the first path segment. - // This keeps the accessor compatible with sibling subclasses in mixed collections. - { - var t = dataItem.GetType(); - // Resolve the declaring type for the first segment (property or indexer). - // If we cannot resolve it, keep the original runtime type as fallback. - var typeRoot = matches.Count > 0 ? GetDeclaringTypeForPathSegment(t, matches[0].Value) ?? t : t; - if (current.Type != typeRoot && !typeRoot.IsValueType) - current = Expression.Convert(current, typeRoot); - } - - // Navigate to the parent object of the final path segment. - for (int i = 0; i < matches.Count - 1; i++) - { - var part = matches[i].Value; - - current = part.StartsWith('[') && part.EndsWith(']') - ? BuildIndexerGetterExpression(current, part) - : BuildPropertyGetterExpression(current, part); - - var lambdaTemp = Expression.Lambda>( - EnsureObjectCompatibleResult(current), - parameterObj); - - var currentValue = lambdaTemp.Compile()(dataItem); - - if (currentValue is null) - throw new ArgumentException($"Cannot build setter. Path segment '{part}' evaluates to null."); - - // Use the declaring type of the NEXT segment instead of the sample instance's runtime type. - // This prevents lock-in to a specific sibling subtype and maintains mixed-collection compatibility. - var nextSegment = matches[i + 1].Value; - var typeCompatible = GetDeclaringTypeForPathSegment(currentValue.GetType(), nextSegment) ?? currentValue.GetType(); - - if (current.Type != typeCompatible) - current = Expression.Convert(current, typeCompatible); - } + // Reuse the getter's navigation logic to reach the parent of the final segment. + // Only the final segment is setter-specific (a write target instead of a read). + var current = BuildPathNavigationExpression(parameterObj, matches, dataItem, matches.Count - 1); var finalPart = matches[^1].Value; @@ -271,7 +234,14 @@ private static Expression BuildObjectIndexerSetterExpression(Expression current, typeof(IList).IsAssignableFrom(current.Type) || typeof(ICollection).IsAssignableFrom(current.Type)) { - var countProperty = current.Type.GetProperty("Count"); + // Count may be declared on a base interface (e.g. IList inherits Count from ICollection), + // and reflection does not surface inherited interface members, so search interfaces too. + // The container type is no longer specialized to the concrete runtime type during navigation, + // so current.Type can legitimately be an interface like IList here. + var countProperty = current.Type.GetProperty("Count") + ?? current.Type.GetInterfaces() + .Select(i => i.GetProperty("Count")) + .FirstOrDefault(p => p is not null); if (countProperty != null) { @@ -497,40 +467,38 @@ private static bool TryConvertObject(object? value, Type targetType, out object? } } - private static Expression ConvertValueExpression(Expression value, Type targetType) + /// + /// Builds an expression tree for accessing a binding path on the given instance expression, with runtime type checking and casting support. + /// + /// The expression representing the instance parameter for which the binding path will be evaluated. + /// The binding path to access. + /// The actual data item to use for runtime type evaluation, to help with any needed subclass type conversions. + /// An expression that accesses the property value specified by the binding path for the provided dataItem instance. + private static Expression BuildGetterExpressionTree(ParameterExpression parameterObj, string bindingPath, object dataItem) { - if (targetType == typeof(object)) - return value; - - var nullableType = Nullable.GetUnderlyingType(targetType); - - if (nullableType is not null) - { - return Expression.Condition( - Expression.Equal(value, Expression.Constant(null)), - Expression.Constant(null, targetType), - Expression.Convert(value, targetType)); - } + var matches = BindingPathRegex().Matches(bindingPath); - if (!targetType.IsValueType) - return Expression.Convert(value, targetType); + // Navigate through every segment to reach the final value. + var current = BuildPathNavigationExpression(parameterObj, matches, dataItem, matches.Count); - return Expression.Convert(value, targetType); + return EnsureObjectCompatibleResult(current); } /// - /// Builds an expression tree for accessing a binding path on the given instance expression, with runtime type checking and casting support. + /// Builds the expression that navigates the first segments of a binding path on + /// the given instance expression, with runtime null-checks and mixed-collection-friendly type specialization. + /// Shared by both the getter (which navigates every segment) and the setter (which navigates to the parent of the + /// final segment), so the navigation logic only lives in one place. /// /// The expression representing the instance parameter for which the binding path will be evaluated. - /// The binding path to access. + /// The parsed binding path segments. /// The actual data item to use for runtime type evaluation, to help with any needed subclass type conversions. - /// An expression that accesses the property value specified by the binding path for the provided dataItem instance. - private static Expression BuildGetterExpressionTree(ParameterExpression parameterObj, string bindingPath, object dataItem) + /// The number of leading segments to navigate into. + /// An expression that accesses the value at the requested depth for the provided dataItem instance. + private static Expression BuildPathNavigationExpression(ParameterExpression parameterObj, MatchCollection matches, object dataItem, int navigateCount) { Expression current = parameterObj; - var matches = BindingPathRegex().Matches(bindingPath); - // The function uses a generic object input parameter to allow for any type of data item, // but we cast only to the root type that actually declares the first path segment. // This keeps the accessor compatible with sibling subclasses in mixed collections. @@ -543,37 +511,13 @@ private static Expression BuildGetterExpressionTree(ParameterExpression paramete current = Expression.Convert(current, typeRoot); } - for (var matchIndex = 0; matchIndex < matches.Count; matchIndex++) + for (var matchIndex = 0; matchIndex < navigateCount; matchIndex++) { var part = matches[matchIndex].Value; - Expression nextPropertyAccess; - // Indexer - if (part.StartsWith('[') && part.EndsWith(']')) - { - object[] indices = GetIndices(part[1..^1]); - - if (current.Type.IsArray) - { - // Arrays only support integer indexing - if (!indices.All(idx => idx is int)) - throw new ArgumentException($"Arrays only support integer indexing, not the provided indexer [{part[1..^1]}]"); - - nextPropertyAccess = AddArrayAccessWithBoundsCheck(current, [.. indices.Select(index => (int)index)]); - } - else - { - nextPropertyAccess = AddIndexerAccessWithSafetyChecks(current, indices); - } - } - // Simple property access - else - { - var propertyInfo = current.Type.GetProperty(part, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) - ?? throw new ArgumentException($"Property '{part}' not found on type '{current.Type.Name}'"); - - nextPropertyAccess = Expression.Property(current, propertyInfo); - } + var nextPropertyAccess = part.StartsWith('[') && part.EndsWith(']') + ? BuildIndexerGetterExpression(current, part) + : BuildPropertyGetterExpression(current, part); if (nextPropertyAccess.Type.IsValueType && !nextPropertyAccess.Type.IsNullableType()) { @@ -591,10 +535,10 @@ private static Expression BuildGetterExpressionTree(ParameterExpression paramete ); } - // Only check for type compatibility (i.e.: the need for conversion) if this is not the last match. + // Only check for type compatibility (i.e.: the need for conversion) if there is a following segment. // We only specialize when the expression type is still object, to avoid over-specializing // to a sample instance subtype and breaking mixed type rows. - if (matchIndex < matches.Count - 1 && current.Type == typeof(object)) + if (matchIndex + 1 < matches.Count && current.Type == typeof(object)) { var lambdaTemp = Expression.Lambda>(EnsureObjectCompatibleResult(current), parameterObj); var funcCurrent = lambdaTemp.Compile(); @@ -617,7 +561,7 @@ private static Expression BuildGetterExpressionTree(ParameterExpression paramete } } - return EnsureObjectCompatibleResult(current); + return current; } private static Expression EnsureObjectCompatibleResult(Expression expression)