diff --git a/AspNetCore.slnx b/AspNetCore.slnx index 2b5b7180956c..979c5d2165b8 100644 --- a/AspNetCore.slnx +++ b/AspNetCore.slnx @@ -1227,6 +1227,7 @@ + diff --git a/src/Components/Components.slnf b/src/Components/Components.slnf index 0ed65e49b0e2..aa83a0f8a9ef 100644 --- a/src/Components/Components.slnf +++ b/src/Components/Components.slnf @@ -19,6 +19,7 @@ "src\\Components\\Forms\\test\\Microsoft.AspNetCore.Components.Forms.Tests.csproj", "src\\Components\\QuickGrid\\Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter\\src\\Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter.csproj", "src\\Components\\QuickGrid\\Microsoft.AspNetCore.Components.QuickGrid\\src\\Microsoft.AspNetCore.Components.QuickGrid.csproj", + "src\\Components\\QuickGrid\\Microsoft.AspNetCore.Components.QuickGrid\\test\\Microsoft.AspNetCore.Components.QuickGrid.Tests.csproj", "src\\Components\\Samples\\BlazorServerApp\\BlazorServerApp.csproj", "src\\Components\\Samples\\BlazorUnitedApp.Client\\BlazorUnitedApp.Client.csproj", "src\\Components\\Samples\\BlazorUnitedApp\\BlazorUnitedApp.csproj", diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/GridSort.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/GridSort.cs index 0e891b2e206f..281616065ecf 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/GridSort.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/GridSort.cs @@ -135,7 +135,16 @@ private List BuildPropertyList(bool ascending) // Not sure we really want this level of complexity, but it converts expressions like @(c => c.Medals.Gold) to "Medals.Gold" private static string ToPropertyName(LambdaExpression expression) { - if (expression.Body is not MemberExpression body) + var expressionBody = expression.Body; + + // Handle UnaryExpressions that can occur due to implicit conversions, such as nullable value types + if (expressionBody.NodeType == ExpressionType.Convert || + expressionBody.NodeType == ExpressionType.ConvertChecked) + { + expressionBody = ((UnaryExpression)expressionBody).Operand; + } + + if (expressionBody is not MemberExpression body) { throw new ArgumentException(ExpressionNotRepresentableMessage); } diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Microsoft.AspNetCore.Components.QuickGrid.csproj b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Microsoft.AspNetCore.Components.QuickGrid.csproj index 6d43b9828a95..8603bc6d164e 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Microsoft.AspNetCore.Components.QuickGrid.csproj +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Microsoft.AspNetCore.Components.QuickGrid.csproj @@ -23,4 +23,8 @@ + + + + diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/test/GridSortTest.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/test/GridSortTest.cs new file mode 100644 index 000000000000..ec7a91d7fcdf --- /dev/null +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/test/GridSortTest.cs @@ -0,0 +1,182 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Linq.Expressions; + +namespace Microsoft.AspNetCore.Components.QuickGrid.Tests; + +public class GridSortTest +{ + // Test model classes + private class TestEntity + { + public string Name { get; set; } = string.Empty; + public int Age { get; set; } + public DateTime? NullableDate { get; set; } + public int? NullableInt { get; set; } + public TestChild Child { get; set; } = new(); + } + + private class TestChild + { + public string ChildName { get; set; } = string.Empty; + public DateTime? ChildNullableDate { get; set; } + } + + [Fact] + public void ToPropertyName_SimpleProperty_ReturnsPropertyName() + { + // Arrange + Expression> expression = x => x.Name; + + // Act + var gridSort = GridSort.ByAscending(expression); + var propertyList = gridSort.ToPropertyList(ascending: true); + + // Assert + Assert.Single(propertyList); + Assert.Equal("Name", propertyList.First().PropertyName); + Assert.Equal(SortDirection.Ascending, propertyList.First().Direction); + } + + [Fact] + public void ToPropertyName_NullableProperty_ReturnsPropertyName() + { + // Arrange + Expression> expression = x => x.NullableDate; + + // Act + var gridSort = GridSort.ByAscending(expression); + var propertyList = gridSort.ToPropertyList(ascending: true); + + // Assert + Assert.Single(propertyList); + Assert.Equal("NullableDate", propertyList.First().PropertyName); + Assert.Equal(SortDirection.Ascending, propertyList.First().Direction); + } + + [Fact] + public void ToPropertyName_NullableInt_ReturnsPropertyName() + { + // Arrange + Expression> expression = x => x.NullableInt; + + // Act + var gridSort = GridSort.ByAscending(expression); + var propertyList = gridSort.ToPropertyList(ascending: true); + + // Assert + Assert.Single(propertyList); + Assert.Equal("NullableInt", propertyList.First().PropertyName); + Assert.Equal(SortDirection.Ascending, propertyList.First().Direction); + } + + [Fact] + public void ToPropertyName_NestedProperty_ReturnsNestedPropertyName() + { + // Arrange + Expression> expression = x => x.Child.ChildName; + + // Act + var gridSort = GridSort.ByAscending(expression); + var propertyList = gridSort.ToPropertyList(ascending: true); + + // Assert + Assert.Single(propertyList); + Assert.Equal("Child.ChildName", propertyList.First().PropertyName); + Assert.Equal(SortDirection.Ascending, propertyList.First().Direction); + } + + [Fact] + public void ToPropertyName_NestedNullableProperty_ReturnsNestedPropertyName() + { + // Arrange + Expression> expression = x => x.Child.ChildNullableDate; + + // Act + var gridSort = GridSort.ByAscending(expression); + var propertyList = gridSort.ToPropertyList(ascending: true); + + // Assert + Assert.Single(propertyList); + Assert.Equal("Child.ChildNullableDate", propertyList.First().PropertyName); + Assert.Equal(SortDirection.Ascending, propertyList.First().Direction); + } + + [Fact] + public void ToPropertyName_DescendingSort_ReturnsCorrectDirection() + { + // Arrange + Expression> expression = x => x.NullableDate; + + // Act + var gridSort = GridSort.ByDescending(expression); + var propertyList = gridSort.ToPropertyList(ascending: true); + + // Assert + Assert.Single(propertyList); + Assert.Equal("NullableDate", propertyList.First().PropertyName); + Assert.Equal(SortDirection.Descending, propertyList.First().Direction); + } + + [Fact] + public void ToPropertyName_MultipleSort_ReturnsAllProperties() + { + // Arrange + Expression> firstExpression = x => x.Name; + Expression> secondExpression = x => x.NullableDate; + + // Act + var gridSort = GridSort.ByAscending(firstExpression) + .ThenDescending(secondExpression); + var propertyList = gridSort.ToPropertyList(ascending: true); + + // Assert + Assert.Equal(2, propertyList.Count); + + var firstProperty = propertyList.First(); + Assert.Equal("Name", firstProperty.PropertyName); + Assert.Equal(SortDirection.Ascending, firstProperty.Direction); + + var secondProperty = propertyList.Last(); + Assert.Equal("NullableDate", secondProperty.PropertyName); + Assert.Equal(SortDirection.Descending, secondProperty.Direction); + } + + [Fact] + public void ToPropertyName_InvalidExpression_ThrowsArgumentException() + { + // Arrange + Expression> invalidExpression = x => x.Name.ToUpper(CultureInfo.InvariantCulture); + + // Act & Assert + var gridSort = GridSort.ByAscending(invalidExpression); + var exception = Assert.Throws(() => gridSort.ToPropertyList(ascending: true)); + Assert.Contains("The supplied expression can't be represented as a property name for sorting", exception.Message); + } + + [Fact] + public void ToPropertyName_MethodCallExpression_ThrowsArgumentException() + { + // Arrange + Expression> invalidExpression = x => x.Name.Substring(0, 1); + + // Act & Assert + var gridSort = GridSort.ByAscending(invalidExpression); + var exception = Assert.Throws(() => gridSort.ToPropertyList(ascending: true)); + Assert.Contains("The supplied expression can't be represented as a property name for sorting", exception.Message); + } + + [Fact] + public void ToPropertyName_ConstantExpression_ThrowsArgumentException() + { + // Arrange + Expression> invalidExpression = x => "constant"; + + // Act & Assert + var gridSort = GridSort.ByAscending(invalidExpression); + var exception = Assert.Throws(() => gridSort.ToPropertyList(ascending: true)); + Assert.Contains("The supplied expression can't be represented as a property name for sorting", exception.Message); + } +} \ No newline at end of file diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/test/Microsoft.AspNetCore.Components.QuickGrid.Tests.csproj b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/test/Microsoft.AspNetCore.Components.QuickGrid.Tests.csproj new file mode 100644 index 000000000000..402e6a12a8f7 --- /dev/null +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/test/Microsoft.AspNetCore.Components.QuickGrid.Tests.csproj @@ -0,0 +1,18 @@ + + + + $(DefaultNetCoreTargetFramework) + Microsoft.AspNetCore.Components.QuickGrid.Tests + + + + + + + + + + + + + \ No newline at end of file