From 265057b939b62cfc50bb7e77788b1636d0ac32ad Mon Sep 17 00:00:00 2001 From: Luiz Fernando Bicalho Date: Mon, 26 May 2025 17:35:27 -0300 Subject: [PATCH 01/16] Upgrade to .NET 8.0 and update testing framework - Updated project files to target .NET 8.0, removing older framework references. - Replaced `JsonSerializer` with `JsonConvert` for deserialization in test files. - Changed assertions from `Assert` to `ClassicAssert` in tests. - Removed .NET Core 3.1 conditional compilation from `CustomJsonSerializerOptions.cs`. - Updated NUnit and Entity Framework Core package versions for compatibility. --- .../KendoNET.DynamicLinq.ConsoleApp.csproj | 10 +- .../AggregatorTest.cs | 42 +++--- .../AggregatorTestSystem.cs | 120 ++++++++++++++++++ .../CustomJsonSerializerOptions.cs | 4 +- test/KendoNET.DynamicLinq.Test/FilterTest.cs | 52 +++----- .../FilterTestSystem.cs | 95 ++++++++++++++ test/KendoNET.DynamicLinq.Test/GroupTest.cs | 26 ++-- .../GroupTestSystem.cs | 37 ++++++ .../KendoNET.DynamicLinq.Test.csproj | 16 +-- 9 files changed, 302 insertions(+), 100 deletions(-) create mode 100644 test/KendoNET.DynamicLinq.Test/AggregatorTestSystem.cs create mode 100644 test/KendoNET.DynamicLinq.Test/FilterTestSystem.cs create mode 100644 test/KendoNET.DynamicLinq.Test/GroupTestSystem.cs diff --git a/test/KendoNET.DynamicLinq.ConsoleApp/KendoNET.DynamicLinq.ConsoleApp.csproj b/test/KendoNET.DynamicLinq.ConsoleApp/KendoNET.DynamicLinq.ConsoleApp.csproj index e4c34a4..fe5bf90 100644 --- a/test/KendoNET.DynamicLinq.ConsoleApp/KendoNET.DynamicLinq.ConsoleApp.csproj +++ b/test/KendoNET.DynamicLinq.ConsoleApp/KendoNET.DynamicLinq.ConsoleApp.csproj @@ -1,17 +1,11 @@ - + Exe - netcoreapp1.0;netcoreapp2.1;netcoreapp3.1 + net8.0 KendoNET.DynamicLinq.ConsoleApp - - - - - - diff --git a/test/KendoNET.DynamicLinq.Test/AggregatorTest.cs b/test/KendoNET.DynamicLinq.Test/AggregatorTest.cs index 3456fe7..e69d781 100644 --- a/test/KendoNET.DynamicLinq.Test/AggregatorTest.cs +++ b/test/KendoNET.DynamicLinq.Test/AggregatorTest.cs @@ -1,13 +1,9 @@ -using NUnit.Framework; -using System.Collections.Generic; +using System.Collections.Generic; using KendoNET.DynamicLinq.Test.Data; - -#if NETCOREAPP3_1 -using System.Text.Json; - -#else using Newtonsoft.Json; -#endif +using NUnit.Framework; +using NUnit.Framework.Legacy; + namespace KendoNET.DynamicLinq.Test { @@ -16,19 +12,15 @@ public class AggregatorTest { private MockContext _dbContext; -#if NETCOREAPP3_1 - private static JsonSerializerOptions jsonSerializerOptions = CustomJsonSerializerOptions.DefaultOptions; -#endif + public static IEnumerable DataSourceRequestWithAggregateSalarySum { get { -#if NETCOREAPP3_1 - yield return JsonSerializer.Deserialize("{\"take\":10,\"skip\":0,\"aggregate\":[{\"field\":\"Salary\",\"aggregate\":\"sum\"}]}", jsonSerializerOptions); -#else + yield return JsonConvert.DeserializeObject("{\"take\":10,\"skip\":0,\"aggregate\":[{\"field\":\"Salary\",\"aggregate\":\"sum\"}]}"); -#endif + } } @@ -36,14 +28,10 @@ public static IEnumerable DataSourceRequestWithManyAggregates { get { -#if NETCOREAPP3_1 - yield return JsonSerializer.Deserialize( - "{\"take\":10,\"skip\":0,\"aggregate\":[{\"field\":\"Salary\",\"aggregate\":\"sum\"},{\"field\":\"Salary\",\"aggregate\":\"average\"},{\"field\":\"Number\",\"aggregate\":\"max\"}]}", - jsonSerializerOptions); -#else + yield return JsonConvert.DeserializeObject( "{\"take\":10,\"skip\":0,\"aggregate\":[{\"field\":\"Salary\",\"aggregate\":\"sum\"},{\"field\":\"Salary\",\"aggregate\":\"average\"},{\"field\":\"Number\",\"aggregate\":\"max\"}]}"); -#endif + } } @@ -66,7 +54,7 @@ public void InputParameter_DecimalSum_CheckResultObjectString() }, null); object expectedObject = "{ Salary = { sum = 14850 } }"; - Assert.AreEqual(expectedObject, result.Aggregates.ToString()); + ClassicAssert.AreEqual(expectedObject, result.Aggregates.ToString()); } [TestCaseSource(nameof(DataSourceRequestWithAggregateSalarySum))] @@ -75,7 +63,7 @@ public void InputDataSourceRequest_DecimalSum_CheckResultObjectString(DataSource var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(dataSourceRequest); object expectedObject = "{ Salary = { sum = 14850 } }"; - Assert.AreEqual(expectedObject, result.Aggregates.ToString()); + ClassicAssert.AreEqual(expectedObject, result.Aggregates.ToString()); } [TestCaseSource(nameof(DataSourceRequestWithAggregateSalarySum))] @@ -86,10 +74,10 @@ public void InputDataSourceRequest_DecimalSum_CheckResultSum(DataSourceRequest d var salarySum = salaryAggregates?.GetType().GetProperty("sum")?.GetValue(salaryAggregates, null); const decimal expectedSalarySum = 14850; - Assert.AreEqual(expectedSalarySum, salarySum); + ClassicAssert.AreEqual(expectedSalarySum, salarySum); const decimal incorrectSalarySum = 9999; - Assert.AreNotEqual(incorrectSalarySum, salarySum); + ClassicAssert.AreNotEqual(incorrectSalarySum, salarySum); } [Test] @@ -115,7 +103,7 @@ public void InputParameter_ManyAggregators_CheckResultObjectString() }, null); object expectedObject = "{ Salary = { sum = 14850, average = 2970 }, Number = { max = 6 } }"; - Assert.AreEqual(expectedObject, result.Aggregates.ToString()); + ClassicAssert.AreEqual(expectedObject, result.Aggregates.ToString()); } [TestCaseSource(nameof(DataSourceRequestWithManyAggregates))] @@ -124,7 +112,7 @@ public void InputDataSourceRequest_ManyAggregators_CheckResultObjectString(DataS var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(dataSourceRequest); object expectedObject = "{ Salary = { sum = 14850, average = 2970 }, Number = { max = 6 } }"; - Assert.AreEqual(expectedObject, result.Aggregates.ToString()); + ClassicAssert.AreEqual(expectedObject, result.Aggregates.ToString()); } } } \ No newline at end of file diff --git a/test/KendoNET.DynamicLinq.Test/AggregatorTestSystem.cs b/test/KendoNET.DynamicLinq.Test/AggregatorTestSystem.cs new file mode 100644 index 0000000..8b18dbb --- /dev/null +++ b/test/KendoNET.DynamicLinq.Test/AggregatorTestSystem.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.Text.Json; +using KendoNET.DynamicLinq.Test.Data; +using NUnit.Framework; +using NUnit.Framework.Legacy; + + +namespace KendoNET.DynamicLinq.Test +{ + [TestFixture] + public class AggregatorTestSystem + { + private MockContext _dbContext; + + + private static JsonSerializerOptions jsonSerializerOptions = CustomJsonSerializerOptions.DefaultOptions; + + + public static IEnumerable DataSourceRequestWithAggregateSalarySum + { + get + { + + yield return JsonSerializer.Deserialize("{\"take\":10,\"skip\":0,\"aggregate\":[{\"field\":\"Salary\",\"aggregate\":\"sum\"}]}", jsonSerializerOptions); + + } + } + + public static IEnumerable DataSourceRequestWithManyAggregates + { + get + { + yield return JsonSerializer.Deserialize( + "{\"take\":10,\"skip\":0,\"aggregate\":[{\"field\":\"Salary\",\"aggregate\":\"sum\"},{\"field\":\"Salary\",\"aggregate\":\"average\"},{\"field\":\"Number\",\"aggregate\":\"max\"}]}", + jsonSerializerOptions); + + } + } + + [SetUp] + public void Setup() + { + _dbContext = MockContext.GetDefaultInMemoryDbContext(); + } + + [Test] + public void InputParameter_DecimalSum_CheckResultObjectString() + { + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(10, 0, null, null, new[] + { + new Aggregator + { + Aggregate = "sum", + Field = "Salary" + } + }, null); + + object expectedObject = "{ Salary = { sum = 14850 } }"; + ClassicAssert.AreEqual(expectedObject, result.Aggregates.ToString()); + } + + [TestCaseSource(nameof(DataSourceRequestWithAggregateSalarySum))] + public void InputDataSourceRequest_DecimalSum_CheckResultObjectString(DataSourceRequest dataSourceRequest) + { + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(dataSourceRequest); + + object expectedObject = "{ Salary = { sum = 14850 } }"; + ClassicAssert.AreEqual(expectedObject, result.Aggregates.ToString()); + } + + [TestCaseSource(nameof(DataSourceRequestWithAggregateSalarySum))] + public void InputDataSourceRequest_DecimalSum_CheckResultSum(DataSourceRequest dataSourceRequest) + { + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(dataSourceRequest); + var salaryAggregates = result.Aggregates.GetType().GetProperty("Salary")?.GetValue(result.Aggregates, null); + var salarySum = salaryAggregates?.GetType().GetProperty("sum")?.GetValue(salaryAggregates, null); + + const decimal expectedSalarySum = 14850; + ClassicAssert.AreEqual(expectedSalarySum, salarySum); + + const decimal incorrectSalarySum = 9999; + ClassicAssert.AreNotEqual(incorrectSalarySum, salarySum); + } + + [Test] + public void InputParameter_ManyAggregators_CheckResultObjectString() + { + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(10, 0, null, null, new[] + { + new Aggregator + { + Aggregate = "sum", + Field = "Salary" + }, + new Aggregator + { + Aggregate = "average", + Field = "Salary" + }, + new Aggregator + { + Aggregate = "max", + Field = "Number" + }, + }, null); + + object expectedObject = "{ Salary = { sum = 14850, average = 2970 }, Number = { max = 6 } }"; + ClassicAssert.AreEqual(expectedObject, result.Aggregates.ToString()); + } + + [TestCaseSource(nameof(DataSourceRequestWithManyAggregates))] + public void InputDataSourceRequest_ManyAggregators_CheckResultObjectString(DataSourceRequest dataSourceRequest) + { + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(dataSourceRequest); + + object expectedObject = "{ Salary = { sum = 14850, average = 2970 }, Number = { max = 6 } }"; + ClassicAssert.AreEqual(expectedObject, result.Aggregates.ToString()); + } + } +} \ No newline at end of file diff --git a/test/KendoNET.DynamicLinq.Test/CustomJsonSerializerOptions.cs b/test/KendoNET.DynamicLinq.Test/CustomJsonSerializerOptions.cs index 9fafabd..3d7cbf9 100644 --- a/test/KendoNET.DynamicLinq.Test/CustomJsonSerializerOptions.cs +++ b/test/KendoNET.DynamicLinq.Test/CustomJsonSerializerOptions.cs @@ -1,4 +1,4 @@ -#if NETCOREAPP3_1 + using System; using System.Text.Json; @@ -61,5 +61,3 @@ public override void Write(Utf8JsonWriter writer, object objectToWrite, JsonSeri throw new InvalidOperationException("Should not get here."); } } - -#endif \ No newline at end of file diff --git a/test/KendoNET.DynamicLinq.Test/FilterTest.cs b/test/KendoNET.DynamicLinq.Test/FilterTest.cs index 6d50ff5..698b813 100644 --- a/test/KendoNET.DynamicLinq.Test/FilterTest.cs +++ b/test/KendoNET.DynamicLinq.Test/FilterTest.cs @@ -1,15 +1,9 @@ -using System.Linq; -using Microsoft.EntityFrameworkCore; -using NUnit.Framework; +using System.Linq; using KendoNET.DynamicLinq.Test.Data; - -#if NETCOREAPP3_1 -using System.Text.Json; -#endif - -#if NETCOREAPP2_1 || NETCOREAPP2_2 +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; -#endif +using NUnit.Framework; +using NUnit.Framework.Legacy; namespace KendoNET.DynamicLinq.Test { @@ -18,9 +12,7 @@ public class FilterTest { private MockContext _dbContext; -#if NETCOREAPP3_1 - private JsonSerializerOptions jsonSerializerOptions = CustomJsonSerializerOptions.DefaultOptions; -#endif + [SetUp] public void Setup() @@ -39,7 +31,7 @@ public void InputParameter_SubPropertyContains_CheckResultCount() Logic = "and" }); - Assert.AreEqual(2, result.Total); + ClassicAssert.AreEqual(2, result.Total); var result2 = _dbContext.Employee.AsQueryable().ToDataSourceResult(10, 0, null, new Filter { @@ -55,7 +47,7 @@ public void InputParameter_SubPropertyContains_CheckResultCount() Logic = "and" }); - Assert.AreEqual(2, result2.Total); + ClassicAssert.AreEqual(2, result2.Total); } [Test] @@ -63,16 +55,12 @@ public void InputDataSourceRequest_DecimalGreaterAndLess_CheckResultCount() { // source string = {"take":20,"skip":0,"filter":{"logic":"and","filters":[{"field":"Salary","operator":"gt","value":999.00},{"field":"Salary","operator":"lt","value":6000.00}]}} -#if NETCOREAPP3_1 - var request = JsonSerializer.Deserialize( - "{\"take\":20,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"field\":\"Salary\",\"operator\":\"gt\",\"value\":999.00},{\"field\":\"Salary\",\"operator\":\"lt\",\"value\":6000.00}]}}", - jsonSerializerOptions); -#else + var request = JsonConvert.DeserializeObject( "{\"take\":20,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"field\":\"Salary\",\"operator\":\"gt\",\"value\":999.00},{\"field\":\"Salary\",\"operator\":\"lt\",\"value\":6000.00}]}}"); -#endif + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(request); - Assert.AreEqual(4, result.Total); + ClassicAssert.AreEqual(4, result.Total); } [Test] @@ -80,16 +68,12 @@ public void InputDataSourceRequest_DoubleGreaterAndLessEqual_CheckResultCount() { // source string = {"take":20,"skip":0,"filter":{"logic":"and","filters":[{"field":"Weight","operator":"gt","value":48},{"field":"Weight","operator":"lt","value":69.2}]}} -#if NETCOREAPP3_1 - var request = JsonSerializer.Deserialize( - "{\"take\":20,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"field\":\"Weight\",\"operator\":\"gt\",\"value\":48},{\"field\":\"Weight\",\"operator\":\"lte\",\"value\":69.2}]}}", - jsonSerializerOptions); -#else + var request = JsonConvert.DeserializeObject( "{\"take\":20,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"field\":\"Weight\",\"operator\":\"gt\",\"value\":48},{\"field\":\"Weight\",\"operator\":\"lte\",\"value\":69.2}]}}"); -#endif + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(request); - Assert.AreEqual(3, result.Total); + ClassicAssert.AreEqual(3, result.Total); } [Test] @@ -97,16 +81,12 @@ public void InputDataSourceRequest_ManyConditions_CheckResultCount() { // source string = {\"take\":10,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"logic\":\"or\",\"filters\":[{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1986-10-09T16:00:00.000Z\"},{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1976-11-05T16:00:00.000Z\"}]},{\"logic\":\"and\",\"filters\":[{\"field\":\"Salary\",\"operator\":\"gte\",\"value\":1000},{\"field\":\"Salary\",\"operator\":\"lte\",\"value\":6000}]}]}} -#if NETCOREAPP3_1 - var request = JsonSerializer.Deserialize( - "{\"take\":10,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"logic\":\"or\",\"filters\":[{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1986-10-09T00:00:00.000Z\"},{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1976-11-05T00:00:00.000Z\"}]},{\"logic\":\"and\",\"filters\":[{\"field\":\"Salary\",\"operator\":\"gte\",\"value\":1000},{\"field\":\"Salary\",\"operator\":\"lte\",\"value\":6000}]}]}}", - jsonSerializerOptions); -#else + var request = JsonConvert.DeserializeObject( "{\"take\":10,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"logic\":\"or\",\"filters\":[{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1986-10-09T00:00:00.000Z\"},{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1976-11-05T00:00:00.000Z\"}]},{\"logic\":\"and\",\"filters\":[{\"field\":\"Salary\",\"operator\":\"gte\",\"value\":1000},{\"field\":\"Salary\",\"operator\":\"lte\",\"value\":6000}]}]}}"); -#endif + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(request); - Assert.AreEqual(2, result.Total); + ClassicAssert.AreEqual(2, result.Total); } } } \ No newline at end of file diff --git a/test/KendoNET.DynamicLinq.Test/FilterTestSystem.cs b/test/KendoNET.DynamicLinq.Test/FilterTestSystem.cs new file mode 100644 index 0000000..03a1edb --- /dev/null +++ b/test/KendoNET.DynamicLinq.Test/FilterTestSystem.cs @@ -0,0 +1,95 @@ +using System.Linq; +using System.Text.Json; +using KendoNET.DynamicLinq.Test.Data; +using Microsoft.EntityFrameworkCore; +using NUnit.Framework; +using NUnit.Framework.Legacy; + + +namespace KendoNET.DynamicLinq.Test +{ + [TestFixture] + public class FilterTestSystem + { + private MockContext _dbContext; + + private JsonSerializerOptions jsonSerializerOptions = CustomJsonSerializerOptions.DefaultOptions; + + + [SetUp] + public void Setup() + { + _dbContext = MockContext.GetDefaultInMemoryDbContext(); + } + + [Test] + public void InputParameter_SubPropertyContains_CheckResultCount() + { + var result = _dbContext.Employee.Include(x => x.Company).AsQueryable().ToDataSourceResult(10, 0, null, new Filter + { + Field = "Company.Name", + Value = "Microsoft", + Operator = "contains", + Logic = "and" + }); + + ClassicAssert.AreEqual(2, result.Total); + + var result2 = _dbContext.Employee.AsQueryable().ToDataSourceResult(10, 0, null, new Filter + { + Filters = new[] + { + new Filter + { + Field = "Company.Name", + Operator = "contains", + Value = "Microsoft" + } + }, + Logic = "and" + }); + + ClassicAssert.AreEqual(2, result2.Total); + } + + [Test] + public void InputDataSourceRequest_DecimalGreaterAndLess_CheckResultCount() + { + // source string = {"take":20,"skip":0,"filter":{"logic":"and","filters":[{"field":"Salary","operator":"gt","value":999.00},{"field":"Salary","operator":"lt","value":6000.00}]}} + + + var request = JsonSerializer.Deserialize( + "{\"take\":20,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"field\":\"Salary\",\"operator\":\"gt\",\"value\":999.00},{\"field\":\"Salary\",\"operator\":\"lt\",\"value\":6000.00}]}}", + jsonSerializerOptions); + + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(request); + ClassicAssert.AreEqual(4, result.Total); + } + + [Test] + public void InputDataSourceRequest_DoubleGreaterAndLessEqual_CheckResultCount() + { + // source string = {"take":20,"skip":0,"filter":{"logic":"and","filters":[{"field":"Weight","operator":"gt","value":48},{"field":"Weight","operator":"lt","value":69.2}]}} + + var request = JsonSerializer.Deserialize( + "{\"take\":20,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"field\":\"Weight\",\"operator\":\"gt\",\"value\":48},{\"field\":\"Weight\",\"operator\":\"lte\",\"value\":69.2}]}}", + jsonSerializerOptions); + + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(request); + ClassicAssert.AreEqual(3, result.Total); + } + + [Test] + public void InputDataSourceRequest_ManyConditions_CheckResultCount() + { + // source string = {\"take\":10,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"logic\":\"or\",\"filters\":[{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1986-10-09T16:00:00.000Z\"},{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1976-11-05T16:00:00.000Z\"}]},{\"logic\":\"and\",\"filters\":[{\"field\":\"Salary\",\"operator\":\"gte\",\"value\":1000},{\"field\":\"Salary\",\"operator\":\"lte\",\"value\":6000}]}]}} + + var request = JsonSerializer.Deserialize( + "{\"take\":10,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"logic\":\"or\",\"filters\":[{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1986-10-09T00:00:00.000Z\"},{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1976-11-05T00:00:00.000Z\"}]},{\"logic\":\"and\",\"filters\":[{\"field\":\"Salary\",\"operator\":\"gte\",\"value\":1000},{\"field\":\"Salary\",\"operator\":\"lte\",\"value\":6000}]}]}}", + jsonSerializerOptions); + + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(request); + ClassicAssert.AreEqual(2, result.Total); + } + } +} \ No newline at end of file diff --git a/test/KendoNET.DynamicLinq.Test/GroupTest.cs b/test/KendoNET.DynamicLinq.Test/GroupTest.cs index 4d70fda..a6a3fc8 100644 --- a/test/KendoNET.DynamicLinq.Test/GroupTest.cs +++ b/test/KendoNET.DynamicLinq.Test/GroupTest.cs @@ -1,14 +1,9 @@ -using NUnit.Framework; -using System.Linq.Dynamic.Core; +using System.Linq.Dynamic.Core; using KendoNET.DynamicLinq.Test.Data; - -#if NETCOREAPP3_1 -using System.Text.Json; -#endif - -#if NETCOREAPP2_1 || NETCOREAPP2_2 using Newtonsoft.Json; -#endif +using NUnit.Framework; +using NUnit.Framework.Legacy; + namespace KendoNET.DynamicLinq.Test { @@ -17,9 +12,7 @@ public class GroupTest { private MockContext _dbContext; -#if NETCOREAPP3_1 - private JsonSerializerOptions _jsonSerializerOptions = CustomJsonSerializerOptions.DefaultOptions; -#endif + [SetUp] public void Setup() @@ -32,16 +25,13 @@ public void DataSourceRequest_EnumField_GroupedCount() { // source string = {"take":20,"skip":0,"sort":[{"field":"Number","dir":"desc"}],"group":[{"field":"Gender"}]} -#if NETCOREAPP3_1 - var request = JsonSerializer.Deserialize("{\"take\":20,\"skip\":0,\"sort\":[{\"field\":\"Number\",\"dir\":\"desc\"}],\"group\":[{\"field\":\"Gender\"}]}", - _jsonSerializerOptions); -#else + var request = JsonConvert.DeserializeObject("{\"take\":20,\"skip\":0,\"sort\":[{\"field\":\"Number\",\"dir\":\"desc\"}],\"group\":[{\"field\":\"Gender\"}]}"); -#endif + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(request); var groupItems = result.Groups.ToDynamicList().Count; - Assert.AreEqual(3, groupItems); + ClassicAssert.AreEqual(3, groupItems); } } } \ No newline at end of file diff --git a/test/KendoNET.DynamicLinq.Test/GroupTestSystem.cs b/test/KendoNET.DynamicLinq.Test/GroupTestSystem.cs new file mode 100644 index 0000000..45324ef --- /dev/null +++ b/test/KendoNET.DynamicLinq.Test/GroupTestSystem.cs @@ -0,0 +1,37 @@ +using System.Linq.Dynamic.Core; +using System.Text.Json; +using KendoNET.DynamicLinq.Test.Data; +using NUnit.Framework; +using NUnit.Framework.Legacy; + +namespace KendoNET.DynamicLinq.Test +{ + [TestFixture] + public class GroupTestSystem + { + private MockContext _dbContext; + + + private JsonSerializerOptions _jsonSerializerOptions = CustomJsonSerializerOptions.DefaultOptions; + + + [SetUp] + public void Setup() + { + _dbContext = MockContext.GetDefaultInMemoryDbContext(); + } + + [Test] + public void DataSourceRequest_EnumField_GroupedCount() + { + // source string = {"take":20,"skip":0,"sort":[{"field":"Number","dir":"desc"}],"group":[{"field":"Gender"}]} + + var request = JsonSerializer.Deserialize("{\"take\":20,\"skip\":0,\"sort\":[{\"field\":\"Number\",\"dir\":\"desc\"}],\"group\":[{\"field\":\"Gender\"}]}", + _jsonSerializerOptions); + + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(request); + var groupItems = result.Groups.ToDynamicList().Count; + ClassicAssert.AreEqual(3, groupItems); + } + } +} \ No newline at end of file diff --git a/test/KendoNET.DynamicLinq.Test/KendoNET.DynamicLinq.Test.csproj b/test/KendoNET.DynamicLinq.Test/KendoNET.DynamicLinq.Test.csproj index 032a16f..b26dfd0 100644 --- a/test/KendoNET.DynamicLinq.Test/KendoNET.DynamicLinq.Test.csproj +++ b/test/KendoNET.DynamicLinq.Test/KendoNET.DynamicLinq.Test.csproj @@ -1,21 +1,21 @@ - + - netcoreapp2.1;netcoreapp3.1 + net8.0 false KendoNET.DynamicLinq.Test - - - - - + + + + + - + From 2bb2ad6f53e0f6d4820e672403e6e3e10a4120cf Mon Sep 17 00:00:00 2001 From: Luiz Fernando Bicalho Date: Mon, 26 May 2025 18:35:37 -0300 Subject: [PATCH 02/16] Refactor DataSourceResult to be generic Updated the DataSourceResult class to a generic type DataSourceResult, enhancing type safety by using IEnumerable for data and IEnumerable for groups. Adjusted methods in QueryableExtensions to return DataSourceResult and updated method signatures to support generic types. Reorganized using directives for clarity. These changes improve flexibility and type safety in Kendo DataSource operations. --- src/DataSourceResult.cs | 8 ++++---- src/EnumerableExtensions.cs | 18 +++++++++++++----- src/KendoNET.DynamicLinq.csproj | 12 +++--------- src/QueryableExtensions.cs | 14 +++++++------- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/DataSourceResult.cs b/src/DataSourceResult.cs index 0441a9c..be5ef34 100644 --- a/src/DataSourceResult.cs +++ b/src/DataSourceResult.cs @@ -1,5 +1,5 @@ using System; -using System.Collections; +using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; @@ -9,17 +9,17 @@ namespace KendoNET.DynamicLinq /// Describes the result of Kendo DataSource read operation. /// [KnownType("GetKnownTypes")] - public class DataSourceResult + public class DataSourceResult { /// /// Represents a single page of processed data. /// - public IEnumerable Data { get; set; } + public IEnumerable Data { get; set; } /// /// Represents a single page of processed grouped data. /// - public IEnumerable Groups { get; set; } + public IEnumerable Groups { get; set; } /// /// Represents a requested aggregates. diff --git a/src/EnumerableExtensions.cs b/src/EnumerableExtensions.cs index 3d504c0..6521631 100644 --- a/src/EnumerableExtensions.cs +++ b/src/EnumerableExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Dynamic.Core; @@ -7,7 +7,7 @@ namespace KendoNET.DynamicLinq { public static class EnumerableExtensions { - public static dynamic GroupByMany(this IEnumerable elements, IEnumerable groupSelectors) + public static IEnumerable GroupByMany(this IEnumerable elements, IEnumerable groupSelectors) { // Create a new list of Kendo Group Selectors var selectors = new List>(groupSelectors.Count()); @@ -29,7 +29,7 @@ public static dynamic GroupByMany(this IEnumerable elements, return elements.GroupByMany(selectors.ToArray()); } - public static dynamic GroupByMany(this IEnumerable elements, params GroupSelector[] groupSelectors) + public static IEnumerable GroupByMany(this IEnumerable elements, params GroupSelector[] groupSelectors) { if (groupSelectors.Length > 0) { @@ -38,7 +38,7 @@ public static dynamic GroupByMany(this IEnumerable elements, var nextSelectors = groupSelectors.Skip(1).ToArray(); // Reduce the list recursively until zero // Group by and return - return elements.GroupBy(selector.Selector).Select( + return elements.GroupBy(selector.Selector).Select( g => new GroupResult { Value = g.Key, @@ -51,7 +51,15 @@ public static dynamic GroupByMany(this IEnumerable elements, } // If there are not more group selectors return data - return elements; + return elements.Select(s => new GroupResult + { + Aggregates = QueryableExtensions.Aggregates(elements.AsQueryable(), null), + Items = s, + Count = 1, + HasSubgroups = false, + SelectorField = string.Empty, + Value = s + }); } } } \ No newline at end of file diff --git a/src/KendoNET.DynamicLinq.csproj b/src/KendoNET.DynamicLinq.csproj index d9dbbf8..348cccb 100644 --- a/src/KendoNET.DynamicLinq.csproj +++ b/src/KendoNET.DynamicLinq.csproj @@ -1,7 +1,7 @@  - netstandard1.6;netstandard2.0;netstandard2.1 + net8.0 KendoNET.DynamicLinq @@ -9,16 +9,10 @@ KendoNET.DynamicLinq Kendo.DynamicLinqCore CoCo Lin - Kendo.DynamicLinqCore implements server paging, filtering, sorting, grouping and aggregating to Kendo UI via Dynamic Linq for .Net Core App(1.x ~ 3.x). + Kendo.DynamicLinqCore implements server paging, filtering, sorting, grouping and aggregating to Kendo UI via Dynamic Linq for .Net 8.0. Supported platforms: - - - .NET Standard 1.6 - - .NET Standard 2.0 - - .NET Standard 2.1 - - .NET Core 1.0 ~ .NET Core 1.1 - - .NET Core 2.0 ~ .NET Core 2.2 - - .NET Core 3.0 ~ .NET Core 3.1 + - .NET Core 8.0 diff --git a/src/QueryableExtensions.cs b/src/QueryableExtensions.cs index ff842d5..f7d1f32 100644 --- a/src/QueryableExtensions.cs +++ b/src/QueryableExtensions.cs @@ -1,10 +1,10 @@ using System; -using System.Threading.Tasks; using System.Collections.Generic; -using System.Reflection; using System.Linq; using System.Linq.Dynamic.Core; using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; namespace KendoNET.DynamicLinq { @@ -20,7 +20,7 @@ public static class QueryableExtensions /// Specifies the current sort order. /// Specifies the current filter. /// A DataSourceResult object populated from the processed IQueryable. - public static DataSourceResult ToDataSourceResult(this IQueryable queryable, int take, int skip, IEnumerable sort, Filter filter) + public static DataSourceResult ToDataSourceResult(this IQueryable queryable, int take, int skip, IEnumerable sort, Filter filter) { return queryable.ToDataSourceResult(take, skip, sort, filter, null, null); } @@ -32,7 +32,7 @@ public static DataSourceResult ToDataSourceResult(this IQueryable queryabl /// The IQueryable which should be processed. /// The DataSourceRequest object containing take, skip, sort, filter, aggregates, and groups data. /// A DataSourceResult object populated from the processed IQueryable. - public static DataSourceResult ToDataSourceResult(this IQueryable queryable, DataSourceRequest request) + public static DataSourceResult ToDataSourceResult(this IQueryable queryable, DataSourceRequest request) { return queryable.ToDataSourceResult(request.Take, request.Skip, request.Sort, request.Filter, request.Aggregate, request.Group); } @@ -49,7 +49,7 @@ public static DataSourceResult ToDataSourceResult(this IQueryable queryabl /// Specifies the current aggregates. /// Specifies the current groups. /// A DataSourceResult object populated from the processed IQueryable. - public static DataSourceResult ToDataSourceResult(this IQueryable queryable, + public static DataSourceResult ToDataSourceResult(this IQueryable queryable, int take, int skip, IEnumerable sort, @@ -92,7 +92,7 @@ public static DataSourceResult ToDataSourceResult(this IQueryable queryabl queryable = Page(queryable, take, skip); } - var result = new DataSourceResult + var result = new DataSourceResult { Total = total, Aggregates = aggregate @@ -129,7 +129,7 @@ public static DataSourceResult ToDataSourceResult(this IQueryable queryabl /// Specifies the current aggregates. /// Specifies the current groups. /// A DataSourceResult object populated from the processed IQueryable. - public static Task ToDataSourceResultAsync(this IQueryable queryable, + public static Task> ToDataSourceResultAsync(this IQueryable queryable, int take, int skip, IEnumerable sort, From c12762cb73a55001aecaf5e74c7b265481bba498 Mon Sep 17 00:00:00 2001 From: Luiz Fernando Bicalho Date: Mon, 26 May 2025 20:09:47 -0300 Subject: [PATCH 03/16] Update KendoNET.DynamicLinq for .NET 8.0 support - Target .NET 8.0 in KendoNET.DynamicLinq.EFCore.csproj, enabling nullable reference types and adding EF Core 9.0.5. - Introduce asynchronous methods in QueryableExtensions for improved data processing. - Update solution file to include KendoNET.DynamicLinq.EFCore and change Visual Studio version to 17. - Modify Aggregator properties to initialize with default values and update return types to be nullable. - Ensure DataSourceRequest and DataSourceResult properties are initialized to prevent null references. - Enhance Filter class with better null handling and error reporting. - Update Group, GroupResult, and GroupSelector classes for property initialization. - Refactor QueryableExtensions for consistency and improved error handling. --- .../KendoNET.DynamicLinq.EFCore.csproj | 17 +++ .../QueryableExtensions.cs | 119 ++++++++++++++++++ KendoNET.DynamicLinq.sln | 20 ++- src/Aggregator.cs | 43 ++++--- src/DataSourceRequest.cs | 8 +- src/DataSourceResult.cs | 12 +- src/EnumerableExtensions.cs | 5 +- src/Filter.cs | 42 +++++-- src/Group.cs | 2 +- src/GroupResult.cs | 8 +- src/GroupSelector.cs | 6 +- src/KendoNET.DynamicLinq.csproj | 10 +- src/QueryableExtensions.cs | 59 ++++----- src/Sort.cs | 4 +- 14 files changed, 262 insertions(+), 93 deletions(-) create mode 100644 KendoNET.DynamicLinq.EFCore/KendoNET.DynamicLinq.EFCore.csproj create mode 100644 KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs diff --git a/KendoNET.DynamicLinq.EFCore/KendoNET.DynamicLinq.EFCore.csproj b/KendoNET.DynamicLinq.EFCore/KendoNET.DynamicLinq.EFCore.csproj new file mode 100644 index 0000000..456f33c --- /dev/null +++ b/KendoNET.DynamicLinq.EFCore/KendoNET.DynamicLinq.EFCore.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + Nullable + + + + + + + + + + + diff --git a/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs b/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs new file mode 100644 index 0000000..540532d --- /dev/null +++ b/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace KendoNET.DynamicLinq.EFCore +{ + public static class QueryableExtensionsAsync + { + + /// + /// Applies data processing (paging, sorting and filtering) over IQueryable using Dynamic Linq. + /// + /// The type of the IQueryable. + /// The IQueryable which should be processed. + /// Specifies how many items to take. Configurable via the pageSize setting of the Kendo DataSource. + /// Specifies how many items to skip. + /// Specifies the current sort order. + /// Specifies the current filter. + /// A DataSourceResult object populated from the processed IQueryable. + public static Task> ToDataSourceResultAsync(this IQueryable queryable, int take, int skip, IEnumerable sort, Filter filter, CancellationToken ct) + { + return queryable.ToDataSourceResultAsync(take, skip, sort, filter, null, null, ct); + } + + /// + /// Applies data processing (paging, sorting and filtering) over IQueryable using Dynamic Linq. + /// + /// The type of the IQueryable. + /// The IQueryable which should be processed. + /// The DataSourceRequest object containing take, skip, sort, filter, aggregates, and groups data. + /// A DataSourceResult object populated from the processed IQueryable. + public static Task> ToDataSourceResultAsync(this IQueryable queryable, DataSourceRequest request, CancellationToken ct) + { + return queryable.ToDataSourceResultAsync(request.Take, request.Skip, request.Sort, request.Filter, request.Aggregate, request.Group, ct); + } + + /// + /// Applies data processing (paging, sorting, filtering and aggregates) over IQueryable using Dynamic Linq. + /// + /// The type of the IQueryable. + /// The IQueryable which should be processed. + /// Specifies how many items to take. Configurable via the pageSize setting of the Kendo DataSource. + /// Specifies how many items to skip. + /// Specifies the current sort order. + /// Specifies the current filter. + /// Specifies the current aggregates. + /// Specifies the current groups. + /// A DataSourceResult object populated from the processed IQueryable. + public static async Task> ToDataSourceResultAsync(this IQueryable queryable, + int take, + int skip, + IEnumerable sort, + Filter filter, + IEnumerable? aggregates, + IEnumerable? group, + CancellationToken ct) + { + var errors = new List(); + + // Filter the data first + queryable = QueryableExtensions.Filters(queryable, filter, errors); + + // Calculate the total number of records (needed for paging) + var total = await queryable.CountAsync(ct); + + // Calculate the aggregates + var aggregate = QueryableExtensions.Aggregates(queryable, aggregates); + if (group?.Any() == true) + { + //if(sort == null) sort = GetDefaultSort(queryable.ElementType, sort); + if (sort == null) + sort = new List(); + foreach (var source in group.Reverse()) + { + sort = sort.Append(new Sort + { + Field = source.Field, + Dir = source.Dir + }); + } + } + + // Sort the data + queryable = QueryableExtensions.Sort(queryable, sort); + + // Finally page the data + if (take > 0) + { + queryable = QueryableExtensions.Page(queryable, take, skip); + } + + var result = new DataSourceResult + { + Total = total, + Aggregates = aggregate + }; + + // Group By + if (group?.Any() == true) + { + result.Groups = queryable.GroupByMany(group); + } + else + { + result.Data = await queryable.ToListAsync(ct); + } + + // Set errors if any + if (errors.Count > 0) + { + result.Errors = errors; + } + + return result; + } + } +} diff --git a/KendoNET.DynamicLinq.sln b/KendoNET.DynamicLinq.sln index d47e720..3fe152e 100644 --- a/KendoNET.DynamicLinq.sln +++ b/KendoNET.DynamicLinq.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29609.76 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36121.58 d17.14 MinimumVisualStudioVersion = 15.0.26124.0 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KendoNET.DynamicLinq", "src\KendoNET.DynamicLinq.csproj", "{6F75D7FE-0A2C-4586-8152-D25AA61E6CA9}" EndProject @@ -17,6 +17,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Assets", "Assets", "{531E0E README.md = README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KendoNET.DynamicLinq.EFCore", "KendoNET.DynamicLinq.EFCore\KendoNET.DynamicLinq.EFCore.csproj", "{E71AD039-BF32-48A0-8BA7-14AA09082B52}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -63,6 +65,18 @@ Global {E3FA5C13-FD73-457B-85F2-30D8BA0CCA82}.Release|x64.Build.0 = Release|Any CPU {E3FA5C13-FD73-457B-85F2-30D8BA0CCA82}.Release|x86.ActiveCfg = Release|Any CPU {E3FA5C13-FD73-457B-85F2-30D8BA0CCA82}.Release|x86.Build.0 = Release|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Debug|x64.ActiveCfg = Debug|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Debug|x64.Build.0 = Debug|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Debug|x86.ActiveCfg = Debug|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Debug|x86.Build.0 = Debug|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Release|Any CPU.Build.0 = Release|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Release|x64.ActiveCfg = Release|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Release|x64.Build.0 = Release|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Release|x86.ActiveCfg = Release|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -70,4 +84,4 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E645F9BA-8510-4B50-98F7-3740F27727CB} EndGlobalSection -EndGlobal \ No newline at end of file +EndGlobal diff --git a/src/Aggregator.cs b/src/Aggregator.cs index 011df5d..3f7e27c 100644 --- a/src/Aggregator.cs +++ b/src/Aggregator.cs @@ -16,35 +16,33 @@ public class Aggregator /// Gets or sets the name of the aggregated field (property). /// [DataMember(Name = "field")] - public string Field { get; set; } + public string Field { get; set; } = string.Empty; /// /// Gets or sets the aggregate. /// [DataMember(Name = "aggregate")] - public string Aggregate { get; set; } + public string Aggregate { get; set; } = string.Empty; /// /// Get MethodInfo. /// /// Specifies the type of querable data. /// A MethodInfo for field. - public MethodInfo MethodInfo(Type type) + public MethodInfo? MethodInfo(Type type) { - var proptype = type.GetProperty(Field).PropertyType; + var proptype = type.GetProperty(Field)?.PropertyType ?? throw new ArgumentException($"Property '{Field}' not found in type '{type.Name}'."); switch (Aggregate) { case "max": case "min": - return GetMethod(ConvertTitleCase(Aggregate), MinMaxFunc().GetMethodInfo(), 2).MakeGenericMethod(type, proptype); + return GetMethod(ConvertTitleCase(Aggregate), MinMaxFunc().GetMethodInfo(), 2)?.MakeGenericMethod(type, proptype); case "average": case "sum": - return GetMethod(ConvertTitleCase(Aggregate), - ((Func)GetType().GetMethod("SumAvgFunc", BindingFlags.Static | BindingFlags.NonPublic).MakeGenericMethod(proptype).Invoke(null, null)) - .GetMethodInfo(), 1).MakeGenericMethod(type); + return GetMethod(ConvertTitleCase(Aggregate), GetSumAvg(GetType(), proptype).GetMethodInfo(), 1)?.MakeGenericMethod(type); case "count": return GetMethod(ConvertTitleCase(Aggregate), - Nullable.GetUnderlyingType(proptype) != null ? CountNullableFunc().GetMethodInfo() : CountFunc().GetMethodInfo(), 1).MakeGenericMethod(type); + Nullable.GetUnderlyingType(proptype) != null ? CountNullableFunc().GetMethodInfo() : CountFunc().GetMethodInfo(), 1)?.MakeGenericMethod(type); } return null; @@ -62,15 +60,20 @@ private static string ConvertTitleCase(string str) return string.Join(" ", tokens); } - private static MethodInfo GetMethod(string methodName, MethodInfo methodTypes, int genericArgumentsCount) + private static MethodInfo? GetMethod(string methodName, MethodInfo? methodTypes, int genericArgumentsCount) { + if (methodTypes == null) + { + throw new ArgumentNullException(nameof(methodTypes), "Method types cannot be null."); + } + var methods = from method in typeof(Queryable).GetMethods(BindingFlags.Public | BindingFlags.Static) - let parameters = method.GetParameters() - let genericArguments = method.GetGenericArguments() - where method.Name == methodName && - genericArguments.Length == genericArgumentsCount && - parameters.Select(p => p.ParameterType).SequenceEqual((Type[])methodTypes.Invoke(null, genericArguments)) - select method; + let parameters = method.GetParameters() + let genericArguments = method.GetGenericArguments() + where method.Name == methodName && + genericArguments.Length == genericArgumentsCount && + parameters.Select(p => p.ParameterType).SequenceEqual((Type[])(methodTypes.Invoke(null, genericArguments) ?? Array.Empty())) + select method; return methods.FirstOrDefault(); } @@ -94,6 +97,14 @@ private static Type[] CountDelegate(Type t) return new[] { typeof(IQueryable<>).MakeGenericType(t) }; } + private static Func GetSumAvg(Type t, Type proptype) + { + return (Func)(t.GetMethod(nameof(SumAvgFunc), BindingFlags.Static | BindingFlags.NonPublic)?.MakeGenericMethod(proptype).Invoke(null, null) + ?? throw new ArgumentException()); + + } + + private static Func MinMaxFunc() { return MinMaxDelegate; diff --git a/src/DataSourceRequest.cs b/src/DataSourceRequest.cs index a92dad8..1232db5 100644 --- a/src/DataSourceRequest.cs +++ b/src/DataSourceRequest.cs @@ -17,21 +17,21 @@ public class DataSourceRequest /// /// Specifies the requested sort order. /// - public IEnumerable Sort { get; set; } + public IEnumerable Sort { get; set; } = []; /// /// Specifies the requested filter. /// - public Filter Filter { get; set; } + public Filter Filter { get; set; } = new(); /// /// Specifies the requested grouping . /// - public IEnumerable Group { get; set; } + public IEnumerable? Group { get; set; } /// /// Specifies the requested aggregators. /// - public IEnumerable Aggregate { get; set; } + public IEnumerable? Aggregate { get; set; } } } \ No newline at end of file diff --git a/src/DataSourceResult.cs b/src/DataSourceResult.cs index be5ef34..32c1e9d 100644 --- a/src/DataSourceResult.cs +++ b/src/DataSourceResult.cs @@ -14,17 +14,17 @@ public class DataSourceResult /// /// Represents a single page of processed data. /// - public IEnumerable Data { get; set; } + public IEnumerable Data { get; set; } = []; /// /// Represents a single page of processed grouped data. /// - public IEnumerable Groups { get; set; } + public IEnumerable? Groups { get; set; } = []; /// /// Represents a requested aggregates. /// - public object Aggregates { get; set; } + public object? Aggregates { get; set; } /// /// The total number of records available. @@ -34,7 +34,7 @@ public class DataSourceResult /// /// Represents error information from server-side. /// - public object Errors { get; set; } + public object? Errors { get; set; } /// /// Used by the KnownType attribute which is required for WCF serialization support @@ -42,8 +42,8 @@ public class DataSourceResult /// private static Type[] GetKnownTypes() { - var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName.StartsWith("DynamicClasses")); - return assembly == null ? new Type[0] : assembly.GetTypes().Where(t => t.Name.StartsWith("DynamicClass")).ToArray(); + var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a?.FullName?.StartsWith("DynamicClasses") ?? false); + return assembly == null ? [] : assembly.GetTypes().Where(t => t.Name.StartsWith("DynamicClass")).ToArray(); } } } \ No newline at end of file diff --git a/src/EnumerableExtensions.cs b/src/EnumerableExtensions.cs index 6521631..f30cc2d 100644 --- a/src/EnumerableExtensions.cs +++ b/src/EnumerableExtensions.cs @@ -36,7 +36,10 @@ public static IEnumerable GroupByMany(this IEnumerable new GroupResult diff --git a/src/Filter.cs b/src/Filter.cs index 50b31e2..4464c30 100644 --- a/src/Filter.cs +++ b/src/Filter.cs @@ -17,31 +17,31 @@ public class Filter /// Gets or sets the name of the sorted field (property). Set to null if the Filters property is set. /// [DataMember(Name = "field")] - public string Field { get; set; } + public string Field { get; set; } = string.Empty; /// /// Gets or sets the filtering operator. Set to null if the Filters property is set. /// [DataMember(Name = "operator")] - public string Operator { get; set; } + public string Operator { get; set; } = string.Empty; /// /// Gets or sets the filtering value. Set to null if the Filters property is set. /// [DataMember(Name = "value")] - public object Value { get; set; } + public object? Value { get; set; } /// /// Gets or sets the filtering logic. Can be set to "or" or "and". Set to null unless Filters is set. /// [DataMember(Name = "logic")] - public string Logic { get; set; } + public string Logic { get; set; } = string.Empty; /// /// Gets or sets the child filter expressions. Set to null if there are no child expressions. /// [DataMember(Name = "filters")] - public IEnumerable Filters { get; set; } + public IEnumerable Filters { get; set; } = []; /// /// Mapping of Kendo DataSource filtering operators to Dynamic Linq @@ -169,7 +169,7 @@ public Expression ToLambdaExpression(ParameterExpression parameter, IList filter.ToLambdaExpression(parameter, filters)).ToArray()) @@ -188,7 +188,7 @@ public Expression ToLambdaExpression(ParameterExpression parameter, IList(ParameterExpression parameter, IList(ParameterExpression parameter, IList(ParameterExpression parameter, IList(ParameterExpression parameter, IList Aggregates { get; set; } + public IEnumerable Aggregates { get; set; } = []; } } diff --git a/src/GroupResult.cs b/src/GroupResult.cs index 77ff762..369630f 100644 --- a/src/GroupResult.cs +++ b/src/GroupResult.cs @@ -8,9 +8,9 @@ public class GroupResult { // Small letter properties are kendo js properties so please excuse the warnings [DataMember(Name = "value")] - public object Value { get; set; } + public object? Value { get; set; } - public string SelectorField { get; set; } + public string SelectorField { get; set; } = string.Empty; [DataMember(Name = "field")] public string Field @@ -21,10 +21,10 @@ public string Field public int Count { get; set; } [DataMember(Name = "aggregates")] - public object Aggregates { get; set; } + public object? Aggregates { get; set; } [DataMember(Name = "items")] - public dynamic Items { get; set; } + public dynamic? Items { get; set; } [DataMember(Name = "hasSubgroups")] public bool HasSubgroups { get; set; } // true if there are subgroups diff --git a/src/GroupSelector.cs b/src/GroupSelector.cs index 257618b..cc1e4a0 100644 --- a/src/GroupSelector.cs +++ b/src/GroupSelector.cs @@ -5,8 +5,8 @@ namespace KendoNET.DynamicLinq { public class GroupSelector { - public Func Selector { get; set; } - public string Field { get; set; } - public IEnumerable Aggregates { get; set; } + public Func? Selector { get; set; } + public string Field { get; set; } = string.Empty; + public IEnumerable Aggregates { get; set; } = []; } } diff --git a/src/KendoNET.DynamicLinq.csproj b/src/KendoNET.DynamicLinq.csproj index 348cccb..efc7185 100644 --- a/src/KendoNET.DynamicLinq.csproj +++ b/src/KendoNET.DynamicLinq.csproj @@ -3,6 +3,8 @@ net8.0 KendoNET.DynamicLinq + enable + Nullable @@ -34,14 +36,8 @@ LICENSE - - - - - - - + diff --git a/src/QueryableExtensions.cs b/src/QueryableExtensions.cs index f7d1f32..aee4957 100644 --- a/src/QueryableExtensions.cs +++ b/src/QueryableExtensions.cs @@ -4,7 +4,6 @@ using System.Linq.Dynamic.Core; using System.Linq.Expressions; using System.Reflection; -using System.Threading.Tasks; namespace KendoNET.DynamicLinq { @@ -54,8 +53,8 @@ public static DataSourceResult ToDataSourceResult(this IQueryable query int skip, IEnumerable sort, Filter filter, - IEnumerable aggregates, - IEnumerable group) + IEnumerable? aggregates, + IEnumerable? group) { var errors = new List(); @@ -117,30 +116,9 @@ public static DataSourceResult ToDataSourceResult(this IQueryable query return result; } - /// - /// Asynchronously applies data processing (paging, sorting, filtering and aggregates) over IQueryable using Dynamic Linq. - /// - /// The type of the IQueryable. - /// The IQueryable which should be processed. - /// Specifies how many items to take. Configurable via the pageSize setting of the Kendo DataSource. - /// Specifies how many items to skip. - /// Specifies the current sort order. - /// Specifies the current filter. - /// Specifies the current aggregates. - /// Specifies the current groups. - /// A DataSourceResult object populated from the processed IQueryable. - public static Task> ToDataSourceResultAsync(this IQueryable queryable, - int take, - int skip, - IEnumerable sort, - Filter filter, - IEnumerable aggregates = null, - IEnumerable group = null) - { - return Task.Run(() => queryable.ToDataSourceResult(take, skip, sort, filter, aggregates, group)); - } - private static IQueryable Filters(IQueryable queryable, Filter filter, List errors) + + public static IQueryable Filters(IQueryable queryable, Filter filter, List errors) { if (filter?.Logic != null) { @@ -193,20 +171,24 @@ private static IQueryable Filters(IQueryable queryable, Filter filter, return queryable; } - internal static object Aggregates(IQueryable queryable, IEnumerable aggregates) + public static object? Aggregates(IQueryable queryable, IEnumerable? aggregates) { if (aggregates?.Any() == true) { var objProps = new Dictionary(); var groups = aggregates.GroupBy(g => g.Field); - Type type = null; + Type? type = null; foreach (var group in groups) { - var fieldProps = new Dictionary(); + var fieldProps = new Dictionary(); foreach (var aggregate in group) { var prop = typeof(T).GetProperty(aggregate.Field); + if (prop == null) + { + throw new ArgumentException($"Property '{aggregate.Field}' does not exist on type '{typeof(T).Name}'."); + } var param = Expression.Parameter(typeof(T), "s"); var selector = aggregate.Aggregate == "count" && (Nullable.GetUnderlyingType(prop.PropertyType) != null) ? Expression.Lambda(Expression.NotEqual(Expression.MakeMemberAccess(param, prop), Expression.Constant(null, prop.PropertyType)), param) @@ -225,9 +207,12 @@ internal static object Aggregates(IQueryable queryable, IEnumerable(IQueryable queryable, IEnumerable(IQueryable queryable, IEnumerable Sort(IQueryable queryable, IEnumerable sort) + public static IQueryable Sort(IQueryable queryable, IEnumerable sort) { if (sort?.Any() == true) { @@ -259,7 +244,7 @@ private static IQueryable Sort(IQueryable queryable, IEnumerable return queryable; } - private static IQueryable Page(IQueryable queryable, int take, int skip) + public static IQueryable Page(IQueryable queryable, int take, int skip) { return queryable.Skip(skip).Take(take); } @@ -318,7 +303,7 @@ private static Filter PreliminaryWork(Type type, Filter filter) new Filter { Field = filter.Field, - Filters = filter.Filters, + Filters = filter.Filters??[], Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, 0, 0, 0), Operator = "gte" }, @@ -326,7 +311,7 @@ private static Filter PreliminaryWork(Type type, Filter filter) new Filter { Field = filter.Field, - Filters = filter.Filters, + Filters = filter.Filters??[], Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, 23, 59, 59), Operator = "lte" } @@ -355,7 +340,7 @@ private static IEnumerable GetDefaultSort(Type type, IEnumerable sor //by default make dir desc var sortByObject = new Sort { Dir = "desc" }; - PropertyInfo propertyInfo; + PropertyInfo? propertyInfo; //look for property that is called id if (properties.Any(p => string.Equals(p.Name, "id", StringComparison.OrdinalIgnoreCase))) { diff --git a/src/Sort.cs b/src/Sort.cs index 6312ac7..deb08ae 100644 --- a/src/Sort.cs +++ b/src/Sort.cs @@ -12,13 +12,13 @@ public class Sort /// Gets or sets the name of the sorted field (property). /// [DataMember(Name = "field")] - public string Field { get; set; } + public string Field { get; set; } = string.Empty; /// /// Gets or sets the sort direction. Should be either "asc" or "desc". /// [DataMember(Name = "dir")] - public string Dir { get; set; } + public string Dir { get; set; } = string.Empty; /// /// Converts to form required by Dynamic Linq e.g. "Field1 desc" From ed6f8376942be19f4ce2df372eedf7d436baed1c Mon Sep 17 00:00:00 2001 From: Luiz Fernando Bicalho Date: Mon, 26 May 2025 20:23:27 -0300 Subject: [PATCH 04/16] Enhance async processing and type safety in grouping - Updated `QueryableExtensions.cs` to use async processing for `GroupByMany`, improving performance. - Changed `Items` property in `GroupResult` from dynamic to `IEnumerable` in `EnumerableExtensions.cs` for better type safety. - Organized using directives in `GroupResult.cs` and ensured necessary namespaces are included. --- KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs | 2 +- src/EnumerableExtensions.cs | 1 - src/GroupResult.cs | 5 +++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs b/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs index 540532d..b6e303c 100644 --- a/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs +++ b/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs @@ -100,7 +100,7 @@ public static async Task> ToDataSourceResultAsync(this IQ // Group By if (group?.Any() == true) { - result.Groups = queryable.GroupByMany(group); + result.Groups = await queryable.GroupByMany(group).AsQueryable().ToListAsync(ct); } else { diff --git a/src/EnumerableExtensions.cs b/src/EnumerableExtensions.cs index f30cc2d..280a68f 100644 --- a/src/EnumerableExtensions.cs +++ b/src/EnumerableExtensions.cs @@ -57,7 +57,6 @@ public static IEnumerable GroupByMany(this IEnumerable new GroupResult { Aggregates = QueryableExtensions.Aggregates(elements.AsQueryable(), null), - Items = s, Count = 1, HasSubgroups = false, SelectorField = string.Empty, diff --git a/src/GroupResult.cs b/src/GroupResult.cs index 369630f..48c6f4c 100644 --- a/src/GroupResult.cs +++ b/src/GroupResult.cs @@ -1,4 +1,5 @@ -using System.Runtime.Serialization; +using System.Collections.Generic; +using System.Runtime.Serialization; namespace KendoNET.DynamicLinq { @@ -24,7 +25,7 @@ public string Field public object? Aggregates { get; set; } [DataMember(Name = "items")] - public dynamic? Items { get; set; } + public IEnumerable? Items { get; set; } [DataMember(Name = "hasSubgroups")] public bool HasSubgroups { get; set; } // true if there are subgroups From d6e5b8bf15d49f6d5f178661929e54c120eb7cd9 Mon Sep 17 00:00:00 2001 From: Luiz Fernando Bicalho Date: Mon, 26 May 2025 21:07:34 -0300 Subject: [PATCH 05/16] Refactor queryable data handling with UpdateQuery method Introduced a new `UpdateQuery` method to consolidate sorting and paging logic for queryable data. Removed inline grouping, sorting, and paging code from the main flow to enhance code organization and readability. The new method accepts parameters for queryable data, pagination, and sorting/grouping, returning the updated queryable while maintaining the overall result structure. --- .../QueryableExtensions.cs | 24 +-------- src/QueryableExtensions.cs | 52 +++++++++++-------- 2 files changed, 30 insertions(+), 46 deletions(-) diff --git a/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs b/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs index b6e303c..b84d2f9 100644 --- a/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs +++ b/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs @@ -67,29 +67,7 @@ public static async Task> ToDataSourceResultAsync(this IQ // Calculate the aggregates var aggregate = QueryableExtensions.Aggregates(queryable, aggregates); - if (group?.Any() == true) - { - //if(sort == null) sort = GetDefaultSort(queryable.ElementType, sort); - if (sort == null) - sort = new List(); - foreach (var source in group.Reverse()) - { - sort = sort.Append(new Sort - { - Field = source.Field, - Dir = source.Dir - }); - } - } - - // Sort the data - queryable = QueryableExtensions.Sort(queryable, sort); - - // Finally page the data - if (take > 0) - { - queryable = QueryableExtensions.Page(queryable, take, skip); - } + queryable = QueryableExtensions.UpdateQuery(queryable, take, skip, ref sort, group); var result = new DataSourceResult { diff --git a/src/QueryableExtensions.cs b/src/QueryableExtensions.cs index aee4957..626582c 100644 --- a/src/QueryableExtensions.cs +++ b/src/QueryableExtensions.cs @@ -67,29 +67,7 @@ public static DataSourceResult ToDataSourceResult(this IQueryable query // Calculate the aggregates var aggregate = Aggregates(queryable, aggregates); - if (group?.Any() == true) - { - //if(sort == null) sort = GetDefaultSort(queryable.ElementType, sort); - if (sort == null) sort = new List(); - - foreach (var source in group.Reverse()) - { - sort = sort.Append(new Sort - { - Field = source.Field, - Dir = source.Dir - }); - } - } - - // Sort the data - queryable = Sort(queryable, sort); - - // Finally page the data - if (take > 0) - { - queryable = Page(queryable, take, skip); - } + queryable = UpdateQuery(queryable, take, skip, ref sort, group); var result = new DataSourceResult { @@ -117,6 +95,34 @@ public static DataSourceResult ToDataSourceResult(this IQueryable query } + public static IQueryable UpdateQuery(IQueryable queryable, int take, int skip, ref IEnumerable sort, IEnumerable? group) + { + if (group?.Any() == true) + { + //if(sort == null) sort = GetDefaultSort(queryable.ElementType, sort); + if (sort == null) + sort = new List(); + foreach (var source in group.Reverse()) + { + sort = sort.Append(new Sort + { + Field = source.Field, + Dir = source.Dir + }); + } + } + + // Sort the data + queryable = QueryableExtensions.Sort(queryable, sort); + + // Finally page the data + if (take > 0) + { + queryable = QueryableExtensions.Page(queryable, take, skip); + } + return queryable; + } + public static IQueryable Filters(IQueryable queryable, Filter filter, List errors) { From 2db282ae17dca0300fd64560482af05e1c55d34a Mon Sep 17 00:00:00 2001 From: Luiz Fernando Bicalho Date: Wed, 28 May 2025 10:50:13 -0300 Subject: [PATCH 06/16] Update repository references and support for .NET 8 - Changed repository name from `linmasaki` to `luizfbicalho` in CHANGELOG.md for multiple versions. - Updated LICENSE file to reflect copyright ownership by `luiz bicalho`. - Modified README.md to indicate support for .NET 8, replacing .NET Core 1.x to 3.x. - Updated sample code reference link in README.md to the new repository URL. - Revised instructions for adding the repository URL to NuGet package metadata. --- CHANGELOG.md | 22 +++++++++++----------- LICENSE | 2 +- README.md | 10 +++++----- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9bf7b5..be0b77a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,17 +2,17 @@ ### V3.1.2 (2022/07/14) --[#22](https://github.com/linmasaki/KendoNET.DynamicLinq/discussions/22) Rename this repository. +-[#22](https://github.com/luizfbicalho/KendoNET.DynamicLinq/discussions/22) Rename this repository. ### V3.1.1 (2020/11/05) -- [#13](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/13) Fix the issue that filter will throw exception if decimal property is optional. -- [#6](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/6) Add asynchronous method of retrieving data(This feature is still in the experimental stage, not recommend using it on your product). +- [#13](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/13) Fix the issue that filter will throw exception if decimal property is optional. +- [#6](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/6) Add asynchronous method of retrieving data(This feature is still in the experimental stage, not recommend using it on your product). ### V3.1.0 (2020/02/11) -- [#10](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/10) Fix the issue that the LINQ query with sub-property can't be translated and will be evaluated locally. -- [#12](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/12) Amend the issue that the method `ToDataSourceResult(this IQueryable queryable, DataSourceRequest request)` would ignore the +- [#10](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/10) Fix the issue that the LINQ query with sub-property can't be translated and will be evaluated locally. +- [#12](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/12) Amend the issue that the method `ToDataSourceResult(this IQueryable queryable, DataSourceRequest request)` would ignore the aggregator parameter. ### V2.2.2 (2019/09/17) @@ -23,13 +23,13 @@ ### V2.2.0 (2019/07/05) - Change the property `Group` of DataSourceResult to `Groups`. -- [#5](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/5) Add new property `Aggregate` to DataSourceRequest. -- [#5](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/5) Fixed getting wrong grouping data in the request using aggregates in grouping configuration. +- [#5](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/5) Add new property `Aggregate` to DataSourceRequest. +- [#5](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/5) Fixed getting wrong grouping data in the request using aggregates in grouping configuration. ### V2.1.0 (2019/05/16) -- [#3](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/3) Support new filtering operators of `is null`, `is not null`, `is empty`, `is not empty`, `has value`, and `has no value` in grid. -- [#3](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/3) Filtering operators of `is empty`, `is not empty`, `has value`, and `has no value` doesn't support non-string types. +- [#3](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/3) Support new filtering operators of `is null`, `is not null`, `is empty`, `is not empty`, `has value`, and `has no value` in grid. +- [#3](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/3) Filtering operators of `is empty`, `is not empty`, `has value`, and `has no value` doesn't support non-string types. ### V2.0.2 (2019/04/12) @@ -41,8 +41,8 @@ ### V2.0.0 (2018/09/10) -- [#2](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/2) Support .Net Standard 2.0. +- [#2](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/2) Support .Net Standard 2.0. ### V1.0.3 (2017/02/069) -- [#1](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/1) Add `Errors` property in **`DataSourceResult`** class. +- [#1](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/1) Add `Errors` property in **`DataSourceResult`** class. diff --git a/LICENSE b/LICENSE index c633310..7fff45f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 CoCo Lin +Copyright (c) 2019 luiz bicalho Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index a50e35f..4780994 100644 --- a/README.md +++ b/README.md @@ -6,18 +6,18 @@ ## Description -KendoNET.DynamicLinq implements server paging, filtering, sorting, grouping, and aggregating to Kendo UI via Dynamic Linq for .Net Core App(1.x ~ 3.x). +KendoNET.DynamicLinq implements server paging, filtering, sorting, grouping, and aggregating to Kendo UI via Dynamic Linq for .Net 8. ## Prerequisites -### .Net Core 1 ~ 2 +### .Net 8 Newtonsoft.Json - None -### .Net Core 3 +### .Net 8 System.Text.Json - You must add custom `ObjectToInferredTypesConverter` to your `JsonSerializerOptions` since `System.Text.Json` didn't deserialize inferred type to object properties now, see - the [sample code](https://github.com/linmasaki/KendoNET.DynamicLinq/blob/master/test/KendoNET.DynamicLinq.Tests/CustomJsonSerializerOptions.cs) + the [sample code](https://github.com/luizfbicalho/KendoNET.DynamicLinq/blob/master/test/KendoNET.DynamicLinq.Tests/CustomJsonSerializerOptions.cs) and [reference](https://docs.microsoft.com/en-gb/dotnet/standard/serialization/system-text-json-converters-how-to#deserialize-inferred-types-to-object-properties). ## Usage @@ -165,7 +165,7 @@ public class MyContext : DbContext 2. Switch to project root directory(src\KendoNET.DynamicLinq). 3. Run "dotnet restore" 4. Run "dotnet pack --configuration Release" -5. Add `` to package metadata of nupkg to show repository URL at Nuget +5. Add `` to package metadata of nupkg to show repository URL at Nuget ## Note From dc07f09b4c2d8aeef9b842ffc90e84ca42d0bd3a Mon Sep 17 00:00:00 2001 From: Luiz Fernando Bicalho Date: Wed, 28 May 2025 11:16:01 -0300 Subject: [PATCH 07/16] remove ref not necessary --- KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs | 2 +- src/QueryableExtensions.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs b/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs index b84d2f9..54e46cb 100644 --- a/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs +++ b/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs @@ -67,7 +67,7 @@ public static async Task> ToDataSourceResultAsync(this IQ // Calculate the aggregates var aggregate = QueryableExtensions.Aggregates(queryable, aggregates); - queryable = QueryableExtensions.UpdateQuery(queryable, take, skip, ref sort, group); + queryable = QueryableExtensions.UpdateQuery(queryable, take, skip, sort, group); var result = new DataSourceResult { diff --git a/src/QueryableExtensions.cs b/src/QueryableExtensions.cs index 626582c..90b4ed7 100644 --- a/src/QueryableExtensions.cs +++ b/src/QueryableExtensions.cs @@ -67,7 +67,7 @@ public static DataSourceResult ToDataSourceResult(this IQueryable query // Calculate the aggregates var aggregate = Aggregates(queryable, aggregates); - queryable = UpdateQuery(queryable, take, skip, ref sort, group); + queryable = UpdateQuery(queryable, take, skip, sort, group); var result = new DataSourceResult { @@ -95,7 +95,7 @@ public static DataSourceResult ToDataSourceResult(this IQueryable query } - public static IQueryable UpdateQuery(IQueryable queryable, int take, int skip, ref IEnumerable sort, IEnumerable? group) + public static IQueryable UpdateQuery(IQueryable queryable, int take, int skip, IEnumerable sort, IEnumerable? group) { if (group?.Any() == true) { From e7deb99e96ee08debe38480c367bd06acc33ac89 Mon Sep 17 00:00:00 2001 From: Luiz Fernando Bicalho Date: Wed, 28 May 2025 11:28:03 -0300 Subject: [PATCH 08/16] Update README for .NET 8.0 support Updated the README.md to reflect support for .NET 8.0 by replacing the .NET Standard badge with a .NET 8.0 badge. This change highlights the compatibility of KendoNET.DynamicLinq with the latest .NET version. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4780994..b0ecfc2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Version](https://img.shields.io/nuget/vpre/KendoNET.DynamicLinq.svg)](https://www.nuget.org/packages/KendoNET.DynamicLinq) [![Downloads](https://img.shields.io/nuget/dt/KendoNET.DynamicLinq.svg)](https://www.nuget.org/packages/KendoNET.DynamicLinq) -[![.NET Standard](https://img.shields.io/badge/.NET%20Standard-%3E%3D%201.6-red.svg)](#) +[![.NET 8.0](https://img.shields.io/badge/.NET-8.0-red)](#) ## Description From b10c1e008bbb4882f7e5d8c714272ffd2133fc7b Mon Sep 17 00:00:00 2001 From: Luiz Fernando Bicalho Date: Thu, 29 May 2025 20:26:02 -0300 Subject: [PATCH 09/16] Update project for .NET 8.0 and enhance documentation - Target .NET 8.0 in `KendoNET.DynamicLinq.EFCore.csproj`. - Add new package references for analyzers and tools. - Change package ID to `KendoNET.luizfbicalho.DynamicLinq`. - Add exception documentation in `QueryableExtensions.cs`, `Aggregator.cs`, `DataSourceResult.cs`, `EnumerableExtensions.cs`, and `Filter.cs`. - Refactor exception handling for clarity and maintainability. - Change `JsonSerializerOptions` fields to readonly in test files. - Overall improvements to documentation and project structure. --- .../KendoNET.DynamicLinq.EFCore.csproj | 40 +++++++++- .../QueryableExtensions.cs | 49 +++++++++++- src/Aggregator.cs | 61 ++++++++++++-- src/DataSourceResult.cs | 2 + src/EnumerableExtensions.cs | 9 +++ src/Filter.cs | 39 ++++++--- src/KendoNET.DynamicLinq.csproj | 55 +++++++++++-- src/QueryableExtensions.cs | 79 ++++++++++++++++++- .../AggregatorTestSystem.cs | 2 +- .../FilterTestSystem.cs | 2 +- .../GroupTestSystem.cs | 2 +- 11 files changed, 308 insertions(+), 32 deletions(-) diff --git a/KendoNET.DynamicLinq.EFCore/KendoNET.DynamicLinq.EFCore.csproj b/KendoNET.DynamicLinq.EFCore/KendoNET.DynamicLinq.EFCore.csproj index 456f33c..2a1426c 100644 --- a/KendoNET.DynamicLinq.EFCore/KendoNET.DynamicLinq.EFCore.csproj +++ b/KendoNET.DynamicLinq.EFCore/KendoNET.DynamicLinq.EFCore.csproj @@ -4,6 +4,7 @@ net8.0 enable Nullable + true @@ -13,5 +14,42 @@ - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs b/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs index 54e46cb..42a817e 100644 --- a/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs +++ b/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -19,6 +21,21 @@ public static class QueryableExtensionsAsync /// Specifies the current sort order. /// Specifies the current filter. /// A DataSourceResult object populated from the processed IQueryable. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// public static Task> ToDataSourceResultAsync(this IQueryable queryable, int take, int skip, IEnumerable sort, Filter filter, CancellationToken ct) { return queryable.ToDataSourceResultAsync(take, skip, sort, filter, null, null, ct); @@ -31,6 +48,21 @@ public static Task> ToDataSourceResultAsync(this IQueryab /// The IQueryable which should be processed. /// The DataSourceRequest object containing take, skip, sort, filter, aggregates, and groups data. /// A DataSourceResult object populated from the processed IQueryable. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// public static Task> ToDataSourceResultAsync(this IQueryable queryable, DataSourceRequest request, CancellationToken ct) { return queryable.ToDataSourceResultAsync(request.Take, request.Skip, request.Sort, request.Filter, request.Aggregate, request.Group, ct); @@ -48,6 +80,21 @@ public static Task> ToDataSourceResultAsync(this IQueryab /// Specifies the current aggregates. /// Specifies the current groups. /// A DataSourceResult object populated from the processed IQueryable. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// public static async Task> ToDataSourceResultAsync(this IQueryable queryable, int take, int skip, diff --git a/src/Aggregator.cs b/src/Aggregator.cs index 3f7e27c..09fd071 100644 --- a/src/Aggregator.cs +++ b/src/Aggregator.cs @@ -29,6 +29,15 @@ public class Aggregator /// /// Specifies the type of querable data. /// A MethodInfo for field. + /// + /// + /// + /// + /// + /// + /// + /// + /// public MethodInfo? MethodInfo(Type type) { var proptype = type.GetProperty(Field)?.PropertyType ?? throw new ArgumentException($"Property '{Field}' not found in type '{type.Name}'."); @@ -48,6 +57,11 @@ public class Aggregator return null; } + /// + /// Converts the aggregate name to title case. + /// + /// + /// private static string ConvertTitleCase(string str) { var tokens = str.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries); @@ -60,6 +74,16 @@ private static string ConvertTitleCase(string str) return string.Join(" ", tokens); } + /// + /// Get MethodInfo from Queryable methods. + /// + /// + /// + /// + /// + /// + /// + /// private static MethodInfo? GetMethod(string methodName, MethodInfo? methodTypes, int genericArgumentsCount) { if (methodTypes == null) @@ -82,9 +106,13 @@ private static Func CountNullableFunc() return CountNullableDelegate; } + /// + /// Count delegate type for nullable types. + /// + /// private static Type[] CountNullableDelegate(Type t) { - return new[] { typeof(IQueryable<>).MakeGenericType(t), typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(t, typeof(bool))) }; + return [typeof(IQueryable<>).MakeGenericType(t), typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(t, typeof(bool)))]; } private static Func CountFunc() @@ -92,15 +120,29 @@ private static Func CountFunc() return CountDelegate; } + /// + /// Returns the type for count delegate. + /// + /// private static Type[] CountDelegate(Type t) { - return new[] { typeof(IQueryable<>).MakeGenericType(t) }; + return [typeof(IQueryable<>).MakeGenericType(t)]; } + /// + /// Gthe Sum or Average delegate type. + /// + /// + /// + /// + /// + /// + /// + /// private static Func GetSumAvg(Type t, Type proptype) { return (Func)(t.GetMethod(nameof(SumAvgFunc), BindingFlags.Static | BindingFlags.NonPublic)?.MakeGenericMethod(proptype).Invoke(null, null) - ?? throw new ArgumentException()); + ?? throw new ArgumentException("Unable to invoke SumAvgFunc.")); } @@ -109,10 +151,13 @@ private static Func MinMaxFunc() { return MinMaxDelegate; } - + /// + /// Minor Max delegate type. + /// + /// private static Type[] MinMaxDelegate(Type a, Type b) { - return new[] { typeof(IQueryable<>).MakeGenericType(a), typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(a, b)) }; + return [typeof(IQueryable<>).MakeGenericType(a), typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(a, b))]; } private static Func SumAvgFunc() @@ -120,9 +165,13 @@ private static Func SumAvgFunc() return SumAvgDelegate; } + /// + /// Sum or Average delegate type. + /// + /// private static Type[] SumAvgDelegate(Type t) { - return new[] { typeof(IQueryable<>).MakeGenericType(t), typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(t, typeof(TU))) }; + return [typeof(IQueryable<>).MakeGenericType(t), typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(t, typeof(TU)))]; } } } \ No newline at end of file diff --git a/src/DataSourceResult.cs b/src/DataSourceResult.cs index 32c1e9d..a25296e 100644 --- a/src/DataSourceResult.cs +++ b/src/DataSourceResult.cs @@ -40,6 +40,8 @@ public class DataSourceResult /// Used by the KnownType attribute which is required for WCF serialization support /// /// + /// + /// private static Type[] GetKnownTypes() { var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a?.FullName?.StartsWith("DynamicClasses") ?? false); diff --git a/src/EnumerableExtensions.cs b/src/EnumerableExtensions.cs index 280a68f..7f7c4b6 100644 --- a/src/EnumerableExtensions.cs +++ b/src/EnumerableExtensions.cs @@ -7,6 +7,10 @@ namespace KendoNET.DynamicLinq { public static class EnumerableExtensions { + /// + /// Group elements by multiple selectors. + /// + /// public static IEnumerable GroupByMany(this IEnumerable elements, IEnumerable groupSelectors) { // Create a new list of Kendo Group Selectors @@ -29,6 +33,11 @@ public static IEnumerable GroupByMany(this IEnumerable + /// Group elements by multiple selectors. + /// + /// + /// public static IEnumerable GroupByMany(this IEnumerable elements, params GroupSelector[] groupSelectors) { if (groupSelectors.Length > 0) diff --git a/src/Filter.cs b/src/Filter.cs index 4464c30..cdc9d29 100644 --- a/src/Filter.cs +++ b/src/Filter.cs @@ -69,11 +69,12 @@ public class Filter /// /// These operators only for string type. /// - private static readonly string[] StringOperators = new[] { "startswith", "endswith", "contains", "doesnotcontain", "isempty", "isnotempty", "isnullorempty", "isnotnullorempty" }; + private static readonly string[] StringOperators = ["startswith", "endswith", "contains", "doesnotcontain", "isempty", "isnotempty", "isnullorempty", "isnotnullorempty"]; /// /// Get a flattened list of all child filter expressions. /// + /// public IList All() { var filters = new List(); @@ -81,7 +82,10 @@ public IList All() return filters; } - + /// + /// Collects the filter expressions into a flat list. + /// + /// private void Collect(IList filters) { if (Filters?.Any() == true) @@ -101,6 +105,9 @@ private void Collect(IList filters) /// Converts the filter expression to a predicate suitable for Dynamic Linq e.g. "Field1 = @1 and Field2.Contains(@2)" /// /// A list of flattened filters. + /// + /// + /// public string ToExpression(Type type, IList filters) { if (Filters?.Any() == true) @@ -111,7 +118,7 @@ public string ToExpression(Type type, IList filters) var currentPropertyType = GetLastPropertyType(type, Field); if (currentPropertyType != typeof(String) && StringOperators.Contains(Operator)) { - throw new NotSupportedException(string.Format("Operator {0} not support non-string type", Operator)); + throw new NotSupportedException($"Operator {Operator} not support non-string type"); } int index = filters.IndexOf(this); @@ -134,30 +141,30 @@ public string ToExpression(Type type, IList filters) if (Operator == "doesnotcontain") { - return String.Format("{0} != null && !{0}.{1}(@{2})", Field, comparison, index); + return $"{Field} != null && !{Field}.{comparison}(@{index})"; } if (Operator == "isnull" || Operator == "isnotnull") { - return String.Format("{0} {1} null", Field, comparison); + return $"{Field} {comparison} null"; } if (Operator == "isempty" || Operator == "isnotempty") { - return String.Format("{0} {1} String.Empty", Field, comparison); + return $"{Field} {comparison} String.Empty"; } if (Operator == "isnullorempty" || Operator == "isnotnullorempty") { - return String.Format("{0}String.IsNullOrEmpty({1})", comparison, Field); + return $"{comparison}String.IsNullOrEmpty({Field})"; } if (comparison == "StartsWith" || comparison == "EndsWith" || comparison == "Contains") { - return String.Format("{0} != null && {0}.{1}(@{2})", Field, comparison, index); + return $"{Field} != null && {Field}.{comparison}(@{index})"; } - return String.Format("{0} {1} @{2}", Field, comparison, index); + return $"{Field} {comparison} @{index}"; } /// @@ -165,6 +172,10 @@ public string ToExpression(Type type, IList filters) /// /// Parameter expression /// A list of flattened filters. + /// + /// + /// + /// public Expression ToLambdaExpression(ParameterExpression parameter, IList filters) { if (Filters?.Any() == true) @@ -194,7 +205,7 @@ public Expression ToLambdaExpression(ParameterExpression parameter, IList(ParameterExpression parameter, IList + /// GEt last property type from the path. + /// + /// + /// internal static Type GetLastPropertyType(Type type, string path) { Type currentType = type; diff --git a/src/KendoNET.DynamicLinq.csproj b/src/KendoNET.DynamicLinq.csproj index efc7185..50e4644 100644 --- a/src/KendoNET.DynamicLinq.csproj +++ b/src/KendoNET.DynamicLinq.csproj @@ -5,12 +5,13 @@ KendoNET.DynamicLinq enable Nullable + true - KendoNET.DynamicLinq + KendoNET.luizfbicalho.DynamicLinq Kendo.DynamicLinqCore - CoCo Lin + Luiz Bicalho Kendo.DynamicLinqCore implements server paging, filtering, sorting, grouping and aggregating to Kendo UI via Dynamic Linq for .Net 8.0. Supported platforms: @@ -22,17 +23,17 @@ 1. Fix the issue that filter will throw exception if decimal property is optional. 2. Add asynchronous method of retrieving data(This feature is still in the experimental stage, not recommend using it on your product). - Full changelog: https://github.com/linmasaki/Kendo.DynamicLinqCore/blob/master/CHANGELOG.md + Full changelog: https://github.com/luizfbicalho/Kendo.DynamicLinqCore/blob/master/CHANGELOG.md - https://github.com/linmasaki/Kendo.DynamicLinqCore - https://github.com/linmasaki/Kendo.DynamicLinqCore - https://raw.githubusercontent.com/linmasaki/CoCoPackageIcon/master/cocodotnet64.png + https://github.com/luizfbicalho/Kendo.DynamicLinqCore + https://github.com/luizfbicalho/Kendo.DynamicLinqCore + https://raw.githubusercontent.com/luizfbicalho/CoCoPackageIcon/master/cocodotnet64.png Icon.png netcore netstandard kendo kendo-ui linq dynamic 3.1.1 3.1.1 - Copyright © 2017-2020 CoCo Lin + Copyright © 2017-2020 LICENSE @@ -40,4 +41,44 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/src/QueryableExtensions.cs b/src/QueryableExtensions.cs index 90b4ed7..8a31e24 100644 --- a/src/QueryableExtensions.cs +++ b/src/QueryableExtensions.cs @@ -19,6 +19,21 @@ public static class QueryableExtensions /// Specifies the current sort order. /// Specifies the current filter. /// A DataSourceResult object populated from the processed IQueryable. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// public static DataSourceResult ToDataSourceResult(this IQueryable queryable, int take, int skip, IEnumerable sort, Filter filter) { return queryable.ToDataSourceResult(take, skip, sort, filter, null, null); @@ -31,6 +46,21 @@ public static DataSourceResult ToDataSourceResult(this IQueryable query /// The IQueryable which should be processed. /// The DataSourceRequest object containing take, skip, sort, filter, aggregates, and groups data. /// A DataSourceResult object populated from the processed IQueryable. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// public static DataSourceResult ToDataSourceResult(this IQueryable queryable, DataSourceRequest request) { return queryable.ToDataSourceResult(request.Take, request.Skip, request.Sort, request.Filter, request.Aggregate, request.Group); @@ -48,6 +78,22 @@ public static DataSourceResult ToDataSourceResult(this IQueryable query /// Specifies the current aggregates. /// Specifies the current groups. /// A DataSourceResult object populated from the processed IQueryable. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// public static DataSourceResult ToDataSourceResult(this IQueryable queryable, int take, int skip, @@ -94,7 +140,10 @@ public static DataSourceResult ToDataSourceResult(this IQueryable query return result; } - + /// + /// Updates the IQueryable with sorting and paging. + /// + /// public static IQueryable UpdateQuery(IQueryable queryable, int take, int skip, IEnumerable sort, IEnumerable? group) { if (group?.Any() == true) @@ -123,7 +172,11 @@ public static IQueryable UpdateQuery(IQueryable queryable, int take, in return queryable; } - + /// + /// Set Filters for IQueryable using Dynamic Linq. + /// + /// + /// public static IQueryable Filters(IQueryable queryable, Filter filter, List errors) { if (filter?.Logic != null) @@ -177,6 +230,23 @@ public static IQueryable Filters(IQueryable queryable, Filter filter, L return queryable; } + /// + /// Agregates the IQueryable using Dynamic Linq. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// public static object? Aggregates(IQueryable queryable, IEnumerable? aggregates) { if (aggregates?.Any() == true) @@ -236,6 +306,10 @@ public static IQueryable Filters(IQueryable queryable, Filter filter, L return null; } + /// + /// Sorts the IQueryable using Dynamic Linq. + /// + /// public static IQueryable Sort(IQueryable queryable, IEnumerable sort) { if (sort?.Any() == true) @@ -259,6 +333,7 @@ public static IQueryable Page(IQueryable queryable, int take, int skip) /// Pretreatment of specific DateTime type and convert some illegal value type /// /// + /// private static Filter PreliminaryWork(Type type, Filter filter) { if (filter.Filters != null && filter.Logic != null) diff --git a/test/KendoNET.DynamicLinq.Test/AggregatorTestSystem.cs b/test/KendoNET.DynamicLinq.Test/AggregatorTestSystem.cs index 8b18dbb..fa01c5e 100644 --- a/test/KendoNET.DynamicLinq.Test/AggregatorTestSystem.cs +++ b/test/KendoNET.DynamicLinq.Test/AggregatorTestSystem.cs @@ -13,7 +13,7 @@ public class AggregatorTestSystem private MockContext _dbContext; - private static JsonSerializerOptions jsonSerializerOptions = CustomJsonSerializerOptions.DefaultOptions; + private static readonly JsonSerializerOptions jsonSerializerOptions = CustomJsonSerializerOptions.DefaultOptions; public static IEnumerable DataSourceRequestWithAggregateSalarySum diff --git a/test/KendoNET.DynamicLinq.Test/FilterTestSystem.cs b/test/KendoNET.DynamicLinq.Test/FilterTestSystem.cs index 03a1edb..143cdd7 100644 --- a/test/KendoNET.DynamicLinq.Test/FilterTestSystem.cs +++ b/test/KendoNET.DynamicLinq.Test/FilterTestSystem.cs @@ -13,7 +13,7 @@ public class FilterTestSystem { private MockContext _dbContext; - private JsonSerializerOptions jsonSerializerOptions = CustomJsonSerializerOptions.DefaultOptions; + private readonly JsonSerializerOptions jsonSerializerOptions = CustomJsonSerializerOptions.DefaultOptions; [SetUp] diff --git a/test/KendoNET.DynamicLinq.Test/GroupTestSystem.cs b/test/KendoNET.DynamicLinq.Test/GroupTestSystem.cs index 45324ef..8d304f4 100644 --- a/test/KendoNET.DynamicLinq.Test/GroupTestSystem.cs +++ b/test/KendoNET.DynamicLinq.Test/GroupTestSystem.cs @@ -12,7 +12,7 @@ public class GroupTestSystem private MockContext _dbContext; - private JsonSerializerOptions _jsonSerializerOptions = CustomJsonSerializerOptions.DefaultOptions; + private readonly JsonSerializerOptions _jsonSerializerOptions = CustomJsonSerializerOptions.DefaultOptions; [SetUp] From a7feaf19023cec7ed4862d29315f94b2bd1d07ce Mon Sep 17 00:00:00 2001 From: Luiz Fernando Bicalho Date: Fri, 30 May 2025 10:28:29 -0300 Subject: [PATCH 10/16] Refactor code for improved readability and safety - Updated `GetSumAvg` to use `nameof` for type safety. - Removed commented-out code in `Filter.cs` to simplify logic. - Changed `sort` handling in `QueryableExtensions.cs` for clarity. - Eliminated the default sorting method, indicating a shift in sorting strategy. - Overall, these changes enhance maintainability and code quality. --- src/Aggregator.cs | 1 - src/DataSourceResult.cs | 2 +- src/Filter.cs | 25 +------------ src/QueryableExtensions.cs | 75 ++------------------------------------ 4 files changed, 5 insertions(+), 98 deletions(-) diff --git a/src/Aggregator.cs b/src/Aggregator.cs index 09fd071..4f2eea0 100644 --- a/src/Aggregator.cs +++ b/src/Aggregator.cs @@ -143,7 +143,6 @@ private static Func GetSumAvg(Type t, Type proptype) { return (Func)(t.GetMethod(nameof(SumAvgFunc), BindingFlags.Static | BindingFlags.NonPublic)?.MakeGenericMethod(proptype).Invoke(null, null) ?? throw new ArgumentException("Unable to invoke SumAvgFunc.")); - } diff --git a/src/DataSourceResult.cs b/src/DataSourceResult.cs index a25296e..0a57492 100644 --- a/src/DataSourceResult.cs +++ b/src/DataSourceResult.cs @@ -8,7 +8,7 @@ namespace KendoNET.DynamicLinq /// /// Describes the result of Kendo DataSource read operation. /// - [KnownType("GetKnownTypes")] + [KnownType(nameof(GetKnownTypes))] public class DataSourceResult { /// diff --git a/src/Filter.cs b/src/Filter.cs index cdc9d29..d9d308f 100644 --- a/src/Filter.cs +++ b/src/Filter.cs @@ -124,21 +124,6 @@ public string ToExpression(Type type, IList filters) int index = filters.IndexOf(this); var comparison = Operators[Operator]; - //switch(Operator) - //{ - // case "doesnotcontain": - // return String.Format("{0} != null && !{0}.{1}(@{2})", Field, comparison, index); - // case "isnull": - // case "isnotnull": - // return String.Format("{0} {1} null", Field, comparison); - // case "isempty": - // case "isnotempty": - // return String.Format("{0} {1} String.Empty", Field, comparison); - // case "isnullorempty": - // case "isnotnullorempty": - // return String.Format("{0}String.IsNullOrEmpty({1})", comparison, Field); - //} - if (Operator == "doesnotcontain") { return $"{Field} != null && !{Field}.{comparison}(@{index})"; @@ -341,7 +326,7 @@ internal static Type GetLastPropertyType(Type type, string path) /* Searches for the public property with the specified name */ /* Used in versions above 3.1.0 */ - foreach (string propertyName in path.Split('.')) + foreach (var propertyName in path.Split('.')) { PropertyInfo? property = currentType.GetProperty(propertyName); if (property == null) @@ -351,14 +336,6 @@ internal static Type GetLastPropertyType(Type type, string path) currentType = property.PropertyType; } - /* Retrieves all properties defined on the specified type, including inherited, non-public, instance, and static properties */ - /* Used in versions under 2.2.2 */ - //foreach (string propertyName in path.Split('.')) - //{ - // var typeProperties = currentType.GetRuntimeProperties(); - // currentType = typeProperties.FirstOrDefault(f => f.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase))?.PropertyType; - //} - return currentType; } } diff --git a/src/QueryableExtensions.cs b/src/QueryableExtensions.cs index 8a31e24..d64759d 100644 --- a/src/QueryableExtensions.cs +++ b/src/QueryableExtensions.cs @@ -148,9 +148,7 @@ public static IQueryable UpdateQuery(IQueryable queryable, int take, in { if (group?.Any() == true) { - //if(sort == null) sort = GetDefaultSort(queryable.ElementType, sort); - if (sort == null) - sort = new List(); + sort ??= []; foreach (var source in group.Reverse()) { sort = sort.Append(new Sort @@ -205,26 +203,6 @@ public static IQueryable Filters(IQueryable queryable, Filter filter, L // Step.3 Use the Where method of Dynamic Linq to filter the data queryable = queryable.Where(predicate, values); - - /* Method.2 Use the combined lambda expression */ - // Step.1 Create a parameter "p" - //var parameter = Expression.Parameter(typeof(T), "p"); - - // Step.2 Make up expression e.g. (p.Number >= 3) AndAlso (p.Company.Name.Contains("M")) - //Expression expression; - //try - //{ - // expression = filter.ToLambdaExpression(parameter, filters); - //} - //catch(Exception ex) - //{ - // errors.Add(ex.Message); - // return queryable; - //} - - // Step.3 The result is e.g. p => (p.Number >= 3) AndAlso (p.Company.Name.Contains("M")) - //var predicateExpression = Expression.Lambda>(expression, parameter); - //queryable = queryable.Where(predicateExpression); } return queryable; @@ -273,8 +251,8 @@ public static IQueryable Filters(IQueryable queryable, Filter filter, L if (mi == null) continue; var val = queryable.Provider.Execute(Expression.Call(null, mi, aggregate.Aggregate == "count" && (Nullable.GetUnderlyingType(prop.PropertyType) == null) - ? new[] { queryable.Expression } - : new[] { queryable.Expression, Expression.Quote(selector) })); + ? (IEnumerable)[queryable.Expression] + : (IEnumerable)[queryable.Expression, Expression.Quote(selector)])); fieldProps.Add(new DynamicProperty(aggregate.Aggregate, typeof(object)), val); } @@ -357,12 +335,6 @@ private static Filter PreliminaryWork(Type type, Filter filter) return filter; } - // if(currentPropertyType.GetTypeInfo().IsEnum && int.TryParse(filter.Value.ToString(), out int enumValue)) - // { - // filter.Value = Enum.ToObject(currentPropertyType, enumValue); - // return filter; - // } - // Convert datetime-string to DateTime if (currentPropertyType == typeof(DateTime) && DateTime.TryParse(filter.Value.ToString(), out DateTime dateTime)) { @@ -407,46 +379,5 @@ private static Filter PreliminaryWork(Type type, Filter filter) return filter; } - - /// - /// The way this extension works it pages the records using skip and takes to do that we need at least one sort property. - /// - private static IEnumerable GetDefaultSort(Type type, IEnumerable sort) - { - if (sort == null) - { - var elementType = type; - var properties = elementType.GetProperties().ToList(); - - //by default make dir desc - var sortByObject = new Sort { Dir = "desc" }; - - PropertyInfo? propertyInfo; - //look for property that is called id - if (properties.Any(p => string.Equals(p.Name, "id", StringComparison.OrdinalIgnoreCase))) - { - propertyInfo = properties.FirstOrDefault(p => string.Equals(p.Name, "id", StringComparison.OrdinalIgnoreCase)); - } - //or contains id - else if (properties.Any(p => p.Name.IndexOf("id", StringComparison.OrdinalIgnoreCase) >= 0)) - { - propertyInfo = properties.FirstOrDefault(p => p.Name.IndexOf("id", StringComparison.OrdinalIgnoreCase) >= 0); - } - //or just get the first property - else - { - propertyInfo = properties.FirstOrDefault(); - } - - if (propertyInfo != null) - { - sortByObject.Field = propertyInfo.Name; - } - - sort = new List { sortByObject }; - } - - return sort; - } } } \ No newline at end of file From 5b3493ff4fc687e50b83d2f109a41925d1379ad4 Mon Sep 17 00:00:00 2001 From: Luiz Fernando Bicalho Date: Fri, 30 May 2025 10:49:48 -0300 Subject: [PATCH 11/16] Refactor date handling and improve null safety - Added warning suppression in `Aggregator.cs` for reflection usage. - Removed null check for selector in `EnumerableExtensions.cs`, which may lead to potential null reference exceptions. - Changed `Selector` in `GroupSelector.cs` to a non-nullable type with a default value to enhance safety. - Updated `using` directives in `QueryableExtensions.cs` to include `System.Globalization`. - Modified `DateTime.TryParse` to use culture-specific parsing with `DateTimeFormatInfo.CurrentInfo`. - Specified `DateTimeKind.Unspecified` for `DateTime` objects in `QueryableExtensions.cs` to clarify date interpretation. --- src/Aggregator.cs | 2 ++ src/EnumerableExtensions.cs | 4 ---- src/GroupSelector.cs | 2 +- src/QueryableExtensions.cs | 9 +++++---- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Aggregator.cs b/src/Aggregator.cs index 4f2eea0..c8255a8 100644 --- a/src/Aggregator.cs +++ b/src/Aggregator.cs @@ -141,8 +141,10 @@ private static Type[] CountDelegate(Type t) /// private static Func GetSumAvg(Type t, Type proptype) { +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields return (Func)(t.GetMethod(nameof(SumAvgFunc), BindingFlags.Static | BindingFlags.NonPublic)?.MakeGenericMethod(proptype).Invoke(null, null) ?? throw new ArgumentException("Unable to invoke SumAvgFunc.")); +#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields } diff --git a/src/EnumerableExtensions.cs b/src/EnumerableExtensions.cs index 7f7c4b6..346adcc 100644 --- a/src/EnumerableExtensions.cs +++ b/src/EnumerableExtensions.cs @@ -45,10 +45,6 @@ public static IEnumerable GroupByMany(this IEnumerable new GroupResult diff --git a/src/GroupSelector.cs b/src/GroupSelector.cs index cc1e4a0..372ede4 100644 --- a/src/GroupSelector.cs +++ b/src/GroupSelector.cs @@ -5,7 +5,7 @@ namespace KendoNET.DynamicLinq { public class GroupSelector { - public Func? Selector { get; set; } + public Func Selector { get; set; } = _ => new object(); public string Field { get; set; } = string.Empty; public IEnumerable Aggregates { get; set; } = []; } diff --git a/src/QueryableExtensions.cs b/src/QueryableExtensions.cs index d64759d..4b6b7be 100644 --- a/src/QueryableExtensions.cs +++ b/src/QueryableExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Linq.Dynamic.Core; using System.Linq.Expressions; @@ -336,7 +337,7 @@ private static Filter PreliminaryWork(Type type, Filter filter) } // Convert datetime-string to DateTime - if (currentPropertyType == typeof(DateTime) && DateTime.TryParse(filter.Value.ToString(), out DateTime dateTime)) + if (currentPropertyType == typeof(DateTime) && DateTime.TryParse(filter.Value.ToString(), DateTimeFormatInfo.CurrentInfo, out var dateTime)) { filter.Value = dateTime; @@ -357,7 +358,7 @@ private static Filter PreliminaryWork(Type type, Filter filter) { Field = filter.Field, Filters = filter.Filters??[], - Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, 0, 0, 0), + Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, 0, 0, 0,DateTimeKind.Unspecified), Operator = "gte" }, // ...and less than the end of that same day (we're making an additional filter here) @@ -365,7 +366,7 @@ private static Filter PreliminaryWork(Type type, Filter filter) { Field = filter.Field, Filters = filter.Filters??[], - Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, 23, 59, 59), + Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, 23, 59, 59,DateTimeKind.Unspecified), Operator = "lte" } }; @@ -374,7 +375,7 @@ private static Filter PreliminaryWork(Type type, Filter filter) } // Convert datetime to local - filter.Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, localTime.Hour, localTime.Minute, localTime.Second, localTime.Millisecond); + filter.Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, localTime.Hour, localTime.Minute, localTime.Second, localTime.Millisecond, DateTimeKind.Unspecified); } return filter; From 01daad5d6c3b9bee9b4c23c74721d6ad1bf1b476 Mon Sep 17 00:00:00 2001 From: Luiz Fernando Bicalho Date: Fri, 30 May 2025 11:03:21 -0300 Subject: [PATCH 12/16] Refactor string usage and enhance extension methods Updated code to use lowercase `string` type for consistency. Modified `Sort` and `Page` methods in `QueryableExtensions.cs` to use extension method syntax. Simplified object initialization in the `Filter` class for improved readability while preserving existing functionality and documentation. --- .../QueryableExtensions.cs | 1 - src/Aggregator.cs | 1 - src/Filter.cs | 16 ++++++++-------- src/QueryableExtensions.cs | 14 ++++++-------- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs b/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs index 42a817e..85022d0 100644 --- a/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs +++ b/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs @@ -10,7 +10,6 @@ namespace KendoNET.DynamicLinq.EFCore { public static class QueryableExtensionsAsync { - /// /// Applies data processing (paging, sorting and filtering) over IQueryable using Dynamic Linq. /// diff --git a/src/Aggregator.cs b/src/Aggregator.cs index c8255a8..542c698 100644 --- a/src/Aggregator.cs +++ b/src/Aggregator.cs @@ -147,7 +147,6 @@ private static Func GetSumAvg(Type t, Type proptype) #pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields } - private static Func MinMaxFunc() { return MinMaxDelegate; diff --git a/src/Filter.cs b/src/Filter.cs index d9d308f..63f9d80 100644 --- a/src/Filter.cs +++ b/src/Filter.cs @@ -112,11 +112,11 @@ public string ToExpression(Type type, IList filters) { if (Filters?.Any() == true) { - return "(" + String.Join(" " + Logic + " ", Filters.Select(filter => filter.ToExpression(type, filters)).ToArray()) + ")"; + return "(" + string.Join(" " + Logic + " ", Filters.Select(filter => filter.ToExpression(type, filters)).ToArray()) + ")"; } var currentPropertyType = GetLastPropertyType(type, Field); - if (currentPropertyType != typeof(String) && StringOperators.Contains(Operator)) + if (currentPropertyType != typeof(string) && StringOperators.Contains(Operator)) { throw new NotSupportedException($"Operator {Operator} not support non-string type"); } @@ -188,7 +188,7 @@ public Expression ToLambdaExpression(ParameterExpression parameter, IList(ParameterExpression parameter, IList(ParameterExpression parameter, IList(ParameterExpression parameter, IList(ParameterExpression parameter, IList(ParameterExpression parameter, IList UpdateQuery(IQueryable queryable, int take, in } // Sort the data - queryable = QueryableExtensions.Sort(queryable, sort); + queryable = queryable.Sort(sort); // Finally page the data if (take > 0) { - queryable = QueryableExtensions.Page(queryable, take, skip); + queryable = queryable.Page(take, skip); } return queryable; } @@ -289,7 +289,7 @@ public static IQueryable Filters(IQueryable queryable, Filter filter, L /// Sorts the IQueryable using Dynamic Linq. /// /// - public static IQueryable Sort(IQueryable queryable, IEnumerable sort) + public static IQueryable Sort(this IQueryable queryable, IEnumerable sort) { if (sort?.Any() == true) { @@ -303,7 +303,7 @@ public static IQueryable Sort(IQueryable queryable, IEnumerable s return queryable; } - public static IQueryable Page(IQueryable queryable, int take, int skip) + public static IQueryable Page(this IQueryable queryable, int take, int skip) { return queryable.Skip(skip).Take(take); } @@ -354,16 +354,14 @@ private static Filter PreliminaryWork(Type type, Filter filter) newFilter.Filters = new List { // Instead of comparing for exact equality, we compare as greater than the start of the day... - new Filter - { + new() { Field = filter.Field, Filters = filter.Filters??[], Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, 0, 0, 0,DateTimeKind.Unspecified), Operator = "gte" }, // ...and less than the end of that same day (we're making an additional filter here) - new Filter - { + new() { Field = filter.Field, Filters = filter.Filters??[], Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, 23, 59, 59,DateTimeKind.Unspecified), From 2607bba4b1035156f0dffcdc5c2b62dedc3977b8 Mon Sep 17 00:00:00 2001 From: Luiz Fernando Bicalho Date: Fri, 30 May 2025 11:52:42 -0300 Subject: [PATCH 13/16] Refactor IQueryable extensions and improve syntax This commit refactors several classes to use extension methods for `IQueryable`, enhancing readability and maintainability. Key changes include: - Updated `Filters`, `Aggregates`, and `UpdateQuery` in `QueryableExtensions.cs` to be extension methods for fluent syntax. - Modernized string splitting in `Aggregator.cs` with improved syntax. - Refactored aggregate handling in `EnumerableExtensions.cs` for consistency. - Enhanced null handling and exception throwing in `Filter.cs` using the null-coalescing operator. - Simplified array initializations in `MockData.cs` and `Program.cs`. - Updated tests in `KendoNET.DynamicLinq.Test` to align with new method signatures. These changes improve code clarity and align with modern C# practices. --- .../QueryableExtensions.cs | 6 +-- src/Aggregator.cs | 2 +- src/EnumerableExtensions.cs | 4 +- src/Filter.cs | 34 +++---------- src/QueryableExtensions.cs | 49 +++++++++---------- .../Models/MockData.cs | 8 +-- .../Program.cs | 48 +++++++++--------- .../AggregatorTestSystem.cs | 12 ++--- .../CustomJsonSerializerOptions.cs | 8 +-- .../Data/MockContext.cs | 10 ++-- test/KendoNET.DynamicLinq.Test/FilterTest.cs | 6 +-- .../FilterTestSystem.cs | 6 +-- 12 files changed, 84 insertions(+), 109 deletions(-) diff --git a/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs b/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs index 85022d0..33d5f72 100644 --- a/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs +++ b/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs @@ -106,14 +106,14 @@ public static async Task> ToDataSourceResultAsync(this IQ var errors = new List(); // Filter the data first - queryable = QueryableExtensions.Filters(queryable, filter, errors); + queryable = queryable.Filters(filter, errors); // Calculate the total number of records (needed for paging) var total = await queryable.CountAsync(ct); // Calculate the aggregates - var aggregate = QueryableExtensions.Aggregates(queryable, aggregates); - queryable = QueryableExtensions.UpdateQuery(queryable, take, skip, sort, group); + var aggregate = queryable.Aggregates(aggregates); + queryable = queryable.UpdateQuery(take, skip, sort, group); var result = new DataSourceResult { diff --git a/src/Aggregator.cs b/src/Aggregator.cs index 542c698..17117e7 100644 --- a/src/Aggregator.cs +++ b/src/Aggregator.cs @@ -64,7 +64,7 @@ public class Aggregator /// private static string ConvertTitleCase(string str) { - var tokens = str.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries); + var tokens = str.Split([" "], StringSplitOptions.RemoveEmptyEntries); for (var i = 0; i < tokens.Length; i++) { var token = tokens[i]; diff --git a/src/EnumerableExtensions.cs b/src/EnumerableExtensions.cs index 346adcc..2c2cb9c 100644 --- a/src/EnumerableExtensions.cs +++ b/src/EnumerableExtensions.cs @@ -50,7 +50,7 @@ public static IEnumerable GroupByMany(this IEnumerable new GroupResult { Value = g.Key, - Aggregates = QueryableExtensions.Aggregates(g.AsQueryable(), selector.Aggregates), + Aggregates = g.AsQueryable().Aggregates(selector.Aggregates), HasSubgroups = groupSelectors.Length > 1, Count = g.Count(), Items = g.GroupByMany(nextSelectors), // Recursivly group the next selectors @@ -61,7 +61,7 @@ public static IEnumerable GroupByMany(this IEnumerable new GroupResult { - Aggregates = QueryableExtensions.Aggregates(elements.AsQueryable(), null), + Aggregates = elements.AsQueryable().Aggregates(null), Count = 1, HasSubgroups = false, SelectorField = string.Empty, diff --git a/src/Filter.cs b/src/Filter.cs index 63f9d80..990994b 100644 --- a/src/Filter.cs +++ b/src/Filter.cs @@ -121,7 +121,7 @@ public string ToExpression(Type type, IList filters) throw new NotSupportedException($"Operator {Operator} not support non-string type"); } - int index = filters.IndexOf(this); + var index = filters.IndexOf(this); var comparison = Operators[Operator]; if (Operator == "doesnotcontain") @@ -219,11 +219,7 @@ public Expression ToLambdaExpression(ParameterExpression parameter, IList(ParameterExpression parameter, IList(ParameterExpression parameter, IList(ParameterExpression parameter, IList internal static Type GetLastPropertyType(Type type, string path) { - Type currentType = type; + var currentType = type; /* Searches for the public property with the specified name */ /* Used in versions above 3.1.0 */ foreach (var propertyName in path.Split('.')) { - PropertyInfo? property = currentType.GetProperty(propertyName); - if (property == null) - { - throw new ArgumentException($"Property '{propertyName}' not found in type '{currentType.Name}'"); - } + var property = currentType.GetProperty(propertyName) ?? throw new ArgumentException($"Property '{propertyName}' not found in type '{currentType.Name}'"); currentType = property.PropertyType; } diff --git a/src/QueryableExtensions.cs b/src/QueryableExtensions.cs index 9de2c48..98b1cf1 100644 --- a/src/QueryableExtensions.cs +++ b/src/QueryableExtensions.cs @@ -112,9 +112,9 @@ public static DataSourceResult ToDataSourceResult(this IQueryable query var total = queryable.Count(); // Calculate the aggregates - var aggregate = Aggregates(queryable, aggregates); + var aggregate = queryable.Aggregates(aggregates); - queryable = UpdateQuery(queryable, take, skip, sort, group); + queryable = queryable.UpdateQuery(take, skip, sort, group); var result = new DataSourceResult { @@ -145,7 +145,7 @@ public static DataSourceResult ToDataSourceResult(this IQueryable query /// Updates the IQueryable with sorting and paging. /// /// - public static IQueryable UpdateQuery(IQueryable queryable, int take, int skip, IEnumerable sort, IEnumerable? group) + public static IQueryable UpdateQuery(this IQueryable queryable, int take, int skip, IEnumerable sort, IEnumerable? group) { if (group?.Any() == true) { @@ -176,7 +176,7 @@ public static IQueryable UpdateQuery(IQueryable queryable, int take, in /// /// /// - public static IQueryable Filters(IQueryable queryable, Filter filter, List errors) + public static IQueryable Filters(this IQueryable queryable, Filter filter, List errors) { if (filter?.Logic != null) { @@ -226,7 +226,7 @@ public static IQueryable Filters(IQueryable queryable, Filter filter, L /// /// /// - public static object? Aggregates(IQueryable queryable, IEnumerable? aggregates) + public static object? Aggregates(this IQueryable queryable, IEnumerable? aggregates) { if (aggregates?.Any() == true) { @@ -239,11 +239,7 @@ public static IQueryable Filters(IQueryable queryable, Filter filter, L var fieldProps = new Dictionary(); foreach (var aggregate in group) { - var prop = typeof(T).GetProperty(aggregate.Field); - if (prop == null) - { - throw new ArgumentException($"Property '{aggregate.Field}' does not exist on type '{typeof(T).Name}'."); - } + var prop = typeof(T).GetProperty(aggregate.Field) ?? throw new ArgumentException($"Property '{aggregate.Field}' does not exist on type '{typeof(T).Name}'."); var param = Expression.Parameter(typeof(T), "s"); var selector = aggregate.Aggregate == "count" && (Nullable.GetUnderlyingType(prop.PropertyType) != null) ? Expression.Lambda(Expression.NotEqual(Expression.MakeMemberAccess(param, prop), Expression.Constant(null, prop.PropertyType)), param) @@ -330,7 +326,7 @@ private static Filter PreliminaryWork(Type type, Filter filter) // When we have a decimal value, it gets converted to an integer/double that will result in the query break var currentPropertyType = Filter.GetLastPropertyType(type, filter.Field); - if ((currentPropertyType == typeof(decimal) || currentPropertyType == typeof(decimal?)) && decimal.TryParse(filter.Value.ToString(), out decimal number)) + if ((currentPropertyType == typeof(decimal) || currentPropertyType == typeof(decimal?)) && decimal.TryParse(filter.Value.ToString(), out var number)) { filter.Value = number; return filter; @@ -350,23 +346,26 @@ private static Filter PreliminaryWork(Type type, Filter filter) if (localTime.Hour != 0 || localTime.Minute != 0 || localTime.Second != 0) return filter; - var newFilter = new Filter { Logic = "and" }; - newFilter.Filters = new List + var newFilter = new Filter { + Logic = "and", + Filters = + [ // Instead of comparing for exact equality, we compare as greater than the start of the day... - new() { - Field = filter.Field, - Filters = filter.Filters??[], - Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, 0, 0, 0,DateTimeKind.Unspecified), - Operator = "gte" - }, + new() { + Field = filter.Field, + Filters = filter.Filters??[], + Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, 0, 0, 0,DateTimeKind.Unspecified), + Operator = "gte" + }, // ...and less than the end of that same day (we're making an additional filter here) - new() { - Field = filter.Field, - Filters = filter.Filters??[], - Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, 23, 59, 59,DateTimeKind.Unspecified), - Operator = "lte" - } + new() { + Field = filter.Field, + Filters = filter.Filters??[], + Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, 23, 59, 59,DateTimeKind.Unspecified), + Operator = "lte" + } + ] }; return newFilter; diff --git a/test/KendoNET.DynamicLinq.ConsoleApp/Models/MockData.cs b/test/KendoNET.DynamicLinq.ConsoleApp/Models/MockData.cs index 2877a8e..538ecd4 100644 --- a/test/KendoNET.DynamicLinq.ConsoleApp/Models/MockData.cs +++ b/test/KendoNET.DynamicLinq.ConsoleApp/Models/MockData.cs @@ -1,11 +1,11 @@ -using System; +using System; namespace KendoNET.DynamicLinq.ConsoleApp.Models { public static class MockData { - public static readonly Employee[] Employees = new Employee[] - { + public static readonly Employee[] Employees = + [ new Employee { Number = 10, @@ -71,6 +71,6 @@ public static class MockData Weight = 99.8F, Birthday = new DateTime(2005, 3, 16, 8, 0, 0) } - }; + ]; } } \ No newline at end of file diff --git a/test/KendoNET.DynamicLinq.ConsoleApp/Program.cs b/test/KendoNET.DynamicLinq.ConsoleApp/Program.cs index d63299e..ac412bd 100644 --- a/test/KendoNET.DynamicLinq.ConsoleApp/Program.cs +++ b/test/KendoNET.DynamicLinq.ConsoleApp/Program.cs @@ -21,8 +21,8 @@ static void Main(string[] args) Console.WriteLine("----------------------------------------"); /* Test 1 (Aggregate)*/ - var result = MockData.Employees.AsQueryable().ToDataSourceResult(1, 2, null, null, new[] - { + var result = MockData.Employees.AsQueryable().ToDataSourceResult(1, 2, null, null, + [ new Aggregator { Aggregate = "sum", @@ -33,7 +33,7 @@ static void Main(string[] args) Aggregate = "average", Field = "Salary" } - }, null); + ], null); Console.WriteLine("\r\n/********** Test 1 (Aggregate) **********/"); Console.WriteLine("Expectation: { Salary = { sum = 24750, average = 4125 } }"); @@ -41,14 +41,14 @@ static void Main(string[] args) /* Test 2 (DateTime)*/ - result = MockData.Employees.AsQueryable().ToDataSourceResult(10, 0, new[] - { + result = MockData.Employees.AsQueryable().ToDataSourceResult(10, 0, + [ new Sort { Field = "Name", Dir = "asc" } - }, + ], new Filter { Field = "Birthday", @@ -63,18 +63,18 @@ static void Main(string[] args) /* Test 3 (String Method)*/ - result = MockData.Employees.AsQueryable().ToDataSourceResult(10, 0, new[] - { + result = MockData.Employees.AsQueryable().ToDataSourceResult(10, 0, + [ new Sort { Field = "Name", Dir = "asc" } - }, + ], new Filter { - Filters = new[] - { + Filters = + [ new Filter { Field = "Introduce", @@ -87,7 +87,7 @@ static void Main(string[] args) Operator = "doesnotcontain", Value = "Monie" } - }, + ], Logic = "and" }, null, null); @@ -97,19 +97,19 @@ static void Main(string[] args) /* Test 4 (Double)*/ - result = MockData.Employees.AsQueryable().ToDataSourceResult(10, 0, new[] - { + result = MockData.Employees.AsQueryable().ToDataSourceResult(10, 0, + [ new Sort { Field = "Name", Dir = "asc" } - }, + ], new Filter { Logic = "or", - Filters = new[] - { + Filters = + [ new Filter { Field = "Height", @@ -122,7 +122,7 @@ static void Main(string[] args) Operator = "lte", Value = 166 } - } + ] }, null, null); Console.WriteLine("\r\n/********** Test 4 (Double) **********/"); @@ -131,19 +131,19 @@ static void Main(string[] args) /* Test 5 (Float)*/ - result = MockData.Employees.AsQueryable().ToDataSourceResult(10, 0, new[] - { + result = MockData.Employees.AsQueryable().ToDataSourceResult(10, 0, + [ new Sort { Field = "Name", Dir = "asc" } - }, + ], new Filter { Logic = "and", - Filters = new[] - { + Filters = + [ new Filter { Field = "Weight", @@ -156,7 +156,7 @@ static void Main(string[] args) Operator = "lte", Value = 82.8F } - } + ] }, null, null); Console.WriteLine("\r\n/********** Test 5 (Float) **********/"); diff --git a/test/KendoNET.DynamicLinq.Test/AggregatorTestSystem.cs b/test/KendoNET.DynamicLinq.Test/AggregatorTestSystem.cs index fa01c5e..f6382e6 100644 --- a/test/KendoNET.DynamicLinq.Test/AggregatorTestSystem.cs +++ b/test/KendoNET.DynamicLinq.Test/AggregatorTestSystem.cs @@ -46,14 +46,14 @@ public void Setup() [Test] public void InputParameter_DecimalSum_CheckResultObjectString() { - var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(10, 0, null, null, new[] - { + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(10, 0, null, null, + [ new Aggregator { Aggregate = "sum", Field = "Salary" } - }, null); + ], null); object expectedObject = "{ Salary = { sum = 14850 } }"; ClassicAssert.AreEqual(expectedObject, result.Aggregates.ToString()); @@ -85,8 +85,8 @@ public void InputDataSourceRequest_DecimalSum_CheckResultSum(DataSourceRequest d [Test] public void InputParameter_ManyAggregators_CheckResultObjectString() { - var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(10, 0, null, null, new[] - { + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(10, 0, null, null, + [ new Aggregator { Aggregate = "sum", @@ -102,7 +102,7 @@ public void InputParameter_ManyAggregators_CheckResultObjectString() Aggregate = "max", Field = "Number" }, - }, null); + ], null); object expectedObject = "{ Salary = { sum = 14850, average = 2970 }, Number = { max = 6 } }"; ClassicAssert.AreEqual(expectedObject, result.Aggregates.ToString()); diff --git a/test/KendoNET.DynamicLinq.Test/CustomJsonSerializerOptions.cs b/test/KendoNET.DynamicLinq.Test/CustomJsonSerializerOptions.cs index 3d7cbf9..ef7053e 100644 --- a/test/KendoNET.DynamicLinq.Test/CustomJsonSerializerOptions.cs +++ b/test/KendoNET.DynamicLinq.Test/CustomJsonSerializerOptions.cs @@ -8,7 +8,7 @@ namespace KendoNET.DynamicLinq.Test { public class CustomJsonSerializerOptions { - public static readonly JsonSerializerOptions DefaultOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + public static readonly JsonSerializerOptions DefaultOptions = new() { PropertyNameCaseInsensitive = true }; static CustomJsonSerializerOptions() { @@ -35,7 +35,7 @@ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS if (reader.TokenType == JsonTokenType.Number) { - if (reader.TryGetInt64(out long l)) + if (reader.TryGetInt64(out var l)) { return l; } @@ -45,7 +45,7 @@ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS if (reader.TokenType == JsonTokenType.String) { - if (reader.TryGetDateTime(out DateTime datetime)) + if (reader.TryGetDateTime(out var datetime)) { return datetime; } @@ -53,7 +53,7 @@ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS return reader.GetString(); } - using JsonDocument document = JsonDocument.ParseValue(ref reader); + using var document = JsonDocument.ParseValue(ref reader); return document.RootElement.Clone(); } diff --git a/test/KendoNET.DynamicLinq.Test/Data/MockContext.cs b/test/KendoNET.DynamicLinq.Test/Data/MockContext.cs index 3de60f3..8067da2 100644 --- a/test/KendoNET.DynamicLinq.Test/Data/MockContext.cs +++ b/test/KendoNET.DynamicLinq.Test/Data/MockContext.cs @@ -1,11 +1,11 @@ -using System; +using System; +using KendoNET.DynamicLinq.Test.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using KendoNET.DynamicLinq.Test.Models; namespace KendoNET.DynamicLinq.Test.Data { - public class MockContext : DbContext + public class MockContext(DbContextOptions options) : DbContext(options) { private static MockContext _defaultDbContext; @@ -24,10 +24,6 @@ public static MockContext GetDefaultInMemoryDbContext() return _defaultDbContext; } - public MockContext(DbContextOptions options) : base(options) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { // Add employee data diff --git a/test/KendoNET.DynamicLinq.Test/FilterTest.cs b/test/KendoNET.DynamicLinq.Test/FilterTest.cs index 698b813..89a9125 100644 --- a/test/KendoNET.DynamicLinq.Test/FilterTest.cs +++ b/test/KendoNET.DynamicLinq.Test/FilterTest.cs @@ -35,15 +35,15 @@ public void InputParameter_SubPropertyContains_CheckResultCount() var result2 = _dbContext.Employee.AsQueryable().ToDataSourceResult(10, 0, null, new Filter { - Filters = new[] - { + Filters = + [ new Filter { Field = "Company.Name", Operator = "contains", Value = "Microsoft" } - }, + ], Logic = "and" }); diff --git a/test/KendoNET.DynamicLinq.Test/FilterTestSystem.cs b/test/KendoNET.DynamicLinq.Test/FilterTestSystem.cs index 143cdd7..f83267f 100644 --- a/test/KendoNET.DynamicLinq.Test/FilterTestSystem.cs +++ b/test/KendoNET.DynamicLinq.Test/FilterTestSystem.cs @@ -37,15 +37,15 @@ public void InputParameter_SubPropertyContains_CheckResultCount() var result2 = _dbContext.Employee.AsQueryable().ToDataSourceResult(10, 0, null, new Filter { - Filters = new[] - { + Filters = + [ new Filter { Field = "Company.Name", Operator = "contains", Value = "Microsoft" } - }, + ], Logic = "and" }); From 29af266e35cf650427407b2ef1555e10eef52010 Mon Sep 17 00:00:00 2001 From: Luiz Fernando Bicalho Date: Fri, 30 May 2025 12:09:29 -0300 Subject: [PATCH 14/16] Rename class for clarity in async querying Changed class name from `QueryableExtensionsAsync` to `QueryableAsyncExtensionsAsync` in `QueryableAsyncExtensions.cs` to better reflect its purpose as an asynchronous extension for querying data. --- .../{QueryableExtensions.cs => QueryableAsyncExtensions.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename KendoNET.DynamicLinq.EFCore/{QueryableExtensions.cs => QueryableAsyncExtensions.cs} (99%) diff --git a/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs b/KendoNET.DynamicLinq.EFCore/QueryableAsyncExtensions.cs similarity index 99% rename from KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs rename to KendoNET.DynamicLinq.EFCore/QueryableAsyncExtensions.cs index 33d5f72..9815d14 100644 --- a/KendoNET.DynamicLinq.EFCore/QueryableExtensions.cs +++ b/KendoNET.DynamicLinq.EFCore/QueryableAsyncExtensions.cs @@ -8,7 +8,7 @@ namespace KendoNET.DynamicLinq.EFCore { - public static class QueryableExtensionsAsync + public static class QueryableAsyncExtensionsAsync { /// /// Applies data processing (paging, sorting and filtering) over IQueryable using Dynamic Linq. From dcfe0532ef30cd827d7272f45e741e2eb6ce1a35 Mon Sep 17 00:00:00 2001 From: Luiz Fernando Bicalho Date: Fri, 30 May 2025 13:01:50 -0300 Subject: [PATCH 15/16] Make Filter parameters nullable across multiple files This commit updates the `Filter` parameter in the `ToDataSourceResultAsync` and `ToDataSourceResult` methods, as well as the `Filters` method, to be nullable (`Filter?`). Additionally, the `Filter` property in the `DataSourceRequest` class is also changed to nullable. These changes enhance the handling of optional filters, improving code robustness. --- KendoNET.DynamicLinq.EFCore/QueryableAsyncExtensions.cs | 2 +- src/DataSourceRequest.cs | 2 +- src/QueryableExtensions.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/KendoNET.DynamicLinq.EFCore/QueryableAsyncExtensions.cs b/KendoNET.DynamicLinq.EFCore/QueryableAsyncExtensions.cs index 9815d14..3d04f3d 100644 --- a/KendoNET.DynamicLinq.EFCore/QueryableAsyncExtensions.cs +++ b/KendoNET.DynamicLinq.EFCore/QueryableAsyncExtensions.cs @@ -98,7 +98,7 @@ public static async Task> ToDataSourceResultAsync(this IQ int take, int skip, IEnumerable sort, - Filter filter, + Filter? filter, IEnumerable? aggregates, IEnumerable? group, CancellationToken ct) diff --git a/src/DataSourceRequest.cs b/src/DataSourceRequest.cs index 1232db5..24ee0c4 100644 --- a/src/DataSourceRequest.cs +++ b/src/DataSourceRequest.cs @@ -22,7 +22,7 @@ public class DataSourceRequest /// /// Specifies the requested filter. /// - public Filter Filter { get; set; } = new(); + public Filter? Filter { get; set; } /// /// Specifies the requested grouping . diff --git a/src/QueryableExtensions.cs b/src/QueryableExtensions.cs index 98b1cf1..8a71a39 100644 --- a/src/QueryableExtensions.cs +++ b/src/QueryableExtensions.cs @@ -99,7 +99,7 @@ public static DataSourceResult ToDataSourceResult(this IQueryable query int take, int skip, IEnumerable sort, - Filter filter, + Filter? filter, IEnumerable? aggregates, IEnumerable? group) { @@ -176,7 +176,7 @@ public static IQueryable UpdateQuery(this IQueryable queryable, int tak /// /// /// - public static IQueryable Filters(this IQueryable queryable, Filter filter, List errors) + public static IQueryable Filters(this IQueryable queryable, Filter? filter, List errors) { if (filter?.Logic != null) { From 8bea725a38b2cb8810366c7b3e88bc59421a9e16 Mon Sep 17 00:00:00 2001 From: Luiz Fernando Bicalho Date: Fri, 30 May 2025 13:28:07 -0300 Subject: [PATCH 16/16] Enhance documentation and improve code consistency Updated XML documentation across multiple files, including `QueryableAsyncExtensions.cs`, `DataSourceRequest.cs`, and `GroupResult.cs`, to clarify class properties and methods. Improved culture-invariant behavior in `Aggregator.cs` and ensured nullable support in `Filter.cs`. Enhanced type retrieval in `DataSourceResult.cs` and introduced `GroupSelector.cs` with comprehensive documentation. --- .../QueryableAsyncExtensions.cs | 11 +++++-- src/Aggregator.cs | 2 +- src/DataSourceRequest.cs | 16 ++++++---- src/DataSourceResult.cs | 4 +-- src/EnumerableExtensions.cs | 29 ++++++++++++++----- src/Filter.cs | 5 ++-- src/Group.cs | 6 ++++ src/GroupResult.cs | 26 ++++++++++++++++- src/GroupSelector.cs | 15 ++++++++++ src/QueryableExtensions.cs | 15 ++++++++++ 10 files changed, 108 insertions(+), 21 deletions(-) diff --git a/KendoNET.DynamicLinq.EFCore/QueryableAsyncExtensions.cs b/KendoNET.DynamicLinq.EFCore/QueryableAsyncExtensions.cs index 3d04f3d..e2d36f9 100644 --- a/KendoNET.DynamicLinq.EFCore/QueryableAsyncExtensions.cs +++ b/KendoNET.DynamicLinq.EFCore/QueryableAsyncExtensions.cs @@ -1,13 +1,17 @@ -using System; +using Microsoft.EntityFrameworkCore; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; namespace KendoNET.DynamicLinq.EFCore { + /// + /// Provides extension methods for asynchronously applying Kendo-style data operations (paging, sorting, filtering, grouping, and aggregation) + /// to sources using Dynamic LINQ and Entity Framework Core. + /// public static class QueryableAsyncExtensionsAsync { /// @@ -19,6 +23,7 @@ public static class QueryableAsyncExtensionsAsync /// Specifies how many items to skip. /// Specifies the current sort order. /// Specifies the current filter. + /// /// A DataSourceResult object populated from the processed IQueryable. /// /// @@ -46,6 +51,7 @@ public static Task> ToDataSourceResultAsync(this IQueryab /// The type of the IQueryable. /// The IQueryable which should be processed. /// The DataSourceRequest object containing take, skip, sort, filter, aggregates, and groups data. + /// /// A DataSourceResult object populated from the processed IQueryable. /// /// @@ -78,6 +84,7 @@ public static Task> ToDataSourceResultAsync(this IQueryab /// Specifies the current filter. /// Specifies the current aggregates. /// Specifies the current groups. + /// /// A DataSourceResult object populated from the processed IQueryable. /// /// diff --git a/src/Aggregator.cs b/src/Aggregator.cs index 17117e7..500c139 100644 --- a/src/Aggregator.cs +++ b/src/Aggregator.cs @@ -68,7 +68,7 @@ private static string ConvertTitleCase(string str) for (var i = 0; i < tokens.Length; i++) { var token = tokens[i]; - tokens[i] = token.Substring(0, 1).ToUpper() + token.Substring(1); + tokens[i] = $"{token.Substring(0, 1).ToUpperInvariant()}{token.Substring(1)}"; } return string.Join(" ", tokens); diff --git a/src/DataSourceRequest.cs b/src/DataSourceRequest.cs index 24ee0c4..9e1eb10 100644 --- a/src/DataSourceRequest.cs +++ b/src/DataSourceRequest.cs @@ -2,35 +2,39 @@ namespace KendoNET.DynamicLinq { + /// + /// Represents a request for data operations such as paging, sorting, filtering, grouping, and aggregation. + /// Used by Kendo UI DataSource to describe the desired data manipulation. + /// public class DataSourceRequest { /// - /// Specifies how many items to take. + /// Gets or sets the number of items to take (page size). /// public int Take { get; set; } /// - /// Specifies how many items to skip. + /// Gets or sets the number of items to skip (used for paging). /// public int Skip { get; set; } /// - /// Specifies the requested sort order. + /// Gets or sets the collection of sort expressions that define the requested sort order. /// public IEnumerable Sort { get; set; } = []; /// - /// Specifies the requested filter. + /// Gets or sets the filter expression that defines the requested filtering. /// public Filter? Filter { get; set; } /// - /// Specifies the requested grouping . + /// Gets or sets the collection of group expressions that define the requested grouping. /// public IEnumerable? Group { get; set; } /// - /// Specifies the requested aggregators. + /// Gets or sets the collection of aggregate expressions that define the requested aggregations. /// public IEnumerable? Aggregate { get; set; } } diff --git a/src/DataSourceResult.cs b/src/DataSourceResult.cs index 0a57492..960b1ba 100644 --- a/src/DataSourceResult.cs +++ b/src/DataSourceResult.cs @@ -44,8 +44,8 @@ public class DataSourceResult /// private static Type[] GetKnownTypes() { - var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a?.FullName?.StartsWith("DynamicClasses") ?? false); - return assembly == null ? [] : assembly.GetTypes().Where(t => t.Name.StartsWith("DynamicClass")).ToArray(); + var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a?.FullName?.StartsWith("DynamicClasses", StringComparison.InvariantCulture) ?? false); + return assembly == null ? [] : assembly.GetTypes().Where(t => t.Name.StartsWith("DynamicClass", StringComparison.InvariantCulture)).ToArray(); } } } \ No newline at end of file diff --git a/src/EnumerableExtensions.cs b/src/EnumerableExtensions.cs index 2c2cb9c..fbafa1c 100644 --- a/src/EnumerableExtensions.cs +++ b/src/EnumerableExtensions.cs @@ -5,12 +5,21 @@ namespace KendoNET.DynamicLinq { + /// + /// Provides extension methods for grouping enumerable collections by multiple selectors. + /// public static class EnumerableExtensions { /// - /// Group elements by multiple selectors. + /// Groups the elements of a sequence according to multiple group selectors specified as objects. /// - /// + /// The type of the elements in the source sequence. + /// The sequence of elements to group. + /// A collection of objects that define the grouping fields and aggregates. + /// + /// An where each element represents a group and its subgroups, including aggregate results. + /// + /// Thrown if an arithmetic operation in the grouping or aggregation overflows. public static IEnumerable GroupByMany(this IEnumerable elements, IEnumerable groupSelectors) { // Create a new list of Kendo Group Selectors @@ -34,10 +43,16 @@ public static IEnumerable GroupByMany(this IEnumerable - /// Group elements by multiple selectors. + /// Groups the elements of a sequence according to multiple group selectors specified as objects. /// - /// - /// + /// The type of the elements in the source sequence. + /// The sequence of elements to group. + /// An array of objects that define the grouping selectors and aggregates. + /// + /// An where each element represents a group and its subgroups, including aggregate results. + /// + /// Thrown if the group selectors are invalid. + /// Thrown if an arithmetic operation in the grouping or aggregation overflows. public static IEnumerable GroupByMany(this IEnumerable elements, params GroupSelector[] groupSelectors) { if (groupSelectors.Length > 0) @@ -45,7 +60,7 @@ public static IEnumerable GroupByMany(this IEnumerable new GroupResult { @@ -53,7 +68,7 @@ public static IEnumerable GroupByMany(this IEnumerable 1, Count = g.Count(), - Items = g.GroupByMany(nextSelectors), // Recursivly group the next selectors + Items = g.GroupByMany(nextSelectors), // Recursively group the next selectors SelectorField = selector.Field }); } diff --git a/src/Filter.cs b/src/Filter.cs index 990994b..ba87d96 100644 --- a/src/Filter.cs +++ b/src/Filter.cs @@ -41,12 +41,12 @@ public class Filter /// Gets or sets the child filter expressions. Set to null if there are no child expressions. /// [DataMember(Name = "filters")] - public IEnumerable Filters { get; set; } = []; + public IEnumerable? Filters { get; set; } /// /// Mapping of Kendo DataSource filtering operators to Dynamic Linq /// - private static readonly IDictionary Operators = new Dictionary + private static readonly Dictionary Operators = new Dictionary { { "eq", "=" }, { "neq", "!=" }, @@ -104,6 +104,7 @@ private void Collect(IList filters) /// /// Converts the filter expression to a predicate suitable for Dynamic Linq e.g. "Field1 = @1 and Field2.Contains(@2)" /// + /// /// A list of flattened filters. /// /// diff --git a/src/Group.cs b/src/Group.cs index a62d6b4..3137e09 100644 --- a/src/Group.cs +++ b/src/Group.cs @@ -3,8 +3,14 @@ namespace KendoNET.DynamicLinq { + /// + /// Represents a group expression of Kendo DataSource, including sorting and aggregation information. + /// public class Group : Sort { + /// + /// Gets or sets the collection of aggregate expressions to be applied to the group. + /// [DataMember(Name = "aggregates")] public IEnumerable Aggregates { get; set; } = []; } diff --git a/src/GroupResult.cs b/src/GroupResult.cs index 48c6f4c..c2da64a 100644 --- a/src/GroupResult.cs +++ b/src/GroupResult.cs @@ -4,29 +4,53 @@ namespace KendoNET.DynamicLinq { // The response format of the group schema : https://docs.telerik.com/kendo-ui/api/javascript/data/datasource/configuration/schema#schemagroups + /// + /// Represents the result of a grouped query, compatible with the Kendo UI DataSource group schema. + /// [DataContract(Name = "groupresult")] public class GroupResult { - // Small letter properties are kendo js properties so please excuse the warnings + /// + /// Gets or sets the value of the group. This is typically the key by which the data is grouped. + /// [DataMember(Name = "value")] public object? Value { get; set; } + /// + /// Gets or sets the field name used for grouping. + /// public string SelectorField { get; set; } = string.Empty; + /// + /// Gets the field name and count in the format "FieldName (Count)". + /// Used by Kendo UI for group display. + /// [DataMember(Name = "field")] public string Field { get { return $"{this.SelectorField} ({this.Count})"; } } + /// + /// Gets or sets the number of items in the group. + /// public int Count { get; set; } + /// + /// Gets or sets the aggregate results for the group. + /// [DataMember(Name = "aggregates")] public object? Aggregates { get; set; } + /// + /// Gets or sets the subgroups or items within this group. + /// [DataMember(Name = "items")] public IEnumerable? Items { get; set; } + /// + /// Gets or sets a value indicating whether this group contains subgroups. + /// [DataMember(Name = "hasSubgroups")] public bool HasSubgroups { get; set; } // true if there are subgroups } diff --git a/src/GroupSelector.cs b/src/GroupSelector.cs index 372ede4..265ef75 100644 --- a/src/GroupSelector.cs +++ b/src/GroupSelector.cs @@ -3,10 +3,25 @@ namespace KendoNET.DynamicLinq { + /// + /// Represents a group selector for Kendo DataSource grouping operations. + /// + /// The type of the elements to group. public class GroupSelector { + /// + /// Gets or sets the selector function used to extract the grouping key from an element. + /// public Func Selector { get; set; } = _ => new object(); + + /// + /// Gets or sets the name of the field to group by. + /// public string Field { get; set; } = string.Empty; + + /// + /// Gets or sets the collection of aggregate expressions to apply to each group. + /// public IEnumerable Aggregates { get; set; } = []; } } diff --git a/src/QueryableExtensions.cs b/src/QueryableExtensions.cs index 8a71a39..2ff2f12 100644 --- a/src/QueryableExtensions.cs +++ b/src/QueryableExtensions.cs @@ -8,6 +8,10 @@ namespace KendoNET.DynamicLinq { + /// + /// Provides extension methods for asynchronously applying Kendo-style data operations (paging, sorting, filtering, grouping, and aggregation) + /// to sources using Dynamic LINQ + /// public static class QueryableExtensions { /// @@ -299,6 +303,16 @@ public static IQueryable Sort(this IQueryable queryable, IEnumerable + /// Applies paging to the by skipping a specified number of elements and then taking a specified number of elements. + /// + /// The type of the elements in the source queryable. + /// The source to page. + /// The number of elements to take (page size). + /// The number of elements to skip (used for paging). + /// + /// An that contains the elements that occur after skipping elements and then taking elements from the input sequence. + /// public static IQueryable Page(this IQueryable queryable, int take, int skip) { return queryable.Skip(skip).Take(take); @@ -307,6 +321,7 @@ public static IQueryable Page(this IQueryable queryable, int take, int /// /// Pretreatment of specific DateTime type and convert some illegal value type /// + /// /// /// private static Filter PreliminaryWork(Type type, Filter filter)