Skip to content

Commit 1c16922

Browse files
committed
Merge branch 'master' into stef-PredefinedMethodsHelper
2 parents 51df80e + 19da876 commit 1c16922

File tree

16 files changed

+446
-74
lines changed

16 files changed

+446
-74
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
# v1.6.6 (11 June 2025)
2+
- [#929](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/929) - Add GroupBy method for Z.DynamicLinq.SystemTextJson and Z.DynamicLinq.NewtonsoftJson contributed by [StefH](https://github.com/StefH)
3+
- [#932](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/932) - Fix "in" for nullable Enums [bug] contributed by [StefH](https://github.com/StefH)
4+
- [#931](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/931) - Syntax IN dont work with nullable Enums [bug]
5+
6+
# v1.6.5 (28 May 2025)
7+
- [#905](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/905) - Fix: Add Fallback in ExpressionPromoter to Handle Cache Cleanup in ConstantExpressionHelper [bug] contributed by [RenanCarlosPereira](https://github.com/RenanCarlosPereira)
8+
- [#904](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/904) - Race Condition in ConstantExpressionHelper Causing Parsing Failures [bug]
9+
110
# v1.6.4 (19 May 2025)
211
- [#915](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/915) - Add support for "not in" and "not_in" [feature] contributed by [StefH](https://github.com/StefH)
312
- [#923](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/923) - Fix MethodFinder TryFindAggregateMethod to support array [bug] contributed by [StefH](https://github.com/StefH)

Generate-ReleaseNotes.bat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
rem https://github.com/StefH/GitHubReleaseNotes
22

3-
SET version=v1.6.4
3+
SET version=v1.6.6
44

55
GitHubReleaseNotes --output CHANGELOG.md --exclude-labels known_issue out_of_scope not_planned invalid question documentation wontfix environment duplicate --language en --version %version% --token %GH_TOKEN%

README.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,8 @@
1-
### Library Powered By
2-
3-
This library is powered by [Entity Framework Extensions](https://entityframework-extensions.net/?z=github&y=system.linq.dynamic.core)
4-
5-
<a href="https://entityframework-extensions.net/?z=github&y=system.linq.dynamic.core">
6-
<kbd>
7-
<img src="https://zzzprojects.github.io/images/logo/entityframework-extensions-pub.jpg" alt="Entity Framework Extensions" />
8-
</kbd>
9-
</a>
10-
11-
---
12-
131
# System.Linq.Dynamic.Core
142
This is a **.NET Core / Standard port** of the Microsoft assembly for the .Net 4.0 Dynamic language functionality.
153

4+
---
5+
166
## Overview
177
With this library it's possible to write Dynamic LINQ queries (string based) on an `IQueryable`:
188
``` c#
@@ -32,6 +22,18 @@ db.Customers.WhereInterpolated($"City == {cityName} and Orders.Count >= {c}");
3222

3323
---
3424

25+
## Sponsors
26+
27+
ZZZ Projects owns and maintains **System.Linq.Dynamic.Core** as part of our [mission](https://zzzprojects.com/mission) to add value to the .NET community
28+
29+
Through [Entity Framework Extensions](https://entityframework-extensions.net/?utm_source=zzzprojects&utm_medium=systemlinqdynamiccore) and [Dapper Plus](https://dapper-plus.net/?utm_source=zzzprojects&utm_medium=systemlinqdynamiccore), we actively sponsor and help key open-source libraries grow.
30+
31+
[![Entity Framework Extensions](https://raw.githubusercontent.com/zzzprojects/System.Linq.Dynamic.Core/master/entity-framework-extensions-sponsor.png)](https://entityframework-extensions.net/bulk-insert?utm_source=zzzprojects&utm_medium=systemlinqdynamiccore)
32+
33+
[![Dapper Plus](https://raw.githubusercontent.com/zzzprojects/System.Linq.Dynamic.Core/master/dapper-plus-sponsor.png)](https://dapper-plus.net/bulk-insert?utm_source=zzzprojects&utm_medium=systemlinqdynamiccore)
34+
35+
---
36+
3537
## :exclamation: Breaking changes
3638

3739
### v1.3.0

dapper-plus-sponsor.png

10.7 KB
Loading
10.5 KB
Loading

src/System.Linq.Dynamic.Core.NewtonsoftJson/NewtonsoftJsonExtensions.cs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Linq.Dynamic.Core.NewtonsoftJson.Config;
33
using System.Linq.Dynamic.Core.NewtonsoftJson.Extensions;
44
using System.Linq.Dynamic.Core.Validation;
5+
using JetBrains.Annotations;
56
using Newtonsoft.Json.Linq;
67

78
namespace System.Linq.Dynamic.Core.NewtonsoftJson;
@@ -303,6 +304,42 @@ public static JToken First(this JArray source, string predicate, params object?[
303304
}
304305
#endregion FirstOrDefault
305306

307+
#region GroupBy
308+
/// <summary>
309+
/// Groups the elements of a sequence according to a specified key string function
310+
/// and creates a result value from each group and its key.
311+
/// </summary>
312+
/// <param name="source">A <see cref="JArray"/> whose elements to group.</param>
313+
/// <param name="keySelector">A string expression to specify the key for each element.</param>
314+
/// <param name="args">An object array that contains zero or more objects to insert into the predicate as parameters. Similar to the way String.Format formats strings.</param>
315+
/// <returns>A <see cref="JArray"/> where each element represents a projection over a group and its key.</returns>
316+
[PublicAPI]
317+
public static JArray GroupBy(this JArray source, string keySelector, params object[]? args)
318+
{
319+
return GroupBy(source, NewtonsoftJsonParsingConfig.Default, keySelector, args);
320+
}
321+
322+
/// <summary>
323+
/// Groups the elements of a sequence according to a specified key string function
324+
/// and creates a result value from each group and its key.
325+
/// </summary>
326+
/// <param name="source">A <see cref="JArray"/> whose elements to group.</param>
327+
/// <param name="config">The <see cref="NewtonsoftJsonParsingConfig"/>.</param>
328+
/// <param name="keySelector">A string expression to specify the key for each element.</param>
329+
/// <param name="args">An object array that contains zero or more objects to insert into the predicate as parameters. Similar to the way String.Format formats strings.</param>
330+
/// <returns>A <see cref="JArray"/> where each element represents a projection over a group and its key.</returns>
331+
[PublicAPI]
332+
public static JArray GroupBy(this JArray source, NewtonsoftJsonParsingConfig config, string keySelector, params object[]? args)
333+
{
334+
Check.NotNull(source);
335+
Check.NotNull(config);
336+
Check.NotNullOrEmpty(keySelector);
337+
338+
var queryable = ToQueryable(source, config);
339+
return ToJArray(() => queryable.GroupBy(config, keySelector, args));
340+
}
341+
#endregion
342+
306343
#region Last
307344
/// <summary>
308345
/// Returns the last element of a sequence that satisfies a specified condition.
@@ -813,7 +850,17 @@ private static JArray ToJArray(Func<IQueryable> func)
813850
var array = new JArray();
814851
foreach (var dynamicElement in func())
815852
{
816-
var element = dynamicElement is DynamicClass dynamicClass ? JObject.FromObject(dynamicClass) : dynamicElement;
853+
var element = dynamicElement switch
854+
{
855+
IGrouping<object, object> grouping => new JObject
856+
{
857+
[nameof(grouping.Key)] = JToken.FromObject(grouping.Key),
858+
["Values"] = ToJArray(grouping.AsQueryable)
859+
},
860+
DynamicClass dynamicClass => JObject.FromObject(dynamicClass),
861+
_ => dynamicElement
862+
};
863+
817864
array.Add(element);
818865
}
819866

src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Linq.Dynamic.Core.SystemTextJson.Utils;
66
using System.Linq.Dynamic.Core.Validation;
77
using System.Text.Json;
8+
using JetBrains.Annotations;
89

910
namespace System.Linq.Dynamic.Core.SystemTextJson;
1011

@@ -371,6 +372,42 @@ public static JsonElement First(this JsonDocument source, string predicate, para
371372
}
372373
#endregion FirstOrDefault
373374

375+
#region GroupBy
376+
/// <summary>
377+
/// Groups the elements of a sequence according to a specified key string function
378+
/// and creates a result value from each group and its key.
379+
/// </summary>
380+
/// <param name="source">A <see cref="JsonDocument"/> whose elements to group.</param>
381+
/// <param name="keySelector">A string expression to specify the key for each element.</param>
382+
/// <param name="args">An object array that contains zero or more objects to insert into the predicate as parameters. Similar to the way String.Format formats strings.</param>
383+
/// <returns>A <see cref="JsonDocument"/> where each element represents a projection over a group and its key.</returns>
384+
[PublicAPI]
385+
public static JsonDocument GroupBy(this JsonDocument source, string keySelector, params object[]? args)
386+
{
387+
return GroupBy(source, SystemTextJsonParsingConfig.Default, keySelector, args);
388+
}
389+
390+
/// <summary>
391+
/// Groups the elements of a sequence according to a specified key string function
392+
/// and creates a result value from each group and its key.
393+
/// </summary>
394+
/// <param name="source">A <see cref="JsonDocument"/> whose elements to group.</param>
395+
/// <param name="config">The <see cref="SystemTextJsonParsingConfig"/>.</param>
396+
/// <param name="keySelector">A string expression to specify the key for each element.</param>
397+
/// <param name="args">An object array that contains zero or more objects to insert into the predicate as parameters. Similar to the way String.Format formats strings.</param>
398+
/// <returns>A <see cref="JsonDocument"/> where each element represents a projection over a group and its key.</returns>
399+
[PublicAPI]
400+
public static JsonDocument GroupBy(this JsonDocument source, SystemTextJsonParsingConfig config, string keySelector, params object[]? args)
401+
{
402+
Check.NotNull(source);
403+
Check.NotNull(config);
404+
Check.NotNullOrEmpty(keySelector);
405+
406+
var queryable = ToQueryable(source, config);
407+
return ToJsonDocumentArray(() => queryable.GroupBy(config, keySelector, args));
408+
}
409+
#endregion
410+
374411
#region Last
375412
/// <summary>
376413
/// Returns the last element of a sequence.
@@ -1037,7 +1074,17 @@ private static JsonDocument ToJsonDocumentArray(Func<IQueryable> func)
10371074
var array = new List<object?>();
10381075
foreach (var dynamicElement in func())
10391076
{
1040-
array.Add(ToJsonElement(dynamicElement));
1077+
var element = dynamicElement switch
1078+
{
1079+
IGrouping<object, object> grouping => ToJsonElement(new
1080+
{
1081+
Key = ToJsonElement(grouping.Key),
1082+
Values = ToJsonDocumentArray(grouping.AsQueryable).RootElement
1083+
}),
1084+
_ => ToJsonElement(dynamicElement)
1085+
};
1086+
1087+
array.Add(element);
10411088
}
10421089

10431090
return JsonDocumentUtils.FromObject(array);

src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,8 +376,8 @@ private Expression ParseIn()
376376
// we need to parse unary expressions because otherwise 'in' clause will fail in use cases like 'in (-1, -1)' or 'in (!true)'
377377
Expression right = ParseUnary();
378378

379-
// if the identifier is an Enum, try to convert the right-side also to an Enum.
380-
if (left.Type.GetTypeInfo().IsEnum)
379+
// if the identifier is an Enum (or nullable Enum), try to convert the right-side also to an Enum.
380+
if (TypeHelper.GetNonNullableType(left.Type).GetTypeInfo().IsEnum)
381381
{
382382
if (right is ConstantExpression constantExprRight)
383383
{

src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ public ExpressionPromoter(ParsingConfig config)
5050
}
5151
else
5252
{
53-
if (_constantExpressionHelper.TryGetText(ce, out var text))
53+
if (!_constantExpressionHelper.TryGetText(ce, out var text))
54+
{
55+
text = ce.Value?.ToString();
56+
}
57+
58+
if (text != null)
5459
{
5560
Type target = TypeHelper.GetNonNullableType(type);
5661
object? value = null;
@@ -67,7 +72,7 @@ public ExpressionPromoter(ParsingConfig config)
6772
// Make sure an enum value stays an enum value
6873
if (target.IsEnum)
6974
{
70-
value = Enum.ToObject(target, value!);
75+
TypeHelper.TryParseEnum(text, target, out value);
7176
}
7277
break;
7378

test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,88 @@ public void FirstOrDefault()
168168
_source.FirstOrDefault("Age > 999").Should().BeNull();
169169
}
170170

171+
[Fact]
172+
public void GroupBySimpleKeySelector()
173+
{
174+
// Arrange
175+
var json =
176+
"""
177+
[
178+
{
179+
"Name": "Mr. Test Smith",
180+
"Type": "PAY",
181+
"Something": {
182+
"Field1": "Test1",
183+
"Field2": "Test2"
184+
}
185+
},
186+
{
187+
"Name": "Mr. Test Smith",
188+
"Type": "DISPATCH",
189+
"Something": {
190+
"Field1": "Test1",
191+
"Field2": "Test2"
192+
}
193+
},
194+
{
195+
"Name": "Different Name",
196+
"Type": "PAY",
197+
"Something": {
198+
"Field1": "Test3",
199+
"Field2": "Test4"
200+
}
201+
}
202+
]
203+
""";
204+
var source = JArray.Parse(json);
205+
206+
// Act
207+
var resultAsJson = source.GroupBy("Type").ToString();
208+
209+
// Assert
210+
var expected =
211+
"""
212+
[
213+
{
214+
"Key": "PAY",
215+
"Values": [
216+
{
217+
"Name": "Mr. Test Smith",
218+
"Type": "PAY",
219+
"Something": {
220+
"Field1": "Test1",
221+
"Field2": "Test2"
222+
}
223+
},
224+
{
225+
"Name": "Different Name",
226+
"Type": "PAY",
227+
"Something": {
228+
"Field1": "Test3",
229+
"Field2": "Test4"
230+
}
231+
}
232+
]
233+
},
234+
{
235+
"Key": "DISPATCH",
236+
"Values": [
237+
{
238+
"Name": "Mr. Test Smith",
239+
"Type": "DISPATCH",
240+
"Something": {
241+
"Field1": "Test1",
242+
"Field2": "Test2"
243+
}
244+
}
245+
]
246+
}
247+
]
248+
""";
249+
250+
resultAsJson.Should().Be(expected);
251+
}
252+
171253
[Fact]
172254
public void Last()
173255
{

0 commit comments

Comments
 (0)