From b8740cacf750c181f72d0be9f2a212d5c9693cdd Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Fri, 26 Sep 2025 16:44:03 +0200 Subject: [PATCH 1/3] Fix GroupByMany using composite key and normal key --- .../DynamicQueryableExtensions.cs | 21 ++- .../Parser/ExpressionHelper.cs | 8 +- .../QueryableTests.GroupByMany.cs | 121 +++++++++++------- 3 files changed, 92 insertions(+), 58 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs index c5d52287..b3ff3092 100644 --- a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs +++ b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs @@ -871,12 +871,10 @@ public static IEnumerable GroupByMany(this IEnumerable>(keySelectors.Length); - - bool createParameterCtor = true; foreach (var selector in keySelectors) { - LambdaExpression l = DynamicExpressionParser.ParseLambda(config, createParameterCtor, typeof(TElement), typeof(object), selector); - selectors.Add((Func)l.Compile()); + var lambdaExpression = DynamicExpressionParser.ParseLambda(config, createParameterCtor: true, typeof(TElement), null, selector); + selectors.Add((Func)EnsureLambdaExpressionReturnsObject(lambdaExpression).Compile()); } return GroupByManyInternal(source, selectors.ToArray(), 0); @@ -913,8 +911,9 @@ private static IEnumerable GroupByManyInternal(IEnumerabl var selector = keySelectors[currentSelector]; - var result = source.GroupBy(selector).Select( - g => new GroupResult + var result = source + .GroupBy(selector) + .Select(g => new GroupResult { Key = g.Key, Count = g.Count(), @@ -2847,6 +2846,16 @@ private static TResult ConvertResultIfNeeded(object result) return (TResult?)Convert.ChangeType(result, typeof(TResult))!; } + + private static LambdaExpression EnsureLambdaExpressionReturnsObject(LambdaExpression lambdaExpression) + { + if (!lambdaExpression.GetReturnType().GetTypeInfo().IsSubclassOf(typeof(DynamicClass))) + { + return Expression.Lambda(Expression.Convert(lambdaExpression.Body, typeof(object)), lambdaExpression.Parameters.ToArray()); + } + + return lambdaExpression; + } #endregion Private Helpers } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs index 7b0167d8..7d72ad61 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs @@ -45,7 +45,7 @@ public bool TryUnwrapAsConstantExpression(Expression? expression, [NotNu return true; } - value = default; + value = null; return false; } @@ -53,7 +53,7 @@ public bool TryUnwrapAsConstantExpression(Expression? expression, [NotNullWhen(t { if (!_parsingConfig.UseParameterizedNamesInDynamicQuery || expression is not MemberExpression memberExpression) { - value = default; + value = null; return false; } @@ -68,7 +68,7 @@ public bool TryUnwrapAsConstantExpression(Expression? expression, [NotNullWhen(t return true; } - value = default; + value = null; return false; } @@ -531,6 +531,6 @@ private static object[] ConvertIfIEnumerableHasValues(IEnumerable? input) return input.Cast().ToArray(); } - return new object[0]; + return []; } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.GroupByMany.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.GroupByMany.cs index d5920a67..4c15053f 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.GroupByMany.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.GroupByMany.cs @@ -1,57 +1,82 @@ using System.Collections.Generic; +using FluentAssertions; using NFluent; using Xunit; -namespace System.Linq.Dynamic.Core.Tests +namespace System.Linq.Dynamic.Core.Tests; + +public partial class QueryableTests { - public partial class QueryableTests + [Fact] + public void GroupByMany_Dynamic_LambdaExpressions() + { + var lst = new List> + { + new(1, 1, 1), + new(1, 1, 2), + new(1, 1, 3), + new(2, 2, 4), + new(2, 2, 5), + new(2, 2, 6), + new(2, 3, 7) + }; + + var sel = lst.GroupByMany(x => x.Item1, x => x.Item2).ToArray(); + + Assert.Equal(2, sel.Length); + Assert.Single(sel.First().Subgroups); + Assert.Equal(2, sel.Skip(1).First().Subgroups.Count()); + } + + [Fact] + public void GroupByMany_Dynamic_StringExpressions() { - [Fact] - public void GroupByMany_Dynamic_LambdaExpressions() + var lst = new List> { - var lst = new List> - { - new Tuple(1, 1, 1), - new Tuple(1, 1, 2), - new Tuple(1, 1, 3), - new Tuple(2, 2, 4), - new Tuple(2, 2, 5), - new Tuple(2, 2, 6), - new Tuple(2, 3, 7) - }; - - var sel = lst.AsQueryable().GroupByMany(x => x.Item1, x => x.Item2); - - Assert.Equal(2, sel.Count()); - Assert.Single(sel.First().Subgroups); - Assert.Equal(2, sel.Skip(1).First().Subgroups.Count()); - } - - [Fact] - public void GroupByMany_Dynamic_StringExpressions() + new(1, 1, 1), + new(1, 1, 2), + new(1, 1, 3), + new(2, 2, 4), + new(2, 2, 5), + new(2, 2, 6), + new(2, 3, 7) + }; + + var sel = lst.GroupByMany("Item1", "Item2").ToList(); + + Check.That(sel.Count).Equals(2); + + var firstGroupResult = sel.First(); + Check.That(firstGroupResult.ToString()).Equals("1 (3)"); + Check.That(firstGroupResult.Subgroups.Count()).Equals(1); + + var skippedGroupResult = sel.Skip(1).First(); + Check.That(skippedGroupResult.ToString()).Equals("2 (4)"); + Check.That(skippedGroupResult.Subgroups.Count()).Equals(2); + } + + [Fact] + public void GroupByMany_Dynamic_CompositeKey() + { + // Arrange + var data = new[] { - var lst = new List> - { - new Tuple(1, 1, 1), - new Tuple(1, 1, 2), - new Tuple(1, 1, 3), - new Tuple(2, 2, 4), - new Tuple(2, 2, 5), - new Tuple(2, 2, 6), - new Tuple(2, 3, 7) - }; - - var sel = lst.AsQueryable().GroupByMany("Item1", "Item2").ToList(); - - Check.That(sel.Count).Equals(2); - - var firstGroupResult = sel.First(); - Check.That(firstGroupResult.ToString()).Equals("1 (3)"); - Check.That(firstGroupResult.Subgroups.Count()).Equals(1); - - var skippedGroupResult = sel.Skip(1).First(); - Check.That(skippedGroupResult.ToString()).Equals("2 (4)"); - Check.That(skippedGroupResult.Subgroups.Count()).Equals(2); - } + new { MachineId = 1, Machine = new { Id = 1, Name = "A" } }, + new { MachineId = 1, Machine = new { Id = 1, Name = "A" } }, + new { MachineId = 2, Machine = new { Id = 2, Name = "B" } } + }; + + // Act + var normalResult = data + .GroupByMany(d => new { d.MachineId, d.Machine.Name }, a => a.Machine.Id) + .Select(x => x.ToString()) + .ToList(); + var result = data + .GroupByMany("new (MachineId, Machine.Name)", "Machine.Id") + .Select(x => x.ToString()) + .ToList(); + + // Assert + result.Should().BeEquivalentTo(normalResult); } -} +} \ No newline at end of file From 8f23c998314908bd00b1686429c490bd91b8c62f Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 27 Sep 2025 06:56:07 +0200 Subject: [PATCH 2/3] . --- .../QueryableTests.GroupByMany.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.GroupByMany.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.GroupByMany.cs index 4c15053f..d6624d35 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.GroupByMany.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.GroupByMany.cs @@ -76,7 +76,7 @@ public void GroupByMany_Dynamic_CompositeKey() .Select(x => x.ToString()) .ToList(); - // Assert + // Assert result.Should().BeEquivalentTo(normalResult); } } \ No newline at end of file From dec6ce78dd4367f09ed85ed108ef705bc6e17870 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 27 Sep 2025 06:58:59 +0200 Subject: [PATCH 3/3] readme --- README.md | 2 +- .../QueryableTests.GroupByMany.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a1c7b643..7fc38608 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Or provide a list of additional types in the [DefaultDynamicLinqCustomTypeProvid |   **Issues** | [![GitHub issues](https://img.shields.io/github/issues/StefH/System.Linq.Dynamic.Core.svg)](https://github.com/StefH/System.Linq.Dynamic.Core/issues) | | | | | ***Quality*** |   | -|   **CI Workflow** | ![CI Workflow](https://github.com/zzzprojects/System.Linq.Dynamic.Core/actions/workflows/ci.yml/badge.svg) | +|   **CI Workflow** | [![CI Workflow](https://github.com/zzzprojects/System.Linq.Dynamic.Core/actions/workflows/ci.yml/badge.svg)](https://github.com/zzzprojects/System.Linq.Dynamic.Core/actions/workflows/ci.yml) | |   **SonarCloud** | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=zzzprojects_System.Linq.Dynamic.Core&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=zzzprojects_System.Linq.Dynamic.Core) | | | | ***NuGet*** |   | diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.GroupByMany.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.GroupByMany.cs index d6624d35..4c15053f 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.GroupByMany.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.GroupByMany.cs @@ -76,7 +76,7 @@ public void GroupByMany_Dynamic_CompositeKey() .Select(x => x.ToString()) .ToList(); - // Assert + // Assert result.Should().BeEquivalentTo(normalResult); } } \ No newline at end of file