diff --git a/src/Columns/TableViewBoundColumn.cs b/src/Columns/TableViewBoundColumn.cs index 5d3c87a..88b2330 100644 --- a/src/Columns/TableViewBoundColumn.cs +++ b/src/Columns/TableViewBoundColumn.cs @@ -1,6 +1,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Data; using System; +using System.Linq.Expressions; using System.Reflection; using WinUI.TableView.Extensions; @@ -11,25 +12,22 @@ namespace WinUI.TableView; /// public abstract class TableViewBoundColumn : TableViewColumn { - private Type? _listType; private string? _propertyPath; private Binding _binding = new(); - private (PropertyInfo, object?)[]? _propertyInfo; + + private Func? _funcCompiledPropertyPath; /// public override object? GetCellContent(object? dataItem) { - if (dataItem is null) return null; + if (dataItem is null) + return null; - if (_propertyInfo is null || dataItem.GetType() != _listType) - { - _listType = dataItem.GetType(); - dataItem = dataItem.GetValue(_listType, PropertyPath, out _propertyInfo); - } - else - { - dataItem = dataItem.GetValue(_propertyInfo); - } + if (_funcCompiledPropertyPath is null && !string.IsNullOrWhiteSpace(PropertyPath)) + _funcCompiledPropertyPath = dataItem.GetFuncCompiledPropertyPath(PropertyPath!); + + if (_funcCompiledPropertyPath is not null) + dataItem = _funcCompiledPropertyPath(dataItem); if (Binding?.Converter is not null) { diff --git a/src/Extensions/ObjectExtensions.cs b/src/Extensions/ObjectExtensions.cs index e53bf41..eee2500 100644 --- a/src/Extensions/ObjectExtensions.cs +++ b/src/Extensions/ObjectExtensions.cs @@ -1,6 +1,8 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Reflection; using System.Text.RegularExpressions; @@ -16,160 +18,369 @@ internal static partial class ObjectExtensions private static partial Regex PropertyPathRegex(); /// - /// Gets the value of a property from an object using a sequence of property info and index pairs. + /// Creates and returns a compiled lambda expression for accessing the property path on instances, with runtime type checking and casting support. /// - /// The object from which to get the value. - /// An array of property info and index pairs. - /// The value of the property, or null if the object is null. - internal static object? GetValue(this object? obj, (PropertyInfo pi, object? index)[] pis) + /// 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. + internal static Func? GetFuncCompiledPropertyPath(this object dataItem, string bindingPath) { - if (pis is null || pis.Length == 0) + try + { + // Build the property access expression chain with runtime type checking + var parameterObj = Expression.Parameter(typeof(object), "obj"); + var expressionTree = BuildPropertyPathExpressionTree(parameterObj, bindingPath, dataItem); + + // Compile the lambda expression + var lambda = Expression.Lambda>(expressionTree, parameterObj); + return lambda.Compile(); // To DEBUG, set a breakpoint here and inspect the "DebugView" property on the variable "lambda" + } + catch { return null; } + } + + /// + /// Builds an expression tree for accessing a property 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) + { + Expression current = parameterObj; - foreach (var (pi, index) in pis) + // 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 { - if (obj is null) - break; + var typeActual = dataItem.GetType(); + if (current.Type != typeActual && !typeActual.IsValueType) + current = Expression.Convert(current, dataItem.GetType()); + } - if (pi != null) - { - // Use property getter, with or without index - obj = index is not null ? pi.GetValue(obj, [index]) : pi.GetValue(obj); - } - else if (obj is IDictionary dictionary && dictionary.Contains(index!)) - { - obj = dictionary[index!]; - } - else if (index is int idx) + var matches = PropertyPathRegex().Matches(bindingPath); + + foreach (Match match in matches) + { + string part = match.Value; + Expression nextPropertyAccess; + + // Indexer + if (part.StartsWith('[') && part.EndsWith(']')) { - // Array - if (obj is Array arr && idx >= 0 && idx < arr.Length) + object[] indices = GetIndices(part[1..^1]); + + if (current.Type.IsArray) { - obj = arr.GetValue(idx); + // 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)]); } - // IList - else if (obj is IList list && idx >= 0 && list.Count < idx) + else { - obj = list[idx]; + 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); + } + + if (nextPropertyAccess.Type.IsValueType && !nextPropertyAccess.Type.IsNullableType()) + { + // Value types cannot be null, so don't need to check for null, and we can directly assign the property access + current = nextPropertyAccess; + } else { - // Not a supported path segment - return null; + // Add null check: if current is null, stop and return null; otherwise, continue with the next access + var notNullCheck = Expression.NotEqual(current, Expression.Constant(null)); + current = Expression.Condition( + notNullCheck, + nextPropertyAccess, + Expression.Constant(null, nextPropertyAccess.Type) + ); + } + + // Only check for type compatibility (i.e.: the need for conversion) if this is not the last match + if (match != matches[^1]) + { + // 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) + { + // 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); + } } } - return obj; + return EnsureObjectCompatibleResult(current); + } + + private static Expression EnsureObjectCompatibleResult(Expression expression) + { + // Only convert to object if the expression type is not already assignable to object without boxing + if (expression.Type.IsValueType + && !expression.Type.IsNullableType() // e.g. int? is considered a ValueType, but also nullable, which means it can be converted to object without boxing + ) + return Expression.Convert(expression, typeof(object)); + return expression; + } + + /// + /// Returns the indices from a (possible multi-dimensional) indexer that may be a mixture of integers and strings. + /// + private static object[] GetIndices(string stringIndexer) + { + // Split by comma and parse each indexer, removing whitespace + var indexerParts = stringIndexer.Split(',') + .Select(s => s.Trim()) + .ToArray(); + + // Parse each part into appropriate type (int or string) + return [.. indexerParts.Select(indexPart => + int.TryParse(indexPart, out int intIndex) ? (object)intIndex : indexPart + )]; } /// - /// Gets the value of a property from an object using a type and a property path. + /// Adds array access to the current expression, given the inputted indices, and adds bounds checking; + /// the code for this expression will return null if the indices are out of bounds. /// - /// The object from which to get the value. - /// The type of the object. - /// The property path. - /// An array of property info and index pairs. - /// The value of the property, or null if the object is null. - internal static object? GetValue(this object? obj, Type? type, string? path, out (PropertyInfo pi, object? index)[] pis) + private static BlockExpression AddArrayAccessWithBoundsCheck(Expression current, int[] indices) { - if (obj == null || string.IsNullOrWhiteSpace(path) || type == null) + if (!current.Type.IsArray) + throw new ArgumentException("Current expression must be an array."); + + // Since array/indexer handling does various checks, it is more performant and readable to store the current expression in a variable + // which we will use as input to the block with array/indexer handling code. + var parameterArray = Expression.Parameter(current.Type, "array"); + var assignArray = Expression.Assign(parameterArray, current); + + var rank = current.Type.GetArrayRank(); + if (indices.Length != rank) + throw new ArgumentException($"Array of rank {rank} requires {rank} indices, but {indices.Length} were provided."); + + // Bounds check for each dimension + Expression boundsCheck = null!; + var getLengthMethod = typeof(Array).GetMethod("GetLength")!; + var indexConstants = new Expression[rank]; + for (int dimension = 0; dimension < rank; dimension++) { - pis = []; - return obj; + var index = indices[dimension]; + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(indices), $"Index for dimension {dimension} cannot be negative: {index}"); + + var indexConst = Expression.Constant(index); + indexConstants[dimension] = indexConst; + + // GetLength method call for the current dimension + var lengthProp = Expression.Call(parameterArray, getLengthMethod, Expression.Constant(dimension)); + + var dimCheck = Expression.LessThan(indexConst, lengthProp); + boundsCheck = boundsCheck == null ? dimCheck : Expression.AndAlso(boundsCheck, dimCheck); } - var matches = PropertyPathRegex().Matches(path); - if (matches.Count == 0) + // If bounds check is not satisfied, return null; otherwise, access the array element + var expressionBlock = Expression.Condition( + boundsCheck, + Expression.Convert(Expression.ArrayAccess(parameterArray, indexConstants), typeof(object)), + Expression.Constant(null, typeof(object)) + ); + + // Add the block + return Expression.Block( + [parameterArray], + assignArray, + expressionBlock + ); + + } + + /// + /// Adds safe indexer access to the current expression, with appropriate bounds/key checking for various collection types. + /// Returns null if the indexer access would fail (out of bounds, missing key, etc.). + /// + private static Expression AddIndexerAccessWithSafetyChecks(Expression current, object[] indices) + { + var currentType = current.Type; + var parameter = Expression.Parameter(currentType, "collection"); + var assignCurrent = Expression.Assign(parameter, current); + + // Handle IDictionary - use TryGetValue + if (TryCreateDictionaryTryGetExpression(currentType, parameter, indices, out var dictionaryExpr)) { - pis = []; - return obj; + return Expression.Block([parameter], assignCurrent, dictionaryExpr); } - // Pre-size the steps array to the number of matches - pis = new (PropertyInfo, object?)[matches.Count]; - var i = 0; + // Handle (generic) IList and ICollection - bounds checking + if (TryCreateIListOrICollectionBoundsCheckExpression(currentType, parameter, indices, out var listExpr)) + { + return Expression.Block([parameter], assignCurrent, listExpr); + } - foreach (Match match in matches) + // Handle generic indexers - check if indexer exists + if (TryCreateGenericIndexerExpression(currentType, parameter, indices, out var indexerExpr)) { - var part = match.Value; - object? index = null; + return Expression.Block([parameter], assignCurrent, indexerExpr); + } - if (part.StartsWith('[') && part.EndsWith(']')) - { - index = part[1..^1]; + // If no safe indexer can be created, return null + return Expression.Constant(null, typeof(object)); + } - // Try IDictionary - if (obj is IDictionary dictionary && dictionary.Contains(index!)) - { - obj = dictionary[index!]; - pis[i++] = (null!, index); - type = obj?.GetType(); - continue; - } - else if (int.TryParse(part[1..^1], out var idx)) - { - index = idx; - - // Try array - if (obj is Array arr && idx >= 0 && idx < arr.Length) - { - obj = arr.GetValue(idx); - pis[i++] = (null!, idx); - type = obj?.GetType(); - continue; - } - // Try IList - else if (obj is IList list && idx >= 0 && list.Count < idx) - { - obj = list[idx]; - pis[i++] = (null!, idx); - type = obj?.GetType(); - continue; - } + /// + /// Creates a TryGetValue expression for IDictionary types. + /// + private static bool TryCreateDictionaryTryGetExpression(Type type, ParameterExpression parameter, object[] indices, out Expression expression) + { + expression = null!; - } + if (indices.Length != 1) + return false; - part = "Item"; // Default indexer property name - } + // Find IDictionary interface + var dictionaryInterface = type.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>)); - if (TryGetPropertyValue(ref obj, ref type, part, index, out var pi)) - { - pis[i++] = (pi!, index); - } - else - { - pis = null!; - return null; - } - } + if (dictionaryInterface == null) + return false; //apparently not a dictionary - static bool TryGetPropertyValue(ref object? obj, ref Type? type, string propertyName, object? index, out PropertyInfo? pi) + var genericArgs = dictionaryInterface.GetGenericArguments(); + var keyType = genericArgs[0]; + var valueType = genericArgs[1]; + + // Check if the index type matches the key type + if (!keyType.IsAssignableFrom(indices[0].GetType())) { - try - { - pi = index is null ? type?.GetProperty(propertyName) : type?.GetProperty(propertyName, [index.GetType()]); + // Type mismatch - return an expression that always returns null + expression = Expression.Constant(null, typeof(object)); + return true; + } - if (pi != null) - { - obj = index is null ? pi.GetValue(obj) : pi.GetValue(obj, [index]); - type = obj?.GetType(); + // Get TryGetValue method + var tryGetValueMethod = dictionaryInterface.GetMethod("TryGetValue"); + if (tryGetValueMethod == null) + throw new InvalidOperationException($"The dictionary type {type} has no TryGetValue method"); // should not happen - return true; - } + // Create variables for the key and output value + var keyExpr = Expression.Constant(indices[0], keyType); + var valueVar = Expression.Parameter(valueType, "value"); - return false; - } - catch + // Call TryGetValue + var tryGetCall = Expression.Call(parameter, tryGetValueMethod, keyExpr, valueVar); + + // Return value if found, null if not found + expression = Expression.Block( + [valueVar], + Expression.Condition( + tryGetCall, + Expression.Convert(valueVar, typeof(object)), + Expression.Constant(null, typeof(object)) + ) + ); + + return true; + } + + /// + /// Creates a bounds-checked expression for IList and ICollection types. + /// + private static bool TryCreateIListOrICollectionBoundsCheckExpression(Type type, ParameterExpression parameter, object[] indices, out Expression expression) + { + expression = null!; + + if (indices.Length != 1 || indices[0] is not int index) + return false; + + // Try to find an indexer property with the appropriate parameter types + var indexerProperty = type.GetProperty("Item", [typeof(int)]); + if (indexerProperty == null) + return false; // No indexer found + + // Check for IList + var listInterface = type.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && + (i.GetGenericTypeDefinition() == typeof(IList<>) || + i.GetGenericTypeDefinition() == typeof(ICollection<>) + )); + + if (listInterface != null || + typeof(IList).IsAssignableFrom(type) || // Does have an indexer + typeof(ICollection).IsAssignableFrom(type) // Has no indexer, but we can still check bounds; since it derives from ICollection at the least, the indexer is expected to be aimed at the collection + ) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(indices), $"Index for (generic) IList/ICollection cannot be negative: {index}"); + + var countProperty = type.GetProperty("Count"); + + if (indexerProperty != null && countProperty != null) { - pi = null!; - return false; + var indexExpr = Expression.Constant(index); + var countExpr = Expression.Property(parameter, countProperty); + var boundsCheck = Expression.LessThan(indexExpr, countExpr); + + expression = Expression.Condition( + boundsCheck, + Expression.Convert(Expression.Property(parameter, indexerProperty, indexExpr), typeof(object)), + Expression.Constant(null, typeof(object)) + ); + + return true; } } - return obj; + return false; + } + + /// + /// Creates an expression for generic indexers, with try-catch to handle potential exceptions. + /// Returns null if indexer access fails for any reason. + /// + private static bool TryCreateGenericIndexerExpression(Type type, ParameterExpression parameter, object[] indices, out Expression expression) + { + expression = null!; + + // Try to find an indexer property with the appropriate parameter types + var indexerTypes = indices.Select(idx => idx.GetType()).ToArray(); + var indexerProperty = type.GetProperty("Item", indexerTypes); + + if (indexerProperty == null) + return false; + + // Create constant expressions for each index + var indexExpressions = indices.Select(Expression.Constant).ToArray(); + + // Create the indexer access + var indexerAccess = Expression.Property(parameter, indexerProperty, indexExpressions); + + // Wrap in try-catch to handle any exceptions that might occur during indexer access, due to non-existing keys or out-of-bounds indices + var tryBlock = Expression.Convert(indexerAccess, typeof(object)); + var catchBlock = Expression.Constant(null, typeof(object)); + + // Create try-catch expression - return null for any exception + expression = Expression.TryCatch( + tryBlock, + Expression.Catch(typeof(Exception), catchBlock) + ); + + return true; } /// diff --git a/tests/ObjectExtensionsTests.cs b/tests/ObjectExtensionsTests.cs index e549279..5041859 100644 --- a/tests/ObjectExtensionsTests.cs +++ b/tests/ObjectExtensionsTests.cs @@ -1,4 +1,3 @@ -using System; using System.Collections; using System.Collections.Generic; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -10,146 +9,280 @@ namespace WinUI.TableView.Tests; public class ObjectExtensionsTests { [TestMethod] - public void GetValue_ShouldAccessSimpleNestedProperty_UsingPathString() + public void GetFuncCompiledPropertyPath_ShouldAccessSimpleProperty() { - var testItem = new TestItem { SubItems = [new() { SubSubItems = [new() { Name = "NestedValue" }] }] }; - var result = testItem.GetValue(typeof(TestItem), "SubItems[0].SubSubItems[0].Name", out var _); - Assert.IsNotNull(result); - Assert.AreEqual("NestedValue", result); + var testItem = new TestItem { Number = 7 }; + var func = testItem.GetFuncCompiledPropertyPath("Number"); + Assert.IsNotNull(func); + var result = func(testItem); + Assert.AreEqual(7, result); } [TestMethod] - public void GetValue_ShouldAccessSimpleNestedProperty_UsingParsedPath() + public void GetFuncCompiledPropertyPath_ShouldAccessNestedProperty() { var testItem = new TestItem { SubItems = [new() { SubSubItems = [new() { Name = "NestedValue" }] }] }; - testItem.GetValue(typeof(TestItem), "SubItems[0].SubSubItems[0].Name", out var pis); - var result = testItem.GetValue(pis); - Assert.IsNotNull(result); + var func = testItem.GetFuncCompiledPropertyPath("SubItems[0].SubSubItems[0].Name"); + Assert.IsNotNull(func); + var result = func(testItem); Assert.AreEqual("NestedValue", result); } [TestMethod] - public void GetValue_ShouldAccessArrayElement_UsingPathString() + public void GetFuncCompiledPropertyPath_ShouldAccessArrayElement() { var testItem = new TestItem { IntArray = [10, 20, 30] }; - var result = testItem.GetValue(typeof(TestItem), "IntArray[1]", out var _); - Assert.IsNotNull(result); + var func = testItem.GetFuncCompiledPropertyPath("IntArray[1]"); + Assert.IsNotNull(func); + var result = func(testItem); Assert.AreEqual(20, result); } [TestMethod] - public void GetValue_ShouldAccessArrayElement_UsingParsedPath() + public void GetFuncCompiledPropertyPath_ShouldAccess2DArrayElement() { - var testItem = new TestItem { IntArray = [10, 20, 30] }; - testItem.GetValue(typeof(TestItem), "IntArray[1]", out var pis); - var result = testItem.GetValue(pis); - Assert.IsNotNull(result); + var testItem = new TestItem { Int2DArray = new int[,] {{1, 2, 3}, {10, 20, 30}} }; + var func = testItem.GetFuncCompiledPropertyPath("Int2DArray[1,1]"); + Assert.IsNotNull(func); + var result = func(testItem); Assert.AreEqual(20, result); } [TestMethod] - public void GetValue_ShouldAccessDictionaryByStringKey_UsingPathString() + public void GetFuncCompiledPropertyPath_ShouldAccessMultiDimensionalIndexer() { - var testItem = new TestItem { Dictionary1 = new() { { "key1", "value1" } } }; - var result = testItem.GetValue(typeof(TestItem), "Dictionary1[key1]", out var _); - Assert.IsNotNull(result); - Assert.AreEqual("value1", result); + var testItem = new TestItem(); + testItem[2, "foo"] = "bar"; + var func = testItem.GetFuncCompiledPropertyPath("[2,foo]"); + Assert.IsNotNull(func); + var result = func(testItem); + Assert.AreEqual("bar", result); } [TestMethod] - public void GetValue_ShouldAccessDictionaryByStringKey_UsingParsedPath() + public void GetFuncCompiledPropertyPath_ShouldAccessDictionaryByStringKey() { var testItem = new TestItem { Dictionary1 = new() { { "key1", "value1" } } }; - testItem.GetValue(typeof(TestItem), "Dictionary1[key1]", out var pis); - var result = testItem.GetValue(pis); - Assert.IsNotNull(result); + var func = testItem.GetFuncCompiledPropertyPath("Dictionary1[key1]"); + Assert.IsNotNull(func); + var result = func(testItem); Assert.AreEqual("value1", result); } [TestMethod] - public void GetValue_ShouldAccessDictionaryByIntKey_UsingPathString() + public void GetFuncCompiledPropertyPath_ShouldAccessDictionaryByIntKey() { var testItem = new TestItem { Dictionary2 = new() { { 1, "value1" } } }; - var result = testItem.GetValue(typeof(TestItem), "Dictionary2[1]", out var _); - Assert.IsNotNull(result); + var func = testItem.GetFuncCompiledPropertyPath("Dictionary2[1]"); + Assert.IsNotNull(func); + var result = func(testItem); Assert.AreEqual("value1", result); } [TestMethod] - public void GetValue_ShouldAccessDictionaryByIntKey_UsingParsedPath() + public void GetFuncCompiledPropertyPath_ShouldReturnNull_ForInvalidProperty() { - var testItem = new TestItem { Dictionary2 = new() { { 1, "value1" } } }; - testItem.GetValue(typeof(TestItem), "Dictionary2[1]", out var pis); - var result = testItem.GetValue(pis); - Assert.IsNotNull(result); - Assert.AreEqual("value1", result); + var testItem = new TestItem(); + var func = testItem.GetFuncCompiledPropertyPath("NonExistent.Property.Path"); + Assert.IsNull(func); } [TestMethod] - public void GetValue_ShouldReturnNull_ForInvalidProperty_UsingPathString() + public void GetFuncCompiledPropertyPath_ShouldReturnNull_ForInvalidProperty2() { var testItem = new TestItem(); - var result = testItem.GetValue(typeof(TestItem), "NonExistent.Property.Path", out var _); - Assert.IsNull(result); + var func = testItem.GetFuncCompiledPropertyPath("SubItems[0].SubSubItems[0].Invalid"); + Assert.IsNull(func); } [TestMethod] - public void GetValue_ShouldReturnNull_ForInvalidProperty_UsingParsedPath() + public void GetFuncCompiledPropertyPath_ShouldReturnNull_ForInvalidProperty3() + { + var testItem = new TestItem { SubItems = [new() { SubSubItems = [new() { Name = "NestedValue" }] }] }; + var func = testItem.GetFuncCompiledPropertyPath("SubItems[0].SubSubItems[0].Invalid"); + Assert.IsNull(func); + } + + [TestMethod] + public void GetFuncCompiledPropertyPath_ShouldReturnNull_ForInvalidProperty4() { var testItem = new TestItem(); - testItem.GetValue(typeof(TestItem), "NonExistent.Property.Path", out var pis); - var result = testItem.GetValue(pis); + var func = testItem.GetFuncCompiledPropertyPath("Dictionary[123]"); + Assert.IsNull(func); + } + + [TestMethod] + public void GetFuncCompiledPropertyPath_ShouldReturnNull_ForInvalidDictionaryIndexer() + { + var testItem = new TestItem { Dictionary2 = new() { { 1, "value1" } } }; + var func = testItem.GetFuncCompiledPropertyPath("Dictionary2[1]"); + Assert.IsNotNull(func); + + var result = func(testItem); + Assert.AreEqual("value1", result); + + testItem = new TestItem { Dictionary2 = new() { { 2, "value2" } } }; + result = func(testItem); Assert.IsNull(result); } [TestMethod] - public void GetValue_ShouldReturnNull_ForInvalidProperty2_UsingPathString() + public void GetFuncCompiledPropertyPath_ShouldReturnNull_ForInvalidArrayIndex() { - var testItem = new TestItem(); - var result = testItem.GetValue(typeof(TestItem), "SubItems[0].SubSubItems[0].Invalid", out var _); + var testItem = new TestItem { SubItems = [new() { SubSubItems = [new() { Name = "NestedValue" }] }] }; + var func = testItem.GetFuncCompiledPropertyPath("SubItems[0].SubSubItems[0].Name"); + Assert.IsNotNull(func); + + var result = func(testItem); + Assert.AreEqual("NestedValue", result); + + testItem = new TestItem { SubItems = [new() { SubSubItems = null! }] }; + result = func(testItem); Assert.IsNull(result); } [TestMethod] - public void GetValue_ShouldReturnNull_ForInvalidProperty2_UsingParsedPath() + public void GetFuncCompiledPropertyPath_ShouldReturnNull_ForOutOfBoundsArrayIndex() { - var testItem = new TestItem(); - testItem.GetValue(typeof(TestItem), "SubItems[0].SubSubItems[0].Invalid", out var pis); - var result = testItem.GetValue(pis); + var testItem = new TestItem { IntArray = [10, 20, 30] }; + var func = testItem.GetFuncCompiledPropertyPath("IntArray[2]"); + Assert.IsNotNull(func); + var testItem2 = new TestItem { IntArray = [1] }; + var result = func(testItem2); Assert.IsNull(result); } [TestMethod] - public void GetValue_ShouldReturnNull_ForInvalidProperty3_UsingPathString() + public void GetFuncCompiledPropertyPath_ShouldReturnNull_ForOutOfBoundsMultiDimArrayIndex() { - var testItem = new TestItem { SubItems = [new() { SubSubItems = [new() { Name = "NestedValue" }] }] }; - var result = testItem.GetValue(typeof(TestItem), "SubItems[0].SubSubItems[0].Invalid", out var _); + var testItem3x3 = new TestItem { Int2DArray = new int[,] { { 1, 2, 3 }, { 10, 20, 30 } } }; + var func = testItem3x3.GetFuncCompiledPropertyPath("Int2DArray[2,2]"); + Assert.IsNotNull(func); + var result = func(testItem3x3); + + var testItem2x2 = new TestItem { Int2DArray = new int[,] { { 1, 2 }, { 10, 30 } } }; + result = func(testItem2x2); Assert.IsNull(result); } [TestMethod] - public void GetValue_ShouldReturnNull_ForInvalidProperty3_UsingParsedPath() + public void GetFuncCompiledPropertyPath_ShouldAccessListByIndex() { - var testItem = new TestItem { SubItems = [new() { SubSubItems = [new() { Name = "NestedValue" }] }] }; - testItem.GetValue(typeof(TestItem), "SubItems[0].SubSubItems[0].Invalid", out var pis); - var result = testItem.GetValue(pis); + var testItem = new TestItem { StringList = ["item0", "item1", "item2"] }; + var func = testItem.GetFuncCompiledPropertyPath("StringList[1]"); + Assert.IsNotNull(func); + var result = func(testItem); + Assert.AreEqual("item1", result); + } + + [TestMethod] + public void GetFuncCompiledPropertyPath_ShouldAccessPropertyOnString() + { + var testItem = new TestItem { StringList = ["item0", "item1 long text", "item2"] }; + var func = testItem.GetFuncCompiledPropertyPath("StringList[1].Length"); + Assert.IsNotNull(func); + var result = func(testItem); + Assert.AreEqual(15, result); + } + + [TestMethod] + public void GetFuncCompiledPropertyPath_ShouldReturnNull_ForOutOfBoundsListIndex() + { + var testItem = new TestItem { StringList = ["item0", "item1", "item2"] }; + var func = testItem.GetFuncCompiledPropertyPath("StringList[2]"); + Assert.IsNotNull(func); + var result = func(testItem); + Assert.AreEqual("item2", result); + + // Test with different data that has fewer items - should return null + testItem = new TestItem { StringList = ["item0"] }; + result = func(testItem); Assert.IsNull(result); } [TestMethod] - public void GetValue_ShouldReturnNull_ForInvalidIndexer_UsingPathString() + public void GetFuncCompiledPropertyPath_ShouldReturnNull_ForDictionaryKeyTypeMismatch() { - var testItem = new TestItem(); - var result = testItem.GetValue(typeof(TestItem), "Dictionary[123]", out var _); + // Dictionary accessed with int key + var testItem = new TestItem { Dictionary1 = new() { { "key1", "value1" } } }; + var func = testItem.GetFuncCompiledPropertyPath("Dictionary1[123]"); // int key for string-keyed dictionary + Assert.IsNotNull(func); + var result = func(testItem); Assert.IsNull(result); } [TestMethod] - public void GetValue_ShouldReturnNull_ForInvalidIndexer_UsingParsedPath() + public void GetFuncCompiledPropertyPath_ShouldAccessValueTypeProperty() + { + var testItem = new TestItem { ValueTypeStruct = new TestStruct { Value = 42 } }; + var func = testItem.GetFuncCompiledPropertyPath("ValueTypeStruct.Value"); + Assert.IsNotNull(func); + var result = func(testItem); + Assert.AreEqual(42, result); + } + + [TestMethod] + public void GetFuncCompiledPropertyPath_ShouldReturnNull_ForNegativeArrayIndex() + { + var testItem = new TestItem { IntArray = [10, 20, 30] }; + var func = testItem.GetFuncCompiledPropertyPath("IntArray[-1]"); + Assert.IsNull(func); // Should fail during expression building + } + + [TestMethod] + public void GetFuncCompiledPropertyPath_ShouldReturnNull_ForNegativeListIndex() + { + var testItem = new TestItem { StringList = ["item0", "item1"] }; + var func = testItem.GetFuncCompiledPropertyPath("StringList[-1]"); + Assert.IsNull(func); // Should fail during expression building + } + + [TestMethod] + public void GetFuncCompiledPropertyPath_ShouldReturnNull_ForWrongArrayDimensions() + { + var testItem = new TestItem { Int2DArray = new int[,] { { 1, 2 }, { 3, 4 } } }; + var func = testItem.GetFuncCompiledPropertyPath("Int2DArray[1]"); // 2D array with 1D index + Assert.IsNull(func); // Should fail during expression building + } + + [TestMethod] + public void GetFuncCompiledPropertyPath_ShouldReturnNull_ForThrowingIndexer() { var testItem = new TestItem(); - testItem.GetValue(typeof(TestItem), "Dictionary[123]", out var pis); - var result = testItem.GetValue(pis); + // This should trigger the generic indexer path with try-catch + var func = testItem.GetFuncCompiledPropertyPath("[999,nonexistent]"); + Assert.IsNotNull(func); + var result = func(testItem); + Assert.IsNull(result); // Custom indexer returns empty string, but expression should handle gracefully + } + + [TestMethod] + public void GetFuncCompiledPropertyPath_ShouldAccessNonGenericList() + { + var testItem = new TestItem { NonGenericList = new System.Collections.ArrayList { "item0", "item1", "item2" } }; + var func = testItem.GetFuncCompiledPropertyPath("NonGenericList[1]"); + Assert.IsNotNull(func); + var result = func(testItem); + Assert.AreEqual("item1", result); + } + + [TestMethod] + public void GetFuncCompiledPropertyPath_ShouldReturnNull_ForOutOfBoundsNonGenericList() + { + var testItem = new TestItem { NonGenericList = new System.Collections.ArrayList { "item0" } }; + var func = testItem.GetFuncCompiledPropertyPath("NonGenericList[5]"); + Assert.IsNotNull(func); + var result = func(testItem); + Assert.IsNull(result); + } + + [TestMethod] + public void GetFuncCompiledPropertyPath_ShouldReturnNull_ForEmptyList() + { + var testItem = new TestItem { StringList = [] }; + var func = testItem.GetFuncCompiledPropertyPath("StringList[0]"); + Assert.IsNotNull(func); + var result = func(testItem); Assert.IsNull(result); } @@ -213,9 +346,28 @@ private class SubItem private class TestItem { + public int Number { get; set; } = 0; + public TestStruct ValueTypeStruct { get; set; } + public IList NonGenericList { get; set; } = new ArrayList(); public List SubItems { get; set; } = []; public Dictionary Dictionary1 { get; set; } = []; public Dictionary Dictionary2 { get; set; } = []; public int[] IntArray { get; set; } = []; + public int[,] Int2DArray { get; set; } = new int[0, 0]; + public List StringList { get; set; } = []; + + // Multi-dimensional indexer + private readonly Dictionary<(int, string), string> _multiIndex = new(); + public string this[int i, string key] + { + get => _multiIndex[(i, key)]; + set => _multiIndex[(i, key)] = value; + } + } + + public struct TestStruct + { + public int Value { get; set; } } } +