From 1b9f42c0875fb8e0c0ecf411c49a1fc6e4e6f360 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Thu, 5 Jun 2025 14:33:55 +0200 Subject: [PATCH 1/2] Add GroupBy method for Z.DynamicLinq.SystemTextJson and Z.DynamicLinq.NewtonsoftJson --- .../NewtonsoftJsonExtensions.cs | 49 ++++++++++- .../SystemTextJsonExtensions.cs | 49 ++++++++++- .../NewtonsoftJsonTests.cs | 82 +++++++++++++++++ .../SystemTextJsonTests.cs | 88 +++++++++++++++++++ 4 files changed, 266 insertions(+), 2 deletions(-) diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs b/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs index 89a6806d..7ca9d6e3 100644 --- a/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs @@ -2,6 +2,7 @@ using System.Linq.Dynamic.Core.NewtonsoftJson.Config; using System.Linq.Dynamic.Core.NewtonsoftJson.Extensions; using System.Linq.Dynamic.Core.Validation; +using JetBrains.Annotations; using Newtonsoft.Json.Linq; namespace System.Linq.Dynamic.Core.NewtonsoftJson; @@ -303,6 +304,42 @@ public static JToken First(this JArray source, string predicate, params object?[ } #endregion FirstOrDefault + #region GroupBy + /// + /// Groups the elements of a sequence according to a specified key string function + /// and creates a result value from each group and its key. + /// + /// A whose elements to group. + /// A string expression to specify the key for each element. + /// An object array that contains zero or more objects to insert into the predicate as parameters. Similar to the way String.Format formats strings. + /// A where each element represents a projection over a group and its key. + [PublicAPI] + public static JArray GroupBy(this JArray source, string keySelector, params object[]? args) + { + return GroupBy(source, NewtonsoftJsonParsingConfig.Default, keySelector, args); + } + + /// + /// Groups the elements of a sequence according to a specified key string function + /// and creates a result value from each group and its key. + /// + /// A whose elements to group. + /// The . + /// A string expression to specify the key for each element. + /// An object array that contains zero or more objects to insert into the predicate as parameters. Similar to the way String.Format formats strings. + /// A where each element represents a projection over a group and its key. + [PublicAPI] + public static JArray GroupBy(this JArray source, NewtonsoftJsonParsingConfig config, string keySelector, params object[]? args) + { + Check.NotNull(source); + Check.NotNull(config); + Check.NotNullOrEmpty(keySelector); + + var queryable = ToQueryable(source, config); + return ToJArray(() => queryable.GroupBy(config, keySelector, args)); + } + #endregion + #region Last /// /// Returns the last element of a sequence that satisfies a specified condition. @@ -813,7 +850,17 @@ private static JArray ToJArray(Func func) var array = new JArray(); foreach (var dynamicElement in func()) { - var element = dynamicElement is DynamicClass dynamicClass ? JObject.FromObject(dynamicClass) : dynamicElement; + var element = dynamicElement switch + { + IGrouping grouping => new JObject + { + [nameof(grouping.Key)] = JToken.FromObject(grouping.Key), + ["Values"] = ToJArray(grouping.AsQueryable) + }, + DynamicClass dynamicClass => JObject.FromObject(dynamicClass), + _ => dynamicElement + }; + array.Add(element); } diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs index a14a468e..4edce255 100644 --- a/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs @@ -5,6 +5,7 @@ using System.Linq.Dynamic.Core.SystemTextJson.Utils; using System.Linq.Dynamic.Core.Validation; using System.Text.Json; +using JetBrains.Annotations; namespace System.Linq.Dynamic.Core.SystemTextJson; @@ -371,6 +372,42 @@ public static JsonElement First(this JsonDocument source, string predicate, para } #endregion FirstOrDefault + #region GroupBy + /// + /// Groups the elements of a sequence according to a specified key string function + /// and creates a result value from each group and its key. + /// + /// A whose elements to group. + /// A string expression to specify the key for each element. + /// An object array that contains zero or more objects to insert into the predicate as parameters. Similar to the way String.Format formats strings. + /// A where each element represents a projection over a group and its key. + [PublicAPI] + public static JsonDocument GroupBy(this JsonDocument source, string keySelector, params object[]? args) + { + return GroupBy(source, SystemTextJsonParsingConfig.Default, keySelector, args); + } + + /// + /// Groups the elements of a sequence according to a specified key string function + /// and creates a result value from each group and its key. + /// + /// A whose elements to group. + /// The . + /// A string expression to specify the key for each element. + /// An object array that contains zero or more objects to insert into the predicate as parameters. Similar to the way String.Format formats strings. + /// A where each element represents a projection over a group and its key. + [PublicAPI] + public static JsonDocument GroupBy(this JsonDocument source, SystemTextJsonParsingConfig config, string keySelector, params object[]? args) + { + Check.NotNull(source); + Check.NotNull(config); + Check.NotNullOrEmpty(keySelector); + + var queryable = ToQueryable(source, config); + return ToJsonDocumentArray(() => queryable.GroupBy(config, keySelector, args)); + } + #endregion + #region Last /// /// Returns the last element of a sequence. @@ -1037,7 +1074,17 @@ private static JsonDocument ToJsonDocumentArray(Func func) var array = new List(); foreach (var dynamicElement in func()) { - array.Add(ToJsonElement(dynamicElement)); + var element = dynamicElement switch + { + IGrouping grouping => ToJsonElement(new + { + Key = ToJsonElement(grouping.Key), + Values = ToJsonDocumentArray(grouping.AsQueryable) + }), + _ => ToJsonElement(dynamicElement) + }; + + array.Add(element); } return JsonDocumentUtils.FromObject(array); diff --git a/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs b/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs index dafa0373..2e94a212 100644 --- a/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs +++ b/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs @@ -168,6 +168,88 @@ public void FirstOrDefault() _source.FirstOrDefault("Age > 999").Should().BeNull(); } + [Fact] + public void GroupBySimpleKeySelector() + { + // Arrange + var json = + """ + [ + { + "Name": "Mr. Test Smith", + "Type": "PAY", + "Something": { + "Field1": "Test1", + "Field2": "Test2" + } + }, + { + "Name": "Mr. Test Smith", + "Type": "DISPATCH", + "Something": { + "Field1": "Test1", + "Field2": "Test2" + } + }, + { + "Name": "Different Name", + "Type": "PAY", + "Something": { + "Field1": "Test3", + "Field2": "Test4" + } + } + ] + """; + var source = JArray.Parse(json); + + // Act + var resultAsJson = source.GroupBy("Type").ToString(); + + // Assert + var expected = + """ + [ + { + "Key": "PAY", + "Values": [ + { + "Name": "Mr. Test Smith", + "Type": "PAY", + "Something": { + "Field1": "Test1", + "Field2": "Test2" + } + }, + { + "Name": "Different Name", + "Type": "PAY", + "Something": { + "Field1": "Test3", + "Field2": "Test4" + } + } + ] + }, + { + "Key": "DISPATCH", + "Values": [ + { + "Name": "Mr. Test Smith", + "Type": "DISPATCH", + "Something": { + "Field1": "Test1", + "Field2": "Test2" + } + } + ] + } + ] + """; + + resultAsJson.Should().Be(expected); + } + [Fact] public void Last() { diff --git a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs index 7041ad7c..94497dda 100644 --- a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs +++ b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs @@ -6,6 +6,11 @@ namespace System.Linq.Dynamic.Core.SystemTextJson.Tests; public class SystemTextJsonTests { + private static readonly JsonSerializerOptions options = new JsonSerializerOptions + { + WriteIndented = true + }; + private const string ExampleJsonObjectArray = """ [ @@ -174,6 +179,89 @@ public void FirstOrDefault() _source.FirstOrDefault("Age > 999").Should().BeNull(); } + [Fact] + public void GroupBySimpleKeySelector() + { + // Arrange + var json = + """ + [ + { + "Name": "Mr. Test Smith", + "Type": "PAY", + "Something": { + "Field1": "Test1", + "Field2": "Test2" + } + }, + { + "Name": "Mr. Test Smith", + "Type": "DISPATCH", + "Something": { + "Field1": "Test1", + "Field2": "Test2" + } + }, + { + "Name": "Different Name", + "Type": "PAY", + "Something": { + "Field1": "Test3", + "Field2": "Test4" + } + } + ] + """; + var source = JsonDocument.Parse(json); + + // Act + var result = source.GroupBy("Type"); + var resultAsJson = JsonSerializer.Serialize(result, options); + + // Assert + var expected = + """ + [ + { + "Key": "PAY", + "Values": [ + { + "Name": "Mr. Test Smith", + "Type": "PAY", + "Something": { + "Field1": "Test1", + "Field2": "Test2" + } + }, + { + "Name": "Different Name", + "Type": "PAY", + "Something": { + "Field1": "Test3", + "Field2": "Test4" + } + } + ] + }, + { + "Key": "DISPATCH", + "Values": [ + { + "Name": "Mr. Test Smith", + "Type": "DISPATCH", + "Something": { + "Field1": "Test1", + "Field2": "Test2" + } + } + ] + } + ] + """; + + resultAsJson.Should().Be(expected); + } + [Fact] public void Last() { From 1c5cb7827725f388fcf7c4a9ca550e5f02387ef9 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Fri, 6 Jun 2025 10:22:39 +0200 Subject: [PATCH 2/2] . --- .../SystemTextJsonExtensions.cs | 2 +- .../SystemTextJsonTests.cs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs index 4edce255..8d1671a0 100644 --- a/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs @@ -1079,7 +1079,7 @@ private static JsonDocument ToJsonDocumentArray(Func func) IGrouping grouping => ToJsonElement(new { Key = ToJsonElement(grouping.Key), - Values = ToJsonDocumentArray(grouping.AsQueryable) + Values = ToJsonDocumentArray(grouping.AsQueryable).RootElement }), _ => ToJsonElement(dynamicElement) }; diff --git a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs index 94497dda..f5dee221 100644 --- a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs +++ b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs @@ -6,7 +6,7 @@ namespace System.Linq.Dynamic.Core.SystemTextJson.Tests; public class SystemTextJsonTests { - private static readonly JsonSerializerOptions options = new JsonSerializerOptions + private static readonly JsonSerializerOptions _options = new() { WriteIndented = true }; @@ -147,7 +147,7 @@ public void Distinct() } ] """; - var source = JsonDocument.Parse(json); + using var source = JsonDocument.Parse(json); // Act var result = source.Select("Name").Distinct(); @@ -212,11 +212,11 @@ public void GroupBySimpleKeySelector() } ] """; - var source = JsonDocument.Parse(json); + using var source = JsonDocument.Parse(json); // Act var result = source.GroupBy("Type"); - var resultAsJson = JsonSerializer.Serialize(result, options); + var resultAsJson = JsonSerializer.Serialize(result, _options); // Assert var expected = @@ -353,7 +353,7 @@ public void OrderBy_Multiple() } ] """; - var source = JsonDocument.Parse(json); + using var source = JsonDocument.Parse(json); // Act var result = source.OrderBy("Age, Name").Select("Name"); @@ -367,7 +367,7 @@ public void OrderBy_Multiple() public void Page() { var json = "[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]"; - var source = JsonDocument.Parse(json); + using var source = JsonDocument.Parse(json); // Act var result = source.Page(2, 3); @@ -381,7 +381,7 @@ public void Page() public void PageResult() { var json = "[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]"; - var source = JsonDocument.Parse(json); + using var source = JsonDocument.Parse(json); // Act var pagedResult = source.PageResult(2, 3); @@ -427,7 +427,7 @@ public void SelectMany() ] }] """; - var source = JsonDocument.Parse(json); + using var source = JsonDocument.Parse(json); // Act var result = source