diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f998851e..79f0fba2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,13 +22,13 @@ jobs: - uses: actions/setup-dotnet@v4 with: - dotnet-version: '9.0.x' + dotnet-version: '10.0.x' - name: Build run: | dotnet build ./src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj -c Release -p:buildType=azure-pipelines-ci - - name: Run Tests EFCore net9.0 + - name: Run Tests EFCore net10.0 run: | dotnet test ./test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj -c Release -p:buildType=azure-pipelines-ci @@ -76,39 +76,24 @@ jobs: - name: Build run: | dotnet build ./src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj -c Debug -p:buildType=azure-pipelines-ci - - - name: Run Tests EF net8.0 (with Coverage) + + - name: Run Tests EFCore .NET 10 (with Coverage) run: | - dotnet-coverage collect 'dotnet test ./test/EntityFramework.DynamicLinq.Tests/EntityFramework.DynamicLinq.Tests.csproj --configuration Debug --framework net8.0 -p:buildType=azure-pipelines-ci' -f xml -o dynamic-coverage-ef.xml + dotnet-coverage collect 'dotnet test ./test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj --configuration Debug -p:buildType=azure-pipelines-ci' -f xml -o dynamic-coverage-efcore.xml - - name: Run Tests EFCore net8.0 (with Coverage) + - name: Run Tests EF .NET 10 (with Coverage) run: | - dotnet-coverage collect 'dotnet test ./test/System.Linq.Dynamic.Core.Tests.Net8/System.Linq.Dynamic.Core.Tests.Net8.csproj --configuration Debug --framework net8.0 -p:buildType=azure-pipelines-ci' -f xml -o dynamic-coverage-efcore.xml + dotnet-coverage collect 'dotnet test ./test/EntityFramework.DynamicLinq.Tests/EntityFramework.DynamicLinq.Tests.csproj --configuration Debug --framework net10.0 -p:buildType=azure-pipelines-ci' -f xml -o dynamic-coverage-ef.xml - - name: Run Tests Newtonsoft.Json .NET 8 (with Coverage) + - name: Run Tests Newtonsoft.Json .NET 10 (with Coverage) run: | - dotnet-coverage collect 'dotnet test ./test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/System.Linq.Dynamic.Core.NewtonsoftJson.Tests.csproj --configuration Debug --framework net8.0 -p:buildType=azure-pipelines-ci' -f xml -o dynamic-coverage-newtonsoftjson.xml + dotnet-coverage collect 'dotnet test ./test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/System.Linq.Dynamic.Core.NewtonsoftJson.Tests.csproj --configuration Debug --framework net10.0 -p:buildType=azure-pipelines-ci' -f xml -o dynamic-coverage-newtonsoftjson.xml - - name: Run Tests System.Text.Json .NET 8 (with Coverage) + - name: Run Tests System.Text.Json .NET 10 (with Coverage) run: | - dotnet-coverage collect 'dotnet test ./test/System.Linq.Dynamic.Core.SystemTextJson.Tests/System.Linq.Dynamic.Core.SystemTextJson.Tests.csproj --configuration Debug --framework net8.0 -p:buildType=azure-pipelines-ci' -f xml -o dynamic-coverage-systemtextjson.xml + dotnet-coverage collect 'dotnet test ./test/System.Linq.Dynamic.Core.SystemTextJson.Tests/System.Linq.Dynamic.Core.SystemTextJson.Tests.csproj --configuration Debug --framework net10.0 -p:buildType=azure-pipelines-ci' -f xml -o dynamic-coverage-systemtextjson.xml - name: End analysis on SonarCloud if: ${{ steps.secret-check.outputs.run_analysis == 'true' }} run: | - dotnet sonarscanner end /d:sonar.token=${{ secrets.SONAR_TOKEN }} - - # - name: Run Tests EFCore net8.0 - # run: | - # dotnet test ./test/System.Linq.Dynamic.Core.Tests.Net7/System.Linq.Dynamic.Core.Tests.Net8.csproj -c Release -p:buildType=azure-pipelines-ci - # continue-on-error: true - - # - name: Run Tests EFCore net7.0 - # run: | - # dotnet test ./test/System.Linq.Dynamic.Core.Tests.Net7/System.Linq.Dynamic.Core.Tests.Net7.csproj -c Release -p:buildType=azure-pipelines-ci - # continue-on-error: true - - # - name: Run Tests EFCore net6.0 - # run: | - # dotnet test ./test/System.Linq.Dynamic.Core.Tests.Net6/System.Linq.Dynamic.Core.Tests.Net6.csproj -c Release -p:buildType=azure-pipelines-ci - # continue-on-error: true \ No newline at end of file + dotnet sonarscanner end /d:sonar.token=${{ secrets.SONAR_TOKEN }} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 210e2f20..4a4ae9bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,23 @@ -# v1.6.9 (10 October 2025) +# v1.7.1 (29 November 2025) +- [#961](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/961) - Fix Json when property value is null [bug] contributed by [StefH](https://github.com/StefH) +- [#962](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/962) - json: fix logic when property is not found [bug] contributed by [StefH](https://github.com/StefH) +- [#965](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/965) - Fix NumberParser for integer < int.MinValue [bug] contributed by [StefH](https://github.com/StefH) +- [#960](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/960) - json: follow up for not existing members [bug] +- [#964](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/964) - Integer numbers smaller than int.MinValue are not parsed correctly [bug] + +# v1.7.0 (15 November 2025) +- [#956](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/956) - Fix parsing Hex and Binary [bug] contributed by [StefH](https://github.com/StefH) +- [#957](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/957) - .NET 10 [feature] contributed by [StefH](https://github.com/StefH) +- [#958](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/958) - Support normalization of objects for Z.DynamicLinq.Json [feature] contributed by [StefH](https://github.com/StefH) +- [#955](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/955) - Hexadecimal und binary literals sometimes are interpreted as decimal [bug] + +# v1.6.10 (08 November 2025) +- [#953](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/953) - Fixed adding Enum and integer [bug] contributed by [StefH](https://github.com/StefH) +- [#954](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/954) - Fix ExpressionHelper.TryConvertTypes to generate correct Convert in case left or right is null [bug] contributed by [StefH](https://github.com/StefH) +- [#951](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/951) - Parsing error adding numeric constant to enum value [bug] +- [#952](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/952) - Json: How to handle not existing member [bug] + +# v1.6.9 (11 October 2025) - [#950](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/950) - DynamicExpressionParser - Handle indexed properties with any number of indices in expression [bug] contributed by [thibault-reigner](https://github.com/thibault-reigner) # v1.6.8 (28 September 2025) diff --git a/Generate-ReleaseNotes.bat b/Generate-ReleaseNotes.bat index aa50344d..809d22d8 100644 --- a/Generate-ReleaseNotes.bat +++ b/Generate-ReleaseNotes.bat @@ -1,5 +1,5 @@ rem https://github.com/StefH/GitHubReleaseNotes -SET version=v1.6.9 +SET version=v1.7.1 GitHubReleaseNotes --output CHANGELOG.md --exclude-labels known_issue out_of_scope not_planned invalid question documentation wontfix environment duplicate --language en --version %version% --token %GH_TOKEN% diff --git a/System.Linq.Dynamic.Core.sln b/System.Linq.Dynamic.Core.sln index 4bfd1ffa..7aaa3982 100644 --- a/System.Linq.Dynamic.Core.sln +++ b/System.Linq.Dynamic.Core.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31606.5 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11205.157 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8463ED7E-69FB-49AE-85CF-0791AFD98E38}" ProjectSection(SolutionItems) = preProject @@ -161,6 +161,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Linq.Dynamic.Core.Te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppPerformanceTest", "src-console\ConsoleAppPerformanceTest\ConsoleAppPerformanceTest.csproj", "{067C00CF-29FA-4643-814D-3A3C3C84634F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp_net10", "src-console\ConsoleApp_net10\ConsoleApp_net10.csproj", "{34C58129-07DB-287E-A29B-FA8EE8BAEB05}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10", "src\Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10\Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10.csproj", "{1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Linq.Dynamic.Core.Tests.Net9", "test\System.Linq.Dynamic.Core.Tests.Net9\System.Linq.Dynamic.Core.Tests.Net9.csproj", "{00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1057,6 +1063,54 @@ Global {067C00CF-29FA-4643-814D-3A3C3C84634F}.Release|x64.Build.0 = Release|Any CPU {067C00CF-29FA-4643-814D-3A3C3C84634F}.Release|x86.ActiveCfg = Release|Any CPU {067C00CF-29FA-4643-814D-3A3C3C84634F}.Release|x86.Build.0 = Release|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Debug|ARM.ActiveCfg = Debug|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Debug|ARM.Build.0 = Debug|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Debug|x64.ActiveCfg = Debug|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Debug|x64.Build.0 = Debug|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Debug|x86.ActiveCfg = Debug|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Debug|x86.Build.0 = Debug|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Release|Any CPU.Build.0 = Release|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Release|ARM.ActiveCfg = Release|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Release|ARM.Build.0 = Release|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Release|x64.ActiveCfg = Release|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Release|x64.Build.0 = Release|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Release|x86.ActiveCfg = Release|Any CPU + {34C58129-07DB-287E-A29B-FA8EE8BAEB05}.Release|x86.Build.0 = Release|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Debug|ARM.ActiveCfg = Debug|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Debug|ARM.Build.0 = Debug|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Debug|x64.ActiveCfg = Debug|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Debug|x64.Build.0 = Debug|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Debug|x86.ActiveCfg = Debug|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Debug|x86.Build.0 = Debug|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Release|Any CPU.Build.0 = Release|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Release|ARM.ActiveCfg = Release|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Release|ARM.Build.0 = Release|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Release|x64.ActiveCfg = Release|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Release|x64.Build.0 = Release|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Release|x86.ActiveCfg = Release|Any CPU + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520}.Release|x86.Build.0 = Release|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Debug|ARM.ActiveCfg = Debug|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Debug|ARM.Build.0 = Debug|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Debug|x64.ActiveCfg = Debug|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Debug|x64.Build.0 = Debug|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Debug|x86.ActiveCfg = Debug|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Debug|x86.Build.0 = Debug|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Release|Any CPU.Build.0 = Release|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Release|ARM.ActiveCfg = Release|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Release|ARM.Build.0 = Release|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Release|x64.ActiveCfg = Release|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Release|x64.Build.0 = Release|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Release|x86.ActiveCfg = Release|Any CPU + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1117,6 +1171,9 @@ Global {C774DAE7-54A0-4FCD-A3B7-3CB63D7E112D} = {DBD7D9B6-FCC7-4650-91AF-E6457573A68F} {CEBE3A33-4814-42A4-BD8E-F7F2308A4C8C} = {8463ED7E-69FB-49AE-85CF-0791AFD98E38} {067C00CF-29FA-4643-814D-3A3C3C84634F} = {7971CAEB-B9F2-416B-966D-2D697C4C1E62} + {34C58129-07DB-287E-A29B-FA8EE8BAEB05} = {7971CAEB-B9F2-416B-966D-2D697C4C1E62} + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520} = {DBD7D9B6-FCC7-4650-91AF-E6457573A68F} + {00C5928F-3846-5C1A-6AFB-DFD2149EA3E2} = {8463ED7E-69FB-49AE-85CF-0791AFD98E38} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {94C56722-194E-4B8B-BC23-B3F754E89A20} diff --git a/src-console/ConsoleAppEF2.0.2_InMemory/ConsoleApp_netcore2.0_EF2.0.2_InMemory.csproj b/src-console/ConsoleAppEF2.0.2_InMemory/ConsoleApp_netcore2.0_EF2.0.2_InMemory.csproj index 3a5d7189..9fd2818d 100644 --- a/src-console/ConsoleAppEF2.0.2_InMemory/ConsoleApp_netcore2.0_EF2.0.2_InMemory.csproj +++ b/src-console/ConsoleAppEF2.0.2_InMemory/ConsoleApp_netcore2.0_EF2.0.2_InMemory.csproj @@ -19,7 +19,7 @@ - + diff --git a/src-console/ConsoleAppEF2.0/ConsoleApp_netcore2.0_EF2.0.1.csproj b/src-console/ConsoleAppEF2.0/ConsoleApp_netcore2.0_EF2.0.1.csproj index c019880d..ac0fd8be 100644 --- a/src-console/ConsoleAppEF2.0/ConsoleApp_netcore2.0_EF2.0.1.csproj +++ b/src-console/ConsoleAppEF2.0/ConsoleApp_netcore2.0_EF2.0.1.csproj @@ -12,7 +12,7 @@ - + diff --git a/src-console/ConsoleAppEF2.1.1/ConsoleApp_netcore2.1_EF2.1.1.csproj b/src-console/ConsoleAppEF2.1.1/ConsoleApp_netcore2.1_EF2.1.1.csproj index f31529c9..71257ddb 100644 --- a/src-console/ConsoleAppEF2.1.1/ConsoleApp_netcore2.1_EF2.1.1.csproj +++ b/src-console/ConsoleAppEF2.1.1/ConsoleApp_netcore2.1_EF2.1.1.csproj @@ -18,7 +18,7 @@ - + diff --git a/src-console/ConsoleAppEF2.1/ConsoleApp_netcore2.0_EF2.1.csproj b/src-console/ConsoleAppEF2.1/ConsoleApp_netcore2.0_EF2.1.csproj index 137df235..c30d8bda 100644 --- a/src-console/ConsoleAppEF2.1/ConsoleApp_netcore2.0_EF2.1.csproj +++ b/src-console/ConsoleAppEF2.1/ConsoleApp_netcore2.0_EF2.1.csproj @@ -16,7 +16,7 @@ - + diff --git a/src-console/ConsoleAppEF3.1/ConsoleApp_netcore3.1_EF3.1.csproj b/src-console/ConsoleAppEF3.1/ConsoleApp_netcore3.1_EF3.1.csproj index 0f71601c..1d297cd3 100644 --- a/src-console/ConsoleAppEF3.1/ConsoleApp_netcore3.1_EF3.1.csproj +++ b/src-console/ConsoleAppEF3.1/ConsoleApp_netcore3.1_EF3.1.csproj @@ -20,7 +20,7 @@ - + diff --git a/src-console/ConsoleAppEF5/ConsoleApp_net5.0_EF5.csproj b/src-console/ConsoleAppEF5/ConsoleApp_net5.0_EF5.csproj index 1fa72fbb..bc4122af 100644 --- a/src-console/ConsoleAppEF5/ConsoleApp_net5.0_EF5.csproj +++ b/src-console/ConsoleAppEF5/ConsoleApp_net5.0_EF5.csproj @@ -19,7 +19,7 @@ - + diff --git a/src-console/ConsoleAppEF5_InMemory/ConsoleApp_net5.0_EF5_InMemory.csproj b/src-console/ConsoleAppEF5_InMemory/ConsoleApp_net5.0_EF5_InMemory.csproj index 0d37e480..69742229 100644 --- a/src-console/ConsoleAppEF5_InMemory/ConsoleApp_net5.0_EF5_InMemory.csproj +++ b/src-console/ConsoleAppEF5_InMemory/ConsoleApp_net5.0_EF5_InMemory.csproj @@ -14,7 +14,7 @@ - + diff --git a/src-console/ConsoleAppEF6_InMemory/ConsoleApp_net6.0_EF6_InMemory.csproj b/src-console/ConsoleAppEF6_InMemory/ConsoleApp_net6.0_EF6_InMemory.csproj index e7a8b0ee..5db0eb28 100644 --- a/src-console/ConsoleAppEF6_InMemory/ConsoleApp_net6.0_EF6_InMemory.csproj +++ b/src-console/ConsoleAppEF6_InMemory/ConsoleApp_net6.0_EF6_InMemory.csproj @@ -14,7 +14,7 @@ - + diff --git a/src-console/ConsoleAppEF6_Sqlite/ConsoleApp_net6.0_EF6_Sqlite.csproj b/src-console/ConsoleAppEF6_Sqlite/ConsoleApp_net6.0_EF6_Sqlite.csproj index 9f54be59..a4203e3d 100644 --- a/src-console/ConsoleAppEF6_Sqlite/ConsoleApp_net6.0_EF6_Sqlite.csproj +++ b/src-console/ConsoleAppEF6_Sqlite/ConsoleApp_net6.0_EF6_Sqlite.csproj @@ -14,7 +14,7 @@ - + diff --git a/src-console/ConsoleApp_net10/ConsoleApp_net10.csproj b/src-console/ConsoleApp_net10/ConsoleApp_net10.csproj new file mode 100644 index 00000000..8c348bf9 --- /dev/null +++ b/src-console/ConsoleApp_net10/ConsoleApp_net10.csproj @@ -0,0 +1,22 @@ + + + + Exe + net10.0 + ConsoleApp + enable + latest + + + + + + + + + + + + + \ No newline at end of file diff --git a/src-console/ConsoleApp_net10/DataColumnOrdinalIgnoreCaseComparer.cs b/src-console/ConsoleApp_net10/DataColumnOrdinalIgnoreCaseComparer.cs new file mode 100644 index 00000000..e1774a82 --- /dev/null +++ b/src-console/ConsoleApp_net10/DataColumnOrdinalIgnoreCaseComparer.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections; + +namespace ConsoleApp_net6._0; + +public class DataColumnOrdinalIgnoreCaseComparer : IComparer +{ + public int Compare(object? x, object? y) + { + if (x == null && y == null) + { + return 0; + } + + if (x == null) + { + return -1; + } + + if (y == null) + { + return 1; + } + + if (x is string xAsString && y is string yAsString) + { + return StringComparer.OrdinalIgnoreCase.Compare(xAsString, yAsString); + } + + return Comparer.Default.Compare(x, y); + } +} \ No newline at end of file diff --git a/src-console/ConsoleApp_net10/Program.cs b/src-console/ConsoleApp_net10/Program.cs new file mode 100644 index 00000000..6108ceb0 --- /dev/null +++ b/src-console/ConsoleApp_net10/Program.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Linq.Dynamic.Core.NewtonsoftJson; +using System.Linq.Dynamic.Core.SystemTextJson; +using System.Linq.Expressions; +using System.Text.Json; +using ConsoleApp_net6._0; +using Newtonsoft.Json.Linq; + +namespace ConsoleApp; + +public class X +{ + public string Key { get; set; } = null!; + + public List? Contestants { get; set; } +} + +public class Y +{ +} + +public class SalesData +{ + public string Region { get; set; } + public string Product { get; set; } + public string Sales { get; set; } +} + +public class GroupedSalesData +{ + public string Region { get; set; } + public string? Product { get; set; } + public int TotalSales { get; set; } + public int GroupLevel { get; set; } +} + +class Program +{ + static void Main(string[] args) + { + Issue918(); + return; + + Issue912a(); + Issue912b(); + return; + + Json(); + NewtonsoftJson(); + + return; + + Issue389DoesNotWork(); + return; + Issue389_Works(); + return; + + var q = new[] + { + new X { Key = "x" }, + new X { Key = "a" }, + new X { Key = "a", Contestants = new List { new() } } + }.AsQueryable(); + var groupByKey = q.GroupBy("Key"); + var selectQry = groupByKey.Select("new (Key, Sum(np(Contestants.Count, 0)) As TotalCount)").ToDynamicList(); + + Normal(); + Dynamic(); + } + + private static void Issue918() + { + var persons = new DataTable(); + persons.Columns.Add("FirstName", typeof(string)); + persons.Columns.Add("Nickname", typeof(string)); + persons.Columns.Add("Income", typeof(decimal)).AllowDBNull = true; + + // Adding sample data to the first DataTable + persons.Rows.Add("alex", DBNull.Value, 5000.50m); + persons.Rows.Add("MAGNUS", "Mag", 5000.50m); + persons.Rows.Add("Terry", "Ter", 4000.20m); + persons.Rows.Add("Charlotte", "Charl", DBNull.Value); + + var linqQuery = + from personsRow in persons.AsEnumerable() + select personsRow; + + var queryableRows = linqQuery.AsQueryable(); + + // Sorted at the top of the list + var comparer = new DataColumnOrdinalIgnoreCaseComparer(); + var sortedRows = queryableRows.OrderBy("FirstName", comparer).ToList(); + + int xxx = 0; + } + + private static void Issue912a() + { + var extractedRows = new List + { + new() { Region = "North", Product = "Widget", Sales = "100" }, + new() { Region = "North", Product = "Gadget", Sales = "150" }, + new() { Region = "South", Product = "Widget", Sales = "200" }, + new() { Region = "South", Product = "Gadget", Sales = "100" }, + new() { Region = "North", Product = "Widget", Sales = "50" } + }; + + var rows = extractedRows.AsQueryable(); + + // GROUPING SET 1: (Region, Product) + var detailed = rows + .GroupBy("new (Region, Product)") + .Select("new (Key.Region as Region, Key.Product as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 0 as GroupLevel)"); + + // GROUPING SET 2: (Region) + var regionSubtotal = rows + .GroupBy("Region") + .Select("new (Key as Region, null as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 1 as GroupLevel)"); + + var combined = detailed.Concat(regionSubtotal).AsQueryable(); + var ordered = combined.OrderBy("Product").ToDynamicList(); + + int x = 9; + } + + private static void Issue912b() + { + var eInfoJoinTable = new DataTable(); + eInfoJoinTable.Columns.Add("Region", typeof(string)); + eInfoJoinTable.Columns.Add("Product", typeof(string)); + eInfoJoinTable.Columns.Add("Sales", typeof(int)); + + eInfoJoinTable.Rows.Add("North", "Apples", 100); + eInfoJoinTable.Rows.Add("North", "Oranges", 150); + eInfoJoinTable.Rows.Add("South", "Apples", 200); + eInfoJoinTable.Rows.Add("South", "Oranges", 250); + + var extractedRows = + from row in eInfoJoinTable.AsEnumerable() + select row; + + var rows = extractedRows.AsQueryable(); + + // GROUPING SET 1: (Region, Product) + var detailed = rows + .GroupBy("new (Region, Product)") + .Select("new (Key.Region as Region, Key.Product as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 0 as GroupLevel)"); + + // GROUPING SET 2: (Region) + var regionSubtotal = rows + .GroupBy("Region") + .Select("new (Key as Region, null as Product, Sum(Convert.ToInt32(Sales)) as TotalSales, 1 as GroupLevel)"); + + var combined = detailed.ToDynamicArray().Concat(regionSubtotal.ToDynamicArray()).AsQueryable(); + var ordered = combined.OrderBy("Product").ToDynamicList(); + + int x = 9; + } + + private static void NewtonsoftJson() + { + var array = JArray.Parse(@"[ + { + ""first"": 1, + ""City"": ""Paris"", + ""third"": ""test"" + }, + { + ""first"": 2, + ""City"": ""New York"", + ""third"": ""abc"" + }]"); + + var where = array.Where("City == @0", "Paris"); + foreach (var result in where) + { + Console.WriteLine(result["first"]); + } + + var select = array.Select("City"); + foreach (var result in select) + { + Console.WriteLine(result); + } + + var whereWithSelect = array.Where("City == @0", "Paris").Select("first"); + foreach (var result in whereWithSelect) + { + Console.WriteLine(result); + } + } + + private static void Json() + { + var doc = JsonDocument.Parse(@"[ + { + ""first"": 1, + ""City"": ""Paris"", + ""third"": ""test"" + }, + { + ""first"": 2, + ""City"": ""New York"", + ""third"": ""abc"" + }]"); + + var where = doc.Where("City == @0", "Paris"); + foreach (var result in where.RootElement.EnumerateArray()) + { + Console.WriteLine(result.GetProperty("first")); + } + + var select = doc.Select("City"); + foreach (var result in select.RootElement.EnumerateArray()) + { + Console.WriteLine(result); + } + + var whereWithSelect = doc.Where("City == @0", "Paris").Select("first"); + foreach (var result in whereWithSelect.RootElement.EnumerateArray()) + { + Console.WriteLine(result); + } + } + + private static void Issue389_Works() + { + var strArray = new[] { "1", "2", "3", "4" }; + var x = new List(); + x.Add(Expression.Parameter(strArray.GetType(), "strArray")); + + string query = "string.Join(\",\", strArray)"; + + var e = DynamicExpressionParser.ParseLambda(x.ToArray(), null, query); + Delegate del = e.Compile(); + var result1 = del.DynamicInvoke(new object?[] { strArray }); + Console.WriteLine(result1); + } + + private static void Issue389WorksWithInts() + { + var intArray = new object[] { 1, 2, 3, 4 }; + var x = new List(); + x.Add(Expression.Parameter(intArray.GetType(), "intArray")); + + string query = "string.Join(\",\", intArray)"; + + var e = DynamicExpressionParser.ParseLambda(x.ToArray(), null, query); + Delegate del = e.Compile(); + var result = del.DynamicInvoke(new object?[] { intArray }); + + Console.WriteLine(result); + } + + private static void Issue389DoesNotWork() + { + var intArray = new[] { 1, 2, 3, 4 }; + var x = new List(); + x.Add(Expression.Parameter(intArray.GetType(), "intArray")); + + string query = "string.Join(\",\", intArray)"; + + var e = DynamicExpressionParser.ParseLambda(x.ToArray(), null, query); + Delegate del = e.Compile(); + var result = del.DynamicInvoke(new object?[] { intArray }); + + Console.WriteLine(result); + } + + private static void Normal() + { + var e = new int[0].AsQueryable(); + var q = new[] { 1 }.AsQueryable(); + + var a = q.FirstOrDefault(); + var b = e.FirstOrDefault(44); + + var c = q.FirstOrDefault(i => i == 0); + var d = q.FirstOrDefault(i => i == 0, 42); + + var t = q.Take(1); + } + + private static void Dynamic() + { + var e = new int[0].AsQueryable() as IQueryable; + var q = new[] { 1 }.AsQueryable() as IQueryable; + + var a = q.FirstOrDefault(); + //var b = e.FirstOrDefault(44); + + var c = q.FirstOrDefault("it == 0"); + //var d = q.FirstOrDefault(i => i == 0, 42); + } +} \ No newline at end of file diff --git a/src/EntityFramework.DynamicLinq/EntityFramework.DynamicLinq.csproj b/src/EntityFramework.DynamicLinq/EntityFramework.DynamicLinq.csproj index a0b95e83..518f2653 100644 --- a/src/EntityFramework.DynamicLinq/EntityFramework.DynamicLinq.csproj +++ b/src/EntityFramework.DynamicLinq/EntityFramework.DynamicLinq.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;entityframework;core;async {D3804228-91F4-4502-9595-39584E510000} net45;net452;net46;netstandard2.1 - 1.6.$(PatchVersion) + 1.7.$(PatchVersion) diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10.csproj b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10.csproj new file mode 100644 index 00000000..54817a4c --- /dev/null +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10.csproj @@ -0,0 +1,47 @@ + + + + + Microsoft.EntityFrameworkCore.DynamicLinq + ../Microsoft.EntityFrameworkCore.DynamicLinq.EFCore2/Microsoft.EntityFrameworkCore.DynamicLinq.snk + Microsoft.EntityFrameworkCore.DynamicLinq + $(DefineConstants);EFCORE;EFCORE_3X;EFDYNAMICFUNCTIONS;ASYNCENUMERABLE + Dynamic Linq extensions for Microsoft.EntityFrameworkCore which adds Async support + system;linq;dynamic;entityframework;core;async + {1CD58B7F-CF5D-4F38-A5E0-9FE2D5216520} + net10.0 + 10.7.$(PatchVersion) + + + + full + + + + + portable + true + + + + net10.0 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10/Properties/AssemblyInfo.cs b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..69c2a3cd --- /dev/null +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore10/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.InteropServices; + +[assembly: ComVisible(false)] +[assembly: Guid("b467c675-c014-4b55-85b9-9578941d2ef8")] \ No newline at end of file diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore2/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore2.csproj b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore2/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore2.csproj index 859ac63e..2e9a5892 100644 --- a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore2/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore2.csproj +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore2/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore2.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;entityframework;core;async {D3804228-91F4-4502-9595-39584E510001} netstandard2.0 - 2.6.$(PatchVersion) + 2.7.$(PatchVersion) diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3.csproj b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3.csproj index f5dafeee..0254e9a8 100644 --- a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3.csproj +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore3.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;entityframework;core;async {7994FECC-965C-4A5D-8B0E-1A6EA769D4BE} netstandard2.0 - 3.6.$(PatchVersion) + 3.7.$(PatchVersion) diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore5/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore5.csproj b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore5/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore5.csproj index 46c2f5f9..8393f083 100644 --- a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore5/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore5.csproj +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore5/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore5.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;entityframework;core;async {D3804228-91F4-4502-9595-39584E519901} netstandard2.1;net5.0 - 5.6.$(PatchVersion) + 5.7.$(PatchVersion) diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore6/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore6.csproj b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore6/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore6.csproj index 058a5abb..d6d07c0b 100644 --- a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore6/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore6.csproj +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore6/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore6.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;entityframework;core;async {D28F6393-B56B-40A2-AF67-E8D669F42546} net6.0 - 6.6.$(PatchVersion) + 6.7.$(PatchVersion) diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore7/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore7.csproj b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore7/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore7.csproj index 61eec0d8..0d590e5e 100644 --- a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore7/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore7.csproj +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore7/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore7.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;entityframework;core;async {FB2F4C99-EC34-4D29-87E2-944B25D90ef7} net6.0;net7.0 - 7.6.$(PatchVersion) + 7.7.$(PatchVersion) diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore8/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore8.csproj b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore8/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore8.csproj index 2e95aabf..b95d7eeb 100644 --- a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore8/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore8.csproj +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore8/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore8.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;entityframework;core;async {9000129D-322D-4FE6-9C47-75464577C374} net8.0 - 8.6.$(PatchVersion) + 8.7.$(PatchVersion) diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore9/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore9.csproj b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore9/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore9.csproj index ea16a63e..129a511b 100644 --- a/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore9/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore9.csproj +++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore9/Microsoft.EntityFrameworkCore.DynamicLinq.EFCore9.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;entityframework;core;async {C774DAE7-54A0-4FCD-A3B7-3CB63D7E112D} net9.0 - 9.6.$(PatchVersion) + 9.7.$(PatchVersion) diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NewtonsoftJsonParsingConfig.cs b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NewtonsoftJsonParsingConfig.cs index fad7f9f1..3cb24306 100644 --- a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NewtonsoftJsonParsingConfig.cs +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NewtonsoftJsonParsingConfig.cs @@ -10,10 +10,28 @@ public class NewtonsoftJsonParsingConfig : ParsingConfig /// /// The default ParsingConfig for . /// - public new static NewtonsoftJsonParsingConfig Default { get; } = new(); + public new static NewtonsoftJsonParsingConfig Default { get; } = new NewtonsoftJsonParsingConfig + { + ConvertObjectToSupportComparison = true + }; /// /// The default to use. /// - public DynamicJsonClassOptions? DynamicJsonClassOptions { get; set; } + public DynamicJsonClassOptions? DynamicJsonClassOptions { get; set; } + + /// + /// Gets or sets a value indicating whether the objects in an array should be normalized before processing. + /// + public bool Normalize { get; set; } = true; + + /// + /// Gets or sets the behavior to apply when a property value does not exist during normalization. + /// + /// + /// Use this property to control how the normalization process handles properties that are missing or undefined. + /// The selected behavior may affect the output or error handling of normalization operations. + /// The default value is . + /// + public NormalizationNonExistingPropertyBehavior NormalizationNonExistingPropertyValueBehavior { get; set; } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NormalizationNonExistingPropertyBehavior.cs b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NormalizationNonExistingPropertyBehavior.cs new file mode 100644 index 00000000..44608e83 --- /dev/null +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Config/NormalizationNonExistingPropertyBehavior.cs @@ -0,0 +1,17 @@ +namespace System.Linq.Dynamic.Core.NewtonsoftJson.Config; + +/// +/// Specifies the behavior to use when setting a property value that does not exist or is missing during normalization. +/// +public enum NormalizationNonExistingPropertyBehavior +{ + /// + /// Specifies that a null value should be used. + /// + UseNull = 0, + + /// + /// Specifies that the default value should be used. + /// + UseDefaultValue = 1 +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Extensions/JObjectExtensions.cs b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Extensions/JObjectExtensions.cs index 1a7be2f4..2f79840b 100644 --- a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Extensions/JObjectExtensions.cs +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Extensions/JObjectExtensions.cs @@ -44,10 +44,7 @@ private class JTokenResolvers : Dictionary func) { var array = new JArray(); - foreach (var dynamicElement in func()) + var funcResult = func(); + foreach (var dynamicElement in funcResult) { var element = dynamicElement switch { @@ -869,7 +871,16 @@ private static JArray ToJArray(Func func) private static IQueryable ToQueryable(JArray source, NewtonsoftJsonParsingConfig? config = null) { - return source.ToDynamicJsonClassArray(config?.DynamicJsonClassOptions).AsQueryable(); + config = config ?? NewtonsoftJsonParsingConfig.Default; + config.ConvertObjectToSupportComparison = true; + + var normalized = config.Normalize == true ? + NormalizeUtils.NormalizeArray(source, config.NormalizationNonExistingPropertyValueBehavior) : + source; + + return normalized + .ToDynamicJsonClassArray(config?.DynamicJsonClassOptions) + .AsQueryable(); } #endregion } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/System.Linq.Dynamic.Core.NewtonsoftJson.csproj b/src/System.Linq.Dynamic.Core.NewtonsoftJson/System.Linq.Dynamic.Core.NewtonsoftJson.csproj index 4cbf4851..4d399df7 100644 --- a/src/System.Linq.Dynamic.Core.NewtonsoftJson/System.Linq.Dynamic.Core.NewtonsoftJson.csproj +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/System.Linq.Dynamic.Core.NewtonsoftJson.csproj @@ -8,8 +8,8 @@ Contains some extensions for System.Linq.Dynamic.Core to dynamically query a Newtonsoft.Json.JArray system;linq;dynamic;core;dotnet;json {8C5851B8-5C47-4229-AB55-D4252703598E} - net45;net452;net46;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0 - 1.6.$(PatchVersion) + net45;net452;net46;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0;net10.0 + 1.7.$(PatchVersion) diff --git a/src/System.Linq.Dynamic.Core.NewtonsoftJson/Utils/NormalizeUtils.cs b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Utils/NormalizeUtils.cs new file mode 100644 index 00000000..fe980319 --- /dev/null +++ b/src/System.Linq.Dynamic.Core.NewtonsoftJson/Utils/NormalizeUtils.cs @@ -0,0 +1,152 @@ +using System.Collections.Generic; +using System.Linq.Dynamic.Core.NewtonsoftJson.Config; +using Newtonsoft.Json.Linq; + +namespace System.Linq.Dynamic.Core.NewtonsoftJson.Utils; + +internal static class NormalizeUtils +{ + /// + /// Normalizes an array of JSON objects so that each object contains all properties found in the array, + /// including nested objects. Missing properties will have null values. + /// + internal static JArray NormalizeArray(JArray jsonArray, NormalizationNonExistingPropertyBehavior normalizationBehavior) + { + if (jsonArray.Any(item => item is not JObject)) + { + return jsonArray; + } + + var schema = BuildSchema(jsonArray); + var normalizedArray = new JArray(); + + foreach (var jo in jsonArray.OfType()) + { + var normalizedObj = NormalizeObject(jo, schema, normalizationBehavior); + normalizedArray.Add(normalizedObj); + } + + return normalizedArray; + } + + private static Dictionary BuildSchema(JArray array) + { + var schema = new Dictionary(); + + foreach (var item in array) + { + if (item is JObject obj) + { + MergeSchema(schema, obj); + } + } + + return schema; + } + + private static void MergeSchema(Dictionary schema, JObject obj) + { + foreach (var prop in obj.Properties()) + { + if (prop.Value is JObject nested) + { + if (!schema.TryGetValue(prop.Name, out var jsonValueInfo)) + { + jsonValueInfo = new JsonValueInfo(JTokenType.Object, new Dictionary()); + schema[prop.Name] = jsonValueInfo; + } + + MergeSchema((Dictionary)jsonValueInfo.Value!, nested); + } + else + { + if (!schema.ContainsKey(prop.Name)) + { + schema[prop.Name] = new JsonValueInfo(prop.Value.Type, null); + } + } + } + } + + private static JObject NormalizeObject(JObject source, Dictionary schema, NormalizationNonExistingPropertyBehavior normalizationBehavior) + { + var result = new JObject(); + + foreach (var key in schema.Keys) + { + if (schema[key].Value is Dictionary nestedSchema) + { + result[key] = source.ContainsKey(key) && source[key] is JObject jo ? NormalizeObject(jo, nestedSchema, normalizationBehavior) : CreateEmptyObject(nestedSchema, normalizationBehavior); + } + else + { + if (source.ContainsKey(key)) + { + result[key] = source[key]; + } + else + { + result[key] = GetDefaultOrNullValue(normalizationBehavior, schema[key]); + } + } + } + + return result; + } + + private static JObject CreateEmptyObject(Dictionary schema, NormalizationNonExistingPropertyBehavior normalizationBehavior) + { + var obj = new JObject(); + foreach (var key in schema.Keys) + { + if (schema[key].Value is Dictionary nestedSchema) + { + obj[key] = CreateEmptyObject(nestedSchema, normalizationBehavior); + } + else + { + obj[key] = GetDefaultOrNullValue(normalizationBehavior, schema[key]); + } + } + + return obj; + } + + private static JToken GetDefaultValue(JsonValueInfo jType) + { + return jType.Type switch + { + JTokenType.Array => new JArray(), + JTokenType.Boolean => default(bool), + JTokenType.Bytes => new byte[0], + JTokenType.Date => DateTime.MinValue, + JTokenType.Float => default(float), + JTokenType.Guid => Guid.Empty, + JTokenType.Integer => default(int), + JTokenType.String => string.Empty, + JTokenType.TimeSpan => TimeSpan.MinValue, + _ => GetNullValue(jType), + }; + } + + private static JValue GetNullValue(JsonValueInfo jType) + { + return jType.Type switch + { + JTokenType.Boolean => new JValue((bool?)null), + JTokenType.Bytes => new JValue((byte[]?)null), + JTokenType.Date => new JValue((DateTime?)null), + JTokenType.Float => new JValue((float?)null), + JTokenType.Guid => new JValue((Guid?)null), + JTokenType.Integer => new JValue((int?)null), + JTokenType.String => new JValue((string?)null), + JTokenType.TimeSpan => new JValue((TimeSpan?)null), + _ => JValue.CreateNull(), + }; + } + + private static JToken GetDefaultOrNullValue(NormalizationNonExistingPropertyBehavior behavior, JsonValueInfo jType) + { + return behavior == NormalizationNonExistingPropertyBehavior.UseDefaultValue ? GetDefaultValue(jType) : GetNullValue(jType); + } +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/Config/NormalizationNonExistingPropertyBehavior.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/Config/NormalizationNonExistingPropertyBehavior.cs new file mode 100644 index 00000000..381f0408 --- /dev/null +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/Config/NormalizationNonExistingPropertyBehavior.cs @@ -0,0 +1,17 @@ +namespace System.Linq.Dynamic.Core.SystemTextJson.Config; + +/// +/// Specifies the behavior to use when setting a property value that does not exist or is missing during normalization. +/// +public enum NormalizationNonExistingPropertyBehavior +{ + /// + /// Specifies that a null value should be used. + /// + UseNull = 0, + + /// + /// Specifies that the default value should be used. + /// + UseDefaultValue = 1 +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/Config/SystemTextJsonParsingConfig.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/Config/SystemTextJsonParsingConfig.cs index eb42ff6d..4892bcf2 100644 --- a/src/System.Linq.Dynamic.Core.SystemTextJson/Config/SystemTextJsonParsingConfig.cs +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/Config/SystemTextJsonParsingConfig.cs @@ -8,5 +8,23 @@ public class SystemTextJsonParsingConfig : ParsingConfig /// /// The default ParsingConfig for . /// - public new static SystemTextJsonParsingConfig Default { get; } = new(); + public new static SystemTextJsonParsingConfig Default { get; } = new SystemTextJsonParsingConfig + { + ConvertObjectToSupportComparison = true + }; + + /// + /// Gets or sets a value indicating whether the objects in an array should be normalized before processing. + /// + public bool Normalize { get; set; } = true; + + /// + /// Gets or sets the behavior to apply when a property value does not exist during normalization. + /// + /// + /// Use this property to control how the normalization process handles properties that are missing or undefined. + /// The selected behavior may affect the output or error handling of normalization operations. + /// The default value is . + /// + public NormalizationNonExistingPropertyBehavior NormalizationNonExistingPropertyValueBehavior { get; set; } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/Extensions/JsonDocumentExtensions.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/Extensions/JsonDocumentExtensions.cs index 7daf15a5..3437d3c9 100644 --- a/src/System.Linq.Dynamic.Core.SystemTextJson/Extensions/JsonDocumentExtensions.cs +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/Extensions/JsonDocumentExtensions.cs @@ -34,10 +34,7 @@ private class JTokenResolvers : Dictionary src, Type newType) { var method = ConvertToTypedArrayGenericMethod.MakeGenericMethod(newType); - return (IEnumerable)method.Invoke(null, new object[] { src })!; + return (IEnumerable)method.Invoke(null, [src])!; } private static readonly MethodInfo ConvertToTypedArrayGenericMethod = typeof(JsonDocumentExtensions).GetMethod(nameof(ConvertToTypedArrayGeneric), BindingFlags.NonPublic | BindingFlags.Static)!; diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/Extensions/JsonValueExtensions.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/Extensions/JsonValueExtensions.cs new file mode 100644 index 00000000..907f7bf3 --- /dev/null +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/Extensions/JsonValueExtensions.cs @@ -0,0 +1,21 @@ +#if !NET8_0_OR_GREATER +namespace System.Text.Json.Nodes; + +internal static class JsonValueExtensions +{ + internal static JsonValueKind? GetValueKind(this JsonNode node) + { + if (node is JsonObject) + { + return JsonValueKind.Object; + } + + if (node is JsonArray) + { + return JsonValueKind.Array; + } + + return node.GetValue() is JsonElement je ? je.ValueKind : null; + } +} +#endif \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/JsonValueInfo.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/JsonValueInfo.cs new file mode 100644 index 00000000..da4f2b5d --- /dev/null +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/JsonValueInfo.cs @@ -0,0 +1,10 @@ +using System.Text.Json; + +namespace System.Linq.Dynamic.Core.SystemTextJson; + +internal readonly struct JsonValueInfo(JsonValueKind type, object? value) +{ + public JsonValueKind Type { get; } = type; + + public object? Value { get; } = value; +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/System.Linq.Dynamic.Core.SystemTextJson.csproj b/src/System.Linq.Dynamic.Core.SystemTextJson/System.Linq.Dynamic.Core.SystemTextJson.csproj index f0c4ec8e..41c3691b 100644 --- a/src/System.Linq.Dynamic.Core.SystemTextJson/System.Linq.Dynamic.Core.SystemTextJson.csproj +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/System.Linq.Dynamic.Core.SystemTextJson.csproj @@ -8,8 +8,8 @@ Contains some extensions for System.Linq.Dynamic.Core to dynamically query a System.Text.Json.JsonDocument system;linq;dynamic;core;dotnet;json {FA01CE15-315A-499E-AFC2-955CA7EB45FF} - netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0 - 1.6.$(PatchVersion) + netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0;net10.0 + 1.7.$(PatchVersion) diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs index 8d1671a0..6d8a5aa0 100644 --- a/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/SystemTextJsonExtensions.cs @@ -1093,13 +1093,20 @@ private static JsonDocument ToJsonDocumentArray(Func func) // ReSharper disable once UnusedParameter.Local private static IQueryable ToQueryable(JsonDocument source, SystemTextJsonParsingConfig? config = null) { + config = config ?? SystemTextJsonParsingConfig.Default; + config.ConvertObjectToSupportComparison = true; + var array = source.RootElement; if (array.ValueKind != JsonValueKind.Array) { throw new NotSupportedException("The source is not a JSON array."); } - return JsonDocumentExtensions.ToDynamicJsonClassArray(array).AsQueryable(); + var normalized = config.Normalize ? + NormalizeUtils.NormalizeJsonDocument(source, config.NormalizationNonExistingPropertyValueBehavior) : + source; + + return JsonDocumentExtensions.ToDynamicJsonClassArray(normalized.RootElement).AsQueryable(); } #endregion } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core.SystemTextJson/Utils/NormalizeUtils.cs b/src/System.Linq.Dynamic.Core.SystemTextJson/Utils/NormalizeUtils.cs new file mode 100644 index 00000000..7f6e4abf --- /dev/null +++ b/src/System.Linq.Dynamic.Core.SystemTextJson/Utils/NormalizeUtils.cs @@ -0,0 +1,175 @@ +using System.Collections.Generic; +using System.Linq.Dynamic.Core.SystemTextJson.Config; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace System.Linq.Dynamic.Core.SystemTextJson.Utils; + +internal static class NormalizeUtils +{ + /// + /// Normalizes a document so that each object contains all properties found in the array, including nested objects. + /// + internal static JsonDocument NormalizeJsonDocument(JsonDocument jsonDocument, NormalizationNonExistingPropertyBehavior normalizationBehavior) + { + if (jsonDocument.RootElement.ValueKind != JsonValueKind.Array) + { + throw new NotSupportedException("The source is not a JSON array."); + } + + var jsonArray = JsonNode.Parse(jsonDocument.RootElement.GetRawText())!.AsArray(); + var normalizedArray = NormalizeJsonArray(jsonArray, normalizationBehavior); + + return JsonDocument.Parse(normalizedArray.ToJsonString()); + } + + /// + /// Normalizes an array of JSON objects so that each object contains all properties found in the array, including nested objects. + /// + internal static JsonArray NormalizeJsonArray(JsonArray jsonArray, NormalizationNonExistingPropertyBehavior normalizationBehavior) + { + if (jsonArray.Any(item => item != null && item.GetValueKind() != JsonValueKind.Object)) + { + return jsonArray; + } + + var schema = BuildSchema(jsonArray); + var normalizedArray = new JsonArray(); + + foreach (var item in jsonArray) + { + if (item is JsonObject obj) + { + var normalizedObj = NormalizeObject(obj, schema, normalizationBehavior); + normalizedArray.Add(normalizedObj); + } + } + + return normalizedArray; + } + + private static Dictionary BuildSchema(JsonArray array) + { + var schema = new Dictionary(); + + foreach (var item in array) + { + if (item is JsonObject obj) + { + MergeSchema(schema, obj); + } + } + + return schema; + } + + private static void MergeSchema(Dictionary schema, JsonObject obj) + { + foreach (var prop in obj) + { + if (prop.Value is JsonObject nested) + { + if (!schema.TryGetValue(prop.Key, out var jsonValueInfo)) + { + jsonValueInfo = new JsonValueInfo(JsonValueKind.Object, new Dictionary()); + schema[prop.Key] = jsonValueInfo; + } + + MergeSchema((Dictionary)jsonValueInfo.Value!, nested); + } + else + { + if (!schema.ContainsKey(prop.Key)) + { + schema[prop.Key] = new JsonValueInfo(prop.Value?.GetValueKind() ?? JsonValueKind.Null, null); + } + } + } + } + + private static JsonObject NormalizeObject(JsonObject source, Dictionary schema, NormalizationNonExistingPropertyBehavior normalizationBehavior) + { + var result = new JsonObject(); + + foreach (var kvp in schema) + { + var key = kvp.Key; + var jType = kvp.Value; + + if (jType.Value is Dictionary nestedSchema) + { + result[key] = source.ContainsKey(key) && source[key] is JsonObject jo ? NormalizeObject(jo, nestedSchema, normalizationBehavior) : CreateEmptyObject(nestedSchema, normalizationBehavior); + } + else + { + if (source.ContainsKey(key)) + { + var value = source[key]; +#if NET8_0_OR_GREATER + result[key] = value?.DeepClone(); +#else + result[key] = value != null ? JsonNode.Parse(value.ToJsonString()) : null; +#endif + } + else + { + result[key] = GetDefaultOrNullValue(normalizationBehavior, jType); + } + } + } + + return result; + } + + private static JsonObject CreateEmptyObject(Dictionary schema, NormalizationNonExistingPropertyBehavior normalizationBehavior) + { + var obj = new JsonObject(); + foreach (var kvp in schema) + { + var key = kvp.Key; + var jType = kvp.Value; + + if (jType.Value is Dictionary nestedSchema) + { + obj[key] = CreateEmptyObject(nestedSchema, normalizationBehavior); + } + else + { + obj[key] = GetDefaultOrNullValue(normalizationBehavior, jType); + } + } + + return obj; + } + + private static JsonNode? GetDefaultValue(JsonValueInfo jType) + { + return jType.Type switch + { + JsonValueKind.Array => new JsonArray(), + JsonValueKind.False => false, + JsonValueKind.Number => default(int), + JsonValueKind.String => string.Empty, + JsonValueKind.True => false, + _ => GetNullValue(jType), + }; + } + + private static JsonNode? GetNullValue(JsonValueInfo jType) + { + return jType.Type switch + { + JsonValueKind.Array => null, + JsonValueKind.False => JsonValue.Create(false), + JsonValueKind.Number => JsonValue.Create(null), + JsonValueKind.String => JsonValue.Create(null), + JsonValueKind.True => JsonValue.Create(true), + _ => null, + }; + } + + private static JsonNode? GetDefaultOrNullValue(NormalizationNonExistingPropertyBehavior behavior, JsonValueInfo jType) + { + return behavior == NormalizationNonExistingPropertyBehavior.UseDefaultValue ? GetDefaultValue(jType) : GetNullValue(jType); + } +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/DynamicClass.cs b/src/System.Linq.Dynamic.Core/DynamicClass.cs index 33f06aee..64685f5e 100644 --- a/src/System.Linq.Dynamic.Core/DynamicClass.cs +++ b/src/System.Linq.Dynamic.Core/DynamicClass.cs @@ -123,6 +123,16 @@ public object? this[string name] } } + /// + /// Determines whether a property with the specified name exists in the collection. + /// + /// The name of the property to locate. Cannot be null. + /// true if a property with the specified name exists; otherwise, false. + public bool ContainsProperty(string name) + { + return Properties.ContainsKey(name); + } + /// /// Returns the enumeration of all dynamic member names. /// diff --git a/src/System.Linq.Dynamic.Core/DynamicClass.uap.cs b/src/System.Linq.Dynamic.Core/DynamicClass.uap.cs index 8778de98..cf28afd3 100644 --- a/src/System.Linq.Dynamic.Core/DynamicClass.uap.cs +++ b/src/System.Linq.Dynamic.Core/DynamicClass.uap.cs @@ -12,7 +12,7 @@ public class DynamicClass : DynamicObject { internal const string IndexerName = "System_Linq_Dynamic_Core_DynamicClass_Indexer"; - private readonly Dictionary _properties = new(); + private readonly Dictionary _properties = new(); /// /// Initializes a new instance of the class. @@ -35,7 +35,7 @@ public DynamicClass(params KeyValuePair[] propertylist) /// The name. /// Value from the property. [IndexerName(IndexerName)] - public object this[string name] + public object? this[string name] { get { @@ -59,6 +59,16 @@ public object this[string name] } } + /// + /// Determines whether a property with the specified name exists in the collection. + /// + /// The name of the property to locate. Cannot be null. + /// true if a property with the specified name exists; otherwise, false. + public bool ContainsProperty(string name) + { + return _properties.ContainsKey(name); + } + /// /// Returns the enumeration of all dynamic member names. /// diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs index 7d72ad61..8222d4d7 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs @@ -11,6 +11,7 @@ namespace System.Linq.Dynamic.Core.Parser; internal class ExpressionHelper : IExpressionHelper { + private static readonly Expression _nullExpression = Expression.Constant(null); private readonly IConstantExpressionWrapper _constantExpressionWrapper = new ConstantExpressionWrapper(); private readonly ParsingConfig _parsingConfig; @@ -340,7 +341,7 @@ public bool TryGenerateAndAlsoNotNullExpression(Expression sourceExpression, boo // Convert all expressions into '!= null' expressions (only if the type can be null) var binaryExpressions = expressions .Where(expression => TypeHelper.TypeCanBeNull(expression.Type)) - .Select(expression => Expression.NotEqual(expression, Expression.Constant(null))) + .Select(expression => Expression.NotEqual(expression, _nullExpression)) .ToArray(); // Convert all binary expressions into `AndAlso(...)` @@ -361,7 +362,12 @@ public bool ExpressionQualifiesForNullPropagation(Expression? expression) public Expression GenerateDefaultExpression(Type type) { #if NET35 - return Expression.Constant(Activator.CreateInstance(type)); + if (type.IsValueType) + { + return Expression.Constant(Activator.CreateInstance(type), type); + } + + return Expression.Constant(null, type); #else return Expression.Default(type); #endif @@ -388,11 +394,49 @@ public bool TryConvertTypes(ref Expression left, ref Expression right) if (left.Type == typeof(object)) { - left = Expression.Convert(left, right.Type); + if (TryGetAsIndexerExpression(left, out var ce)) + { + var rightTypeAsNullableType = TypeHelper.GetNullableType(right.Type); + + right = Expression.Convert(right, rightTypeAsNullableType); + + left = Expression.Condition( + ce.Test, + Expression.Convert(ce.IfTrue, rightTypeAsNullableType), + Expression.Convert(_nullExpression, rightTypeAsNullableType) + ); + + return true; + } + + left = Expression.Condition( + Expression.Equal(left, _nullExpression), + GenerateDefaultExpression(right.Type), + Expression.Convert(left, right.Type) + ); } else if (right.Type == typeof(object)) { - right = Expression.Convert(right, left.Type); + if (TryGetAsIndexerExpression(right, out var ce)) + { + var leftTypeAsNullableType = TypeHelper.GetNullableType(left.Type); + + left = Expression.Convert(left, leftTypeAsNullableType); + + right = Expression.Condition( + ce.Test, + Expression.Convert(ce.IfTrue, leftTypeAsNullableType), + Expression.Convert(_nullExpression, leftTypeAsNullableType) + ); + + return true; + } + + right = Expression.Condition( + Expression.Equal(right, _nullExpression), + GenerateDefaultExpression(left.Type), + Expression.Convert(right, left.Type) + ); } return true; @@ -533,4 +577,17 @@ private static object[] ConvertIfIEnumerableHasValues(IEnumerable? input) return []; } + + private static bool TryGetAsIndexerExpression(Expression expression, [NotNullWhen(true)] out ConditionalExpression? indexerExpresion) + { + indexerExpresion = expression as ConditionalExpression; + if (indexerExpresion == null) + { + return false; + } + + return + indexerExpresion.IfTrue.ToString().Contains(DynamicClass.IndexerName) && + indexerExpresion.Test.ToString().Contains("ContainsProperty"); + } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 4cc77a00..b8091f55 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -984,11 +984,12 @@ private Expression ParseRealLiteral() { _textParser.ValidateToken(TokenId.RealLiteral); - string text = _textParser.CurrentToken.Text; + var text = _textParser.CurrentToken.Text; + var textOriginal = text; _textParser.NextToken(); - return _numberParser.ParseRealLiteral(text, text[text.Length - 1], true); + return _numberParser.ParseRealLiteral(text, textOriginal, text[text.Length - 1], true); } private Expression ParseParenExpression() @@ -1920,11 +1921,35 @@ private Expression ParseMemberAccess(Type? type, Expression? expression, string? if (!_parsingConfig.DisableMemberAccessToIndexAccessorFallback && extraCheck) { - var indexerName = TypeHelper.IsDynamicClass(type!) ? DynamicClass.IndexerName : "Item"; + var isDynamicClass = TypeHelper.IsDynamicClass(type!); + var indexerName = isDynamicClass ? DynamicClass.IndexerName : "Item"; + + // Try to get the indexer property "Item" or "DynamicClass_Indexer" which takes a string as parameter var indexerMethod = expression?.Type.GetMethod($"get_{indexerName}", [typeof(string)]); if (indexerMethod != null) { - return Expression.Call(expression, indexerMethod, Expression.Constant(id)); + if (!isDynamicClass) + { + return Expression.Call(expression, indexerMethod, Expression.Constant(id)); + } + + var containsPropertyMethod = typeof(DynamicClass).GetMethod("ContainsProperty"); + if (containsPropertyMethod == null) + { + return Expression.Call(expression, indexerMethod, Expression.Constant(id)); + } + + var callContainsPropertyExpression = Expression.Call( + expression!, + containsPropertyMethod, + Expression.Constant(id) + ); + + return Expression.Condition( + Expression.Equal(callContainsPropertyExpression, Expression.Constant(true)), + Expression.Call(expression, indexerMethod, Expression.Constant(id)), + Expression.Constant(null) + ); } } diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs index 088b755e..49731b24 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs @@ -122,7 +122,7 @@ public ExpressionPromoter(ParsingConfig config) if (TypeHelper.IsCompatibleWith(returnType, type)) { - if (type == typeof(decimal) && TypeHelper.IsEnumType(sourceExpression.Type)) + if (TypeHelper.TypesAreEqual(type, typeof(decimal)) && TypeHelper.IsEnumType(sourceExpression.Type)) { return Expression.Convert(Expression.Convert(sourceExpression, Enum.GetUnderlyingType(sourceExpression.Type)), type); } diff --git a/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs index bbc691cd..4e52949b 100644 --- a/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs @@ -52,5 +52,5 @@ internal interface IExpressionHelper /// /// If the types are different (and not null), try to convert the object type to other type. /// - public bool TryConvertTypes(ref Expression left, ref Expression right); + bool TryConvertTypes(ref Expression left, ref Expression right); } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs b/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs index 7fdfdaad..f4bbc731 100644 --- a/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/NumberParser.cs @@ -4,244 +4,234 @@ using System.Linq.Expressions; using System.Text.RegularExpressions; -namespace System.Linq.Dynamic.Core.Parser +namespace System.Linq.Dynamic.Core.Parser; + +/// +/// NumberParser +/// +public class NumberParser { + private static readonly Regex RegexBinary32 = new("^[01]{1,32}$", RegexOptions.Compiled); + private static readonly Regex RegexBinary64 = new("^[01]{1,64}$", RegexOptions.Compiled); + private static readonly char[] Qualifiers = { 'U', 'u', 'L', 'l', 'F', 'f', 'D', 'd', 'M', 'm' }; + private static readonly char[] QualifiersHex = { 'U', 'u', 'L', 'l' }; + private static readonly string[] QualifiersReal = { "F", "f", "D", "d", "M", "m" }; + private readonly ConstantExpressionHelper _constantExpressionHelper; + + private readonly CultureInfo _culture; + /// - /// NumberParser + /// Initializes a new instance of the class. /// - public class NumberParser + /// The ParsingConfig. + public NumberParser(ParsingConfig? config) { - private static readonly Regex RegexBinary32 = new("^[01]{1,32}$", RegexOptions.Compiled); - private static readonly Regex RegexBinary64 = new("^[01]{1,64}$", RegexOptions.Compiled); - private static readonly char[] Qualifiers = { 'U', 'u', 'L', 'l', 'F', 'f', 'D', 'd', 'M', 'm' }; - private static readonly char[] QualifiersHex = { 'U', 'u', 'L', 'l' }; - private static readonly string[] QualifiersReal = { "F", "f", "D", "d", "M", "m" }; - private readonly ConstantExpressionHelper _constantExpressionHelper; - - private readonly CultureInfo _culture; - - /// - /// Initializes a new instance of the class. - /// - /// The ParsingConfig. - public NumberParser(ParsingConfig? config) - { - _culture = config?.NumberParseCulture ?? CultureInfo.InvariantCulture; - _constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(config ?? ParsingConfig.Default); - } + _culture = config?.NumberParseCulture ?? CultureInfo.InvariantCulture; + _constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(config ?? ParsingConfig.Default); + } - /// - /// Tries to parse the text into a IntegerLiteral ConstantExpression. - /// - /// The current token position (needed for error reporting). - /// The text. - public Expression ParseIntegerLiteral(int tokenPosition, string text) - { - Check.NotEmpty(text, nameof(text)); + /// + /// Tries to parse the text into a IntegerLiteral ConstantExpression. + /// + /// The current token position (needed for error reporting). + /// The text. + public Expression ParseIntegerLiteral(int tokenPosition, string text) + { + Check.NotEmpty(text); - var last = text[text.Length - 1]; - var isNegative = text[0] == '-'; - var isHexadecimal = text.StartsWith(isNegative ? "-0x" : "0x", StringComparison.OrdinalIgnoreCase); - var isBinary = text.StartsWith(isNegative ? "-0b" : "0b", StringComparison.OrdinalIgnoreCase); - var qualifiers = isHexadecimal ? QualifiersHex : Qualifiers; + var textOriginal = text; + var last = text[text.Length - 1]; + var isNegative = text[0] == '-'; + var isHexadecimal = text.StartsWith(isNegative ? "-0x" : "0x", StringComparison.OrdinalIgnoreCase); + var isBinary = text.StartsWith(isNegative ? "-0b" : "0b", StringComparison.OrdinalIgnoreCase); + var qualifiers = isHexadecimal ? QualifiersHex : Qualifiers; - string? qualifier = null; - if (qualifiers.Contains(last)) + string? qualifier = null; + if (qualifiers.Contains(last)) + { + int pos = text.Length - 1, count = 0; + while (qualifiers.Contains(text[pos])) { - int pos = text.Length - 1, count = 0; - while (qualifiers.Contains(text[pos])) - { - ++count; - --pos; - } - qualifier = text.Substring(text.Length - count, count); - text = text.Substring(0, text.Length - count); + ++count; + --pos; } + qualifier = text.Substring(text.Length - count, count); + text = text.Substring(0, text.Length - count); + } - if (!isNegative) + if (!isNegative) + { + if (isHexadecimal || isBinary) { - if (isHexadecimal || isBinary) - { - text = text.Substring(2); - } - - if (isBinary) - { - return ParseAsBinary(tokenPosition, text, isNegative); - } + text = text.Substring(2); + } - if (!ulong.TryParse(text, isHexadecimal ? NumberStyles.HexNumber : NumberStyles.Integer, _culture, out ulong unsignedValue)) - { - throw new ParseException(string.Format(_culture, Res.InvalidIntegerLiteral, text), tokenPosition); - } + if (isBinary) + { + return ParseAsBinary(tokenPosition, text, textOriginal, isNegative); + } - if (!string.IsNullOrEmpty(qualifier) && qualifier!.Length > 0) - { - if (qualifier == "U" || qualifier == "u") - { - return _constantExpressionHelper.CreateLiteral((uint)unsignedValue, text); - } - - if (qualifier == "L" || qualifier == "l") - { - return _constantExpressionHelper.CreateLiteral((long)unsignedValue, text); - } - - if (QualifiersReal.Contains(qualifier)) - { - return ParseRealLiteral(text, qualifier[0], false); - } - - return _constantExpressionHelper.CreateLiteral(unsignedValue, text); - } + if (!ulong.TryParse(text, isHexadecimal ? NumberStyles.HexNumber : NumberStyles.Integer, _culture, out ulong unsignedValue)) + { + throw new ParseException(string.Format(_culture, Res.InvalidIntegerLiteral, text), tokenPosition); + } - if (unsignedValue <= int.MaxValue) + if (!string.IsNullOrEmpty(qualifier) && qualifier!.Length > 0) + { + if (qualifier == "U" || qualifier == "u") { - return _constantExpressionHelper.CreateLiteral((int)unsignedValue, text); + return _constantExpressionHelper.CreateLiteral((uint)unsignedValue, textOriginal); } - if (unsignedValue <= uint.MaxValue) + if (qualifier == "L" || qualifier == "l") { - return _constantExpressionHelper.CreateLiteral((uint)unsignedValue, text); + return _constantExpressionHelper.CreateLiteral((long)unsignedValue, textOriginal); } - if (unsignedValue <= long.MaxValue) + if (QualifiersReal.Contains(qualifier)) { - return _constantExpressionHelper.CreateLiteral((long)unsignedValue, text); + return ParseRealLiteral(text, textOriginal, qualifier[0], false); } - return _constantExpressionHelper.CreateLiteral(unsignedValue, text); + return _constantExpressionHelper.CreateLiteral(unsignedValue, textOriginal); } - if (isHexadecimal || isBinary) + if (unsignedValue <= int.MaxValue) { - text = text.Substring(3); + return _constantExpressionHelper.CreateLiteral((int)unsignedValue, textOriginal); } - if (isBinary) + if (unsignedValue <= uint.MaxValue) { - return ParseAsBinary(tokenPosition, text, isNegative); + return _constantExpressionHelper.CreateLiteral((uint)unsignedValue, textOriginal); } - if (!long.TryParse(text, isHexadecimal ? NumberStyles.HexNumber : NumberStyles.Integer, _culture, out long value)) + if (unsignedValue <= long.MaxValue) { - throw new ParseException(string.Format(_culture, Res.InvalidIntegerLiteral, text), tokenPosition); + return _constantExpressionHelper.CreateLiteral((long)unsignedValue, textOriginal); } - if (isHexadecimal) - { - value = -value; - } + return _constantExpressionHelper.CreateLiteral(unsignedValue, textOriginal); + } - if (!string.IsNullOrEmpty(qualifier) && qualifier!.Length > 0) - { - if (qualifier == "L" || qualifier == "l") - { - return _constantExpressionHelper.CreateLiteral(value, text); - } + if (isHexadecimal || isBinary) + { + text = text.Substring(3); + } - if (QualifiersReal.Contains(qualifier)) - { - return ParseRealLiteral(text, qualifier[0], false); - } + if (isBinary) + { + return ParseAsBinary(tokenPosition, text, textOriginal, isNegative); + } - throw new ParseException(Res.MinusCannotBeAppliedToUnsignedInteger, tokenPosition); + if (!long.TryParse(text, isHexadecimal ? NumberStyles.HexNumber : NumberStyles.Integer, _culture, out long value)) + { + throw new ParseException(string.Format(_culture, Res.InvalidIntegerLiteral, text), tokenPosition); + } + + if (isHexadecimal) + { + value = -value; + } + + if (!string.IsNullOrEmpty(qualifier) && qualifier!.Length > 0) + { + if (qualifier == "L" || qualifier == "l") + { + return _constantExpressionHelper.CreateLiteral(value, textOriginal); } - if (value <= int.MaxValue) + if (QualifiersReal.Contains(qualifier)) { - return _constantExpressionHelper.CreateLiteral((int)value, text); + return ParseRealLiteral(text, textOriginal, qualifier[0], false); } - return _constantExpressionHelper.CreateLiteral(value, text); + throw new ParseException(Res.MinusCannotBeAppliedToUnsignedInteger, tokenPosition); } - /// - /// Parse the text into a Real ConstantExpression. - /// - public Expression ParseRealLiteral(string text, char qualifier, bool stripQualifier) + if (value >= int.MinValue && value <= int.MaxValue) { - if (stripQualifier) - { - var pos = text.Length - 1; - while (pos >= 0 && Qualifiers.Contains(text[pos])) - { - pos--; - } + return _constantExpressionHelper.CreateLiteral((int)value, textOriginal); + } - if (pos < text.Length - 1) - { - qualifier = text[pos + 1]; - text = text.Substring(0, pos + 1); - } - } + return _constantExpressionHelper.CreateLiteral(value, textOriginal); + } - switch (qualifier) + /// + /// Parse the text into a Real ConstantExpression. + /// + public Expression ParseRealLiteral(string text, string textOriginal, char qualifier, bool stripQualifier) + { + if (stripQualifier) + { + var pos = text.Length - 1; + while (pos >= 0 && Qualifiers.Contains(text[pos])) { - case 'f': - case 'F': - return _constantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(float))!, text); - - case 'm': - case 'M': - return _constantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(decimal))!, text); - - case 'd': - case 'D': - return _constantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(double))!, text); + pos--; + } - default: - return _constantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(double))!, text); + if (pos < text.Length - 1) + { + qualifier = text[pos + 1]; + text = text.Substring(0, pos + 1); } } - /// - /// Tries to parse the number (text) into the specified type. - /// - /// The text. - /// The type. - /// The result. - public bool TryParseNumber(string text, Type type, out object? result) + return qualifier switch { - result = ParseNumber(text, type); - return result != null; - } + 'f' or 'F' => _constantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(float))!, textOriginal), + 'm' or 'M' => _constantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(decimal))!, textOriginal), + _ => _constantExpressionHelper.CreateLiteral(ParseNumber(text, typeof(double))!, textOriginal) + }; + } - /// - /// Parses the number (text) into the specified type. - /// - /// The text. - /// The type. - public object? ParseNumber(string text, Type type) + /// + /// Tries to parse the number (text) into the specified type. + /// + /// The text. + /// The type. + /// The result. + public bool TryParseNumber(string text, Type type, out object? result) + { + result = ParseNumber(text, type); + return result != null; + } + + /// + /// Parses the number (text) into the specified type. + /// + /// The text. + /// The type. + public object? ParseNumber(string text, Type type) + { + try { - try - { #if !(UAP10_0 || NETSTANDARD) - switch (Type.GetTypeCode(TypeHelper.GetNonNullableType(type))) - { - case TypeCode.SByte: - return sbyte.Parse(text, _culture); - case TypeCode.Byte: - return byte.Parse(text, _culture); - case TypeCode.Int16: - return short.Parse(text, _culture); - case TypeCode.UInt16: - return ushort.Parse(text, _culture); - case TypeCode.Int32: - return int.Parse(text, _culture); - case TypeCode.UInt32: - return uint.Parse(text, _culture); - case TypeCode.Int64: - return long.Parse(text, _culture); - case TypeCode.UInt64: - return ulong.Parse(text, _culture); - case TypeCode.Single: - return float.Parse(text, _culture); - case TypeCode.Double: - return double.Parse(text, _culture); - case TypeCode.Decimal: - return decimal.Parse(text, _culture); - } + switch (Type.GetTypeCode(TypeHelper.GetNonNullableType(type))) + { + case TypeCode.SByte: + return sbyte.Parse(text, _culture); + case TypeCode.Byte: + return byte.Parse(text, _culture); + case TypeCode.Int16: + return short.Parse(text, _culture); + case TypeCode.UInt16: + return ushort.Parse(text, _culture); + case TypeCode.Int32: + return int.Parse(text, _culture); + case TypeCode.UInt32: + return uint.Parse(text, _culture); + case TypeCode.Int64: + return long.Parse(text, _culture); + case TypeCode.UInt64: + return ulong.Parse(text, _culture); + case TypeCode.Single: + return float.Parse(text, _culture); + case TypeCode.Double: + return double.Parse(text, _culture); + case TypeCode.Decimal: + return decimal.Parse(text, _culture); + } #else var tp = TypeHelper.GetNonNullableType(type); if (tp == typeof(sbyte)) @@ -289,28 +279,27 @@ public bool TryParseNumber(string text, Type type, out object? result) return decimal.Parse(text, _culture); } #endif - } - catch - { - return null; - } - + } + catch + { return null; } - private Expression ParseAsBinary(int tokenPosition, string text, bool isNegative) - { - if (RegexBinary32.IsMatch(text)) - { - return _constantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt32(text, 2), text); - } + return null; + } - if (RegexBinary64.IsMatch(text)) - { - return _constantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt64(text, 2), text); - } + private Expression ParseAsBinary(int tokenPosition, string text, string textOriginal, bool isNegative) + { + if (RegexBinary32.IsMatch(text)) + { + return _constantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt32(text, 2), textOriginal); + } - throw new ParseException(string.Format(_culture, Res.InvalidBinaryIntegerLiteral, text), tokenPosition); + if (RegexBinary64.IsMatch(text)) + { + return _constantExpressionHelper.CreateLiteral((isNegative ? -1 : 1) * Convert.ToInt64(text, 2), textOriginal); } + + throw new ParseException(string.Format(_culture, Res.InvalidBinaryIntegerLiteral, text), tokenPosition); } -} +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs b/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs index 19002c4f..f4401b63 100644 --- a/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs @@ -83,20 +83,20 @@ public static bool IsCompatibleWith(Type source, Type target) return target.IsAssignableFrom(source); } - Type st = GetNonNullableType(source); - Type tt = GetNonNullableType(target); + var sourceType = GetNonNullableType(source); + var targetType = GetNonNullableType(target); - if (st != source && tt == target) + if (sourceType != source && targetType == target) { return false; } - TypeCode sc = st.GetTypeInfo().IsEnum ? TypeCode.Int64 : Type.GetTypeCode(st); - TypeCode tc = tt.GetTypeInfo().IsEnum ? TypeCode.Int64 : Type.GetTypeCode(tt); - switch (sc) + var sourceTypeCode = sourceType.GetTypeInfo().IsEnum ? TypeCode.Int32 : Type.GetTypeCode(sourceType); + var targetTypeCode = targetType.GetTypeInfo().IsEnum ? TypeCode.Int32 : Type.GetTypeCode(targetType); + switch (sourceTypeCode) { case TypeCode.SByte: - switch (tc) + switch (targetTypeCode) { case TypeCode.SByte: case TypeCode.Int16: @@ -110,7 +110,7 @@ public static bool IsCompatibleWith(Type source, Type target) break; case TypeCode.Byte: - switch (tc) + switch (targetTypeCode) { case TypeCode.Byte: case TypeCode.Int16: @@ -127,7 +127,7 @@ public static bool IsCompatibleWith(Type source, Type target) break; case TypeCode.Int16: - switch (tc) + switch (targetTypeCode) { case TypeCode.Int16: case TypeCode.Int32: @@ -140,7 +140,7 @@ public static bool IsCompatibleWith(Type source, Type target) break; case TypeCode.UInt16: - switch (tc) + switch (targetTypeCode) { case TypeCode.UInt16: case TypeCode.Int32: @@ -155,7 +155,7 @@ public static bool IsCompatibleWith(Type source, Type target) break; case TypeCode.Int32: - switch (tc) + switch (targetTypeCode) { case TypeCode.Int32: case TypeCode.Int64: @@ -167,7 +167,7 @@ public static bool IsCompatibleWith(Type source, Type target) break; case TypeCode.UInt32: - switch (tc) + switch (targetTypeCode) { case TypeCode.UInt32: case TypeCode.Int64: @@ -180,7 +180,7 @@ public static bool IsCompatibleWith(Type source, Type target) break; case TypeCode.Int64: - switch (tc) + switch (targetTypeCode) { case TypeCode.Int64: case TypeCode.Single: @@ -191,7 +191,7 @@ public static bool IsCompatibleWith(Type source, Type target) break; case TypeCode.UInt64: - switch (tc) + switch (targetTypeCode) { case TypeCode.UInt64: case TypeCode.Single: @@ -202,7 +202,7 @@ public static bool IsCompatibleWith(Type source, Type target) break; case TypeCode.Single: - switch (tc) + switch (targetTypeCode) { case TypeCode.Single: case TypeCode.Double: @@ -211,7 +211,7 @@ public static bool IsCompatibleWith(Type source, Type target) break; default: - if (st == tt) + if (sourceType == targetType) { return true; } @@ -471,6 +471,11 @@ public static Type GetUnderlyingType(Type type) return type; } + public static bool TypesAreEqual(Type type, Type typeToCheck) + { + return GetNullableType(type) == GetNullableType(typeToCheck); + } + public static IList GetSelfAndBaseTypes(Type type, bool excludeObject = false) { if (type.GetTypeInfo().IsInterface) diff --git a/src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj b/src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj index bcc32ad0..1627393c 100644 --- a/src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj +++ b/src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.csproj @@ -9,8 +9,8 @@ This is a .NETStandard / .NET Core port of the the Microsoft assembly for the .Net 4.0 Dynamic language functionality. system;linq;dynamic;core;dotnet;NETCoreApp;NETStandard {D3804228-91F4-4502-9595-39584E510002} - net35;net40;net45;net452;net46;netstandard1.3;netstandard2.0;netstandard2.1;uap10.0;netcoreapp2.1;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0 - 1.6.$(PatchVersion) + net35;net40;net45;net452;net46;netstandard1.3;netstandard2.0;netstandard2.1;uap10.0;netcoreapp2.1;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0 + 1.7.$(PatchVersion) @@ -34,7 +34,7 @@ $(DefineConstants);NETSTANDARD - + $(DefineConstants);ASYNCENUMERABLE diff --git a/src/Z.EntityFramework.Classic.DynamicLinq/Z.EntityFramework.Classic.DynamicLinq.csproj b/src/Z.EntityFramework.Classic.DynamicLinq/Z.EntityFramework.Classic.DynamicLinq.csproj index 8cf07a98..4edc3a0e 100644 --- a/src/Z.EntityFramework.Classic.DynamicLinq/Z.EntityFramework.Classic.DynamicLinq.csproj +++ b/src/Z.EntityFramework.Classic.DynamicLinq/Z.EntityFramework.Classic.DynamicLinq.csproj @@ -10,7 +10,7 @@ system;linq;dynamic;Z.EntityFramework;core;async;classic {D3804228-91F4-4502-9595-39584Ea20000} net45;netstandard2.0 - 1.6.$(PatchVersion) + 1.7.$(PatchVersion) diff --git a/test/EntityFramework.DynamicLinq.Tests/EntityFramework.DynamicLinq.Tests.csproj b/test/EntityFramework.DynamicLinq.Tests/EntityFramework.DynamicLinq.Tests.csproj index bb2d9ae6..88c88664 100644 --- a/test/EntityFramework.DynamicLinq.Tests/EntityFramework.DynamicLinq.Tests.csproj +++ b/test/EntityFramework.DynamicLinq.Tests/EntityFramework.DynamicLinq.Tests.csproj @@ -2,7 +2,7 @@ Stef Heyenrath - net461;net8.0;net9.0 + net461;net8.0;net9.0;net10.0 full EF;NET461 EntityFramework.DynamicLinq.Tests @@ -40,7 +40,7 @@ - + @@ -58,20 +58,25 @@ - + - + - + - + + + + + + $(DefineConstants);AspNetCoreIdentity diff --git a/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs b/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs index 2e94a212..759f156f 100644 --- a/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs +++ b/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/NewtonsoftJsonTests.cs @@ -1,4 +1,5 @@ -using FluentAssertions; +using System.Linq.Dynamic.Core.NewtonsoftJson.Config; +using FluentAssertions; using Newtonsoft.Json.Linq; using Xunit; @@ -11,11 +12,13 @@ public class NewtonsoftJsonTests [ { "Name": "John", - "Age": 30 + "Age": 30, + "IsNull": null }, { "Name": "Doe", - "Age": 40 + "Age": 40, + "AlsoNull": null } ] """; @@ -506,4 +509,67 @@ public void Where_With_Select() var first = result.First(); first.Value().Should().Be("Doe"); } + + [Theory] + [InlineData("notExisting == true")] + [InlineData("notExisting == \"true\"")] + [InlineData("notExisting == 1")] + [InlineData("notExisting == \"1\"")] + [InlineData("notExisting == \"something\"")] + [InlineData("notExisting > 1")] + [InlineData("notExisting < 1")] + [InlineData("true == notExisting")] + [InlineData("\"true\" == notExisting")] + [InlineData("1 == notExisting")] + [InlineData("\"1\" == notExisting")] + [InlineData("\"something\" == notExisting")] + [InlineData("1 < notExisting")] + [InlineData("1 > notExisting")] + public void Where_NonExistingMember_EmptyResult(string predicate) + { + // Arrange + var config = new NewtonsoftJsonParsingConfig + { + ConvertObjectToSupportComparison = true + }; + + // Act + var result = _source.Where(config, predicate); + + // Assert + result.Should().BeEmpty(); + } + + [Theory] + [InlineData("""[ { "Name": "John", "Age": 30 }, { "Name": "Doe" }, { } ]""")] + [InlineData("""[ { "Name": "Doe" }, { "Name": "John", "Age": 30 }, { } ]""")] + public void NormalizeArray(string array) + { + // Act + var result = JArray.Parse(array) + .Where("Age >= 30") + .Select("Name"); + + // Assert + result.Should().HaveCount(1); + var first = result.First(); + first.Value().Should().Be("John"); + } + + [Fact] + public void NormalizeArray_When_NormalizeIsFalse_ShouldThrow() + { + // Arrange + var config = new NewtonsoftJsonParsingConfig + { + Normalize = false + }; + var array = """[ { "Name": "Doe" }, { "Name": "John", "Age": 30 }, { } ]"""; + + // Act + Action act = () => JArray.Parse(array).Where(config, "Age >= 30"); + + // Assert + act.Should().Throw().WithMessage("Unable to find property 'Age' on type '<>*"); + } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/System.Linq.Dynamic.Core.NewtonsoftJson.Tests.csproj b/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/System.Linq.Dynamic.Core.NewtonsoftJson.Tests.csproj index 4882ff18..57de2628 100644 --- a/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/System.Linq.Dynamic.Core.NewtonsoftJson.Tests.csproj +++ b/test/System.Linq.Dynamic.Core.NewtonsoftJson.Tests/System.Linq.Dynamic.Core.NewtonsoftJson.Tests.csproj @@ -1,7 +1,7 @@  Stef Heyenrath - net452;netcoreapp3.1;net8.0 + net452;netcoreapp3.1;net8.0;net10.0 full True latest diff --git a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/DynamicExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/DynamicExpressionParserTests.cs index 6e174735..ed3d4f95 100644 --- a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/DynamicExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/DynamicExpressionParserTests.cs @@ -2,7 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Text.Json; -using FluentAssertions; +using AwesomeAssertions; namespace System.Linq.Dynamic.Core.SystemTextJson.Tests; diff --git a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/Extensions/JsonDocumentExtensionsTests.cs b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/Extensions/JsonDocumentExtensionsTests.cs index 048f7f3a..5c3698dc 100644 --- a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/Extensions/JsonDocumentExtensionsTests.cs +++ b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/Extensions/JsonDocumentExtensionsTests.cs @@ -1,7 +1,7 @@ using System.Linq.Dynamic.Core.SystemTextJson.Extensions; using System.Text; using System.Text.Json; -using FluentAssertions; +using AwesomeAssertions; using Xunit; namespace System.Linq.Dynamic.Core.SystemTextJson.Tests.Extensions; diff --git a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/System.Linq.Dynamic.Core.SystemTextJson.Tests.csproj b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/System.Linq.Dynamic.Core.SystemTextJson.Tests.csproj index dd619154..bfb5547e 100644 --- a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/System.Linq.Dynamic.Core.SystemTextJson.Tests.csproj +++ b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/System.Linq.Dynamic.Core.SystemTextJson.Tests.csproj @@ -1,7 +1,7 @@  Stef Heyenrath - net6.0;net8.0 + net6.0;net8.0;net10.0 full True latest @@ -11,7 +11,7 @@ - + all diff --git a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs index f5dee221..9a81f76a 100644 --- a/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs +++ b/test/System.Linq.Dynamic.Core.SystemTextJson.Tests/SystemTextJsonTests.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using FluentAssertions; +using System.Linq.Dynamic.Core.SystemTextJson.Config; +using System.Text.Json; +using AwesomeAssertions; using Xunit; namespace System.Linq.Dynamic.Core.SystemTextJson.Tests; @@ -16,11 +17,13 @@ public class SystemTextJsonTests [ { "Name": "John", - "Age": 30 + "Age": 30, + "IsNull": null }, { "Name": "Doe", - "Age": 40 + "Age": 40, + "AlsoNull": null } ] """; @@ -160,20 +163,20 @@ public void Distinct() public void First() { // Act + Assert 1 - _source.First().GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""John"",""Age"":30}").RootElement.GetRawText()); + _source.First().GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""John"",""Age"":30,""IsNull"":null,""AlsoNull"":null}").RootElement.GetRawText()); // Act + Assert 2 - _source.First("Age > 30").GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40}").RootElement.GetRawText()); + _source.First("Age > 30").GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40,""IsNull"":null,""AlsoNull"":null}").RootElement.GetRawText()); } [Fact] public void FirstOrDefault() { // Act + Assert 1 - _source.FirstOrDefault()!.Value.GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""John"",""Age"":30}").RootElement.GetRawText()); + _source.FirstOrDefault()!.Value.GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""John"",""Age"":30,""IsNull"":null,""AlsoNull"":null}").RootElement.GetRawText()); // Act + Assert 2 - _source.FirstOrDefault("Age > 30")!.Value.GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40}").RootElement.GetRawText()); + _source.FirstOrDefault("Age > 30")!.Value.GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40,""IsNull"":null,""AlsoNull"":null}").RootElement.GetRawText()); // Act + Assert 3 _source.FirstOrDefault("Age > 999").Should().BeNull(); @@ -266,20 +269,20 @@ public void GroupBySimpleKeySelector() public void Last() { // Act + Assert 1 - _source.Last().GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40}").RootElement.GetRawText()); + _source.Last().GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40,""IsNull"":null,""AlsoNull"":null}").RootElement.GetRawText()); // Act + Assert 2 - _source.Last("Age > 0").GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40}").RootElement.GetRawText()); + _source.Last("Age > 0").GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40,""IsNull"":null,""AlsoNull"":null}").RootElement.GetRawText()); } [Fact] public void LastOrDefault() { // Act + Assert 1 - _source.LastOrDefault()!.Value.GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40}").RootElement.GetRawText()); + _source.LastOrDefault()!.Value.GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40,""IsNull"":null,""AlsoNull"":null}").RootElement.GetRawText()); // Act + Assert 2 - _source.LastOrDefault("Age > 0")!.Value.GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40}").RootElement.GetRawText()); + _source.LastOrDefault("Age > 0")!.Value.GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40,""IsNull"":null,""AlsoNull"":null}").RootElement.GetRawText()); // Act + Assert 3 _source.LastOrDefault("Age > 999").Should().BeNull(); @@ -443,7 +446,7 @@ public void SelectMany() public void Single() { // Act + Assert - _source.Single("Age > 30").GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40}").RootElement.GetRawText()); + _source.Single("Age > 30").GetRawText().Should().BeEquivalentTo(JsonDocument.Parse(@"{""Name"":""Doe"",""Age"":40,""IsNull"":null,""AlsoNull"":null}").RootElement.GetRawText()); } [Fact] @@ -535,4 +538,65 @@ public void Where_With_Select() array.Should().HaveCount(1); array.First().GetString().Should().Be("Doe"); } + + [Theory] + [InlineData("notExisting == true")] + [InlineData("notExisting == \"true\"")] + [InlineData("notExisting == 1")] + [InlineData("notExisting == \"1\"")] + [InlineData("notExisting == \"something\"")] + [InlineData("notExisting > 1")] + [InlineData("notExisting < 1")] + [InlineData("true == notExisting")] + [InlineData("\"true\" == notExisting")] + [InlineData("1 == notExisting")] + [InlineData("\"1\" == notExisting")] + [InlineData("\"something\" == notExisting")] + [InlineData("1 < notExisting")] + [InlineData("1 > notExisting")] + public void Where_NonExistingMember_EmptyResult(string predicate) + { + // Act + var result = _source.Where(predicate).RootElement.EnumerateArray(); + + // Assert + result.Should().BeEmpty(); + } + + [Theory] + [InlineData("""[ { "Name": "John", "Age": 30 }, { "Name": "Doe" }, { } ]""")] + [InlineData("""[ { "Name": "Doe" }, { "Name": "John", "Age": 30 }, { } ]""")] + public void NormalizeArray(string data) + { + // Arrange + var source = JsonDocument.Parse(data); + + // Act + var result = source + .Where("Age >= 30") + .Select("Name"); + + // Assert + var array = result.RootElement.EnumerateArray(); + array.Should().HaveCount(1); + var first = result.First(); + array.First().GetString().Should().Be("John"); + } + + [Fact] + public void NormalizeArray_When_NormalizeIsFalse_ShouldThrow() + { + // Arrange + var config = new SystemTextJsonParsingConfig + { + Normalize = false + }; + var data = """[ { "Name": "Doe" }, { "Name": "John", "Age": 30 }, { } ]"""; + + // Act + Action act = () => JsonDocument.Parse(data).Where(config, "Age >= 30"); + + // Assert + act.Should().Throw().WithMessage("Unable to find property 'Age' on type '<>f__AnonymousType*"); + } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests.Net5/System.Linq.Dynamic.Core.Tests.Net5.csproj b/test/System.Linq.Dynamic.Core.Tests.Net5/System.Linq.Dynamic.Core.Tests.Net5.csproj index 7e8ccc72..47b4bb3d 100644 --- a/test/System.Linq.Dynamic.Core.Tests.Net5/System.Linq.Dynamic.Core.Tests.Net5.csproj +++ b/test/System.Linq.Dynamic.Core.Tests.Net5/System.Linq.Dynamic.Core.Tests.Net5.csproj @@ -17,7 +17,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/test/System.Linq.Dynamic.Core.Tests.Net6/System.Linq.Dynamic.Core.Tests.Net6.csproj b/test/System.Linq.Dynamic.Core.Tests.Net6/System.Linq.Dynamic.Core.Tests.Net6.csproj index c20f741c..0550f818 100644 --- a/test/System.Linq.Dynamic.Core.Tests.Net6/System.Linq.Dynamic.Core.Tests.Net6.csproj +++ b/test/System.Linq.Dynamic.Core.Tests.Net6/System.Linq.Dynamic.Core.Tests.Net6.csproj @@ -16,7 +16,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/test/System.Linq.Dynamic.Core.Tests.Net7/System.Linq.Dynamic.Core.Tests.Net7.csproj b/test/System.Linq.Dynamic.Core.Tests.Net7/System.Linq.Dynamic.Core.Tests.Net7.csproj index 6d415bb0..90fd9b01 100644 --- a/test/System.Linq.Dynamic.Core.Tests.Net7/System.Linq.Dynamic.Core.Tests.Net7.csproj +++ b/test/System.Linq.Dynamic.Core.Tests.Net7/System.Linq.Dynamic.Core.Tests.Net7.csproj @@ -16,7 +16,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/test/System.Linq.Dynamic.Core.Tests.Net8/System.Linq.Dynamic.Core.Tests.Net8.csproj b/test/System.Linq.Dynamic.Core.Tests.Net8/System.Linq.Dynamic.Core.Tests.Net8.csproj index 978abb1f..a6dda0af 100644 --- a/test/System.Linq.Dynamic.Core.Tests.Net8/System.Linq.Dynamic.Core.Tests.Net8.csproj +++ b/test/System.Linq.Dynamic.Core.Tests.Net8/System.Linq.Dynamic.Core.Tests.Net8.csproj @@ -16,7 +16,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + @@ -31,7 +31,7 @@ - + diff --git a/test/System.Linq.Dynamic.Core.Tests.Net9/System.Linq.Dynamic.Core.Tests.Net9.csproj b/test/System.Linq.Dynamic.Core.Tests.Net9/System.Linq.Dynamic.Core.Tests.Net9.csproj new file mode 100644 index 00000000..f7cb2fa7 --- /dev/null +++ b/test/System.Linq.Dynamic.Core.Tests.Net9/System.Linq.Dynamic.Core.Tests.Net9.csproj @@ -0,0 +1,48 @@ + + + + net9.0 + System.Linq.Dynamic.Core.Tests + full + True + ../../src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.snk + false + $(DefineConstants);NETCOREAPP;EFCORE;EFCORE_3X;NETCOREAPP3_1;AspNetCoreIdentity + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/System.Linq.Dynamic.Core.Tests.NetCoreApp31/System.Linq.Dynamic.Core.Tests.NetCoreApp31.csproj b/test/System.Linq.Dynamic.Core.Tests.NetCoreApp31/System.Linq.Dynamic.Core.Tests.NetCoreApp31.csproj index 5b908293..76de3f36 100644 --- a/test/System.Linq.Dynamic.Core.Tests.NetCoreApp31/System.Linq.Dynamic.Core.Tests.NetCoreApp31.csproj +++ b/test/System.Linq.Dynamic.Core.Tests.NetCoreApp31/System.Linq.Dynamic.Core.Tests.NetCoreApp31.csproj @@ -22,7 +22,7 @@ all runtime; build; native; contentfiles; analyzers - + diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs index 32dd3002..066d5ce4 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs @@ -109,7 +109,11 @@ public void DynamicClass_GetPropertyValue_Should_Work() value.Should().Be(test); } +#if NET461 + [Fact(Skip = "Does not work for .NET 4.6.1")] +#else [Fact] +#endif public void DynamicClass_GettingValue_ByIndex_Should_Work() { // Arrange @@ -127,27 +131,39 @@ public void DynamicClass_GettingValue_ByIndex_Should_Work() value.Should().Be(test); } +#if NET461 + [Fact(Skip = "Does not work for .NET 4.6.1")] +#else [Fact] +#endif public void DynamicClass_SettingExistingPropertyValue_ByIndex_Should_Work() { // Arrange - var test = "Test"; - var newTest = "abc"; - var range = new List + var originalValue = "Test"; + var newValue = "abc"; + var array = new object[] { - new { FieldName = test, Value = 3.14159 } + new + { + FieldName = originalValue, + Value = 3.14159 + } }; // Act - var rangeResult = range.AsQueryable().Select("new(FieldName as FieldName)").ToDynamicList(); + var rangeResult = array.AsQueryable().Select("new(FieldName as FieldName)").ToDynamicList(); var item = rangeResult.First(); - item["FieldName"] = newTest; + item["FieldName"] = newValue; var value = item["FieldName"] as string; - value.Should().Be(newTest); + value.Should().Be(newValue); } +#if NET461 + [Fact(Skip = "Does not work for .NET 4.6.1")] +#else [Fact] +#endif public void DynamicClass_SettingNewProperty_ByIndex_Should_Work() { // Arrange diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs index 4d5d19b5..e301394c 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs @@ -924,6 +924,7 @@ public void DynamicExpressionParser_ParseLambda_Float(string? culture, string ex [null, "1.2345E4", 12345d] ]; } + [Theory] [MemberData(nameof(Doubles))] public void DynamicExpressionParser_ParseLambda_Double(string? culture, string expression, double expected) @@ -945,6 +946,42 @@ public void DynamicExpressionParser_ParseLambda_Double(string? culture, string e result.Should().Be(expected); } + [Theory] + [InlineData("0x0", 0)] + [InlineData("0xa", 10)] + [InlineData("0xA", 10)] + [InlineData("0x10", 16)] + public void DynamicExpressionParser_ParseLambda_HexToLong(string expression, long expected) + { + // Arrange + var parameters = Array.Empty(); + + // Act + var lambda = DynamicExpressionParser.ParseLambda( parameters, typeof(long), expression); + var result = lambda.Compile().DynamicInvoke(); + + // Assert + result.Should().Be(expected); + } + + [Theory] + [InlineData("0b0", 0)] + [InlineData("0B0", 0)] + [InlineData("0b1000", 8)] + [InlineData("0b1001", 9)] + public void DynamicExpressionParser_ParseLambda_BinaryToLong(string expression, long expected) + { + // Arrange + var parameters = Array.Empty(); + + // Act + var lambda = DynamicExpressionParser.ParseLambda(parameters, typeof(long), expression); + var result = lambda.Compile().DynamicInvoke(); + + // Assert + result.Should().Be(expected); + } + public class EntityDbo { public string Name { get; set; } = string.Empty; @@ -1711,9 +1748,9 @@ public void DynamicExpressionParser_ParseLambda_CustomType_Method_With_ComplexEx } [Theory] - [InlineData(true, "c => c.Age == 8", "c => (c.Age == 8)")] + [InlineData(true, "c => c.Age == 8", "c => (c.Age == ")] [InlineData(true, "c => c.Name == \"test\"", "c => (c.Name == \"test\")")] - [InlineData(false, "c => c.Age == 8", "Param_0 => (Param_0.Age == 8)")] + [InlineData(false, "c => c.Age == 8", "Param_0 => (Param_0.Age == ")] [InlineData(false, "c => c.Name == \"test\"", "Param_0 => (Param_0.Name == \"test\")")] public void DynamicExpressionParser_ParseLambda_RenameParameterExpression(bool renameParameterExpression, string expressionAsString, string expected) { @@ -1728,11 +1765,11 @@ public void DynamicExpressionParser_ParseLambda_RenameParameterExpression(bool r var result = expression.ToString(); // Assert - Check.That(result).IsEqualTo(expected); + Check.That(result).Contains(expected); } [Theory] - [InlineData("c => c.Age == 8", "([a-z]{16}) =\\> \\(\\1\\.Age == 8\\)")] + [InlineData("c => c.Age == 8", "([a-z]{16}) =\\> \\(\\1\\.Age == .+")] [InlineData("c => c.Name == \"test\"", "([a-z]{16}) =\\> \\(\\1\\.Name == \"test\"\\)")] public void DynamicExpressionParser_ParseLambda_RenameEmptyParameterExpressionNames(string expressionAsString, string expected) { diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/NumberParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/NumberParserTests.cs index e291c34e..2f8a8ed8 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/NumberParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/NumberParserTests.cs @@ -1,8 +1,8 @@ -using FluentAssertions; using System.Collections.Generic; using System.Globalization; using System.Linq.Dynamic.Core.Parser; using System.Linq.Expressions; +using FluentAssertions; using Xunit; namespace System.Linq.Dynamic.Core.Tests.Parser; @@ -129,6 +129,8 @@ public void NumberParser_ParseNumber_Double(string? culture, string text, double [Theory] [InlineData("42", 42)] [InlineData("-42", -42)] + [InlineData("3000000000", 3000000000)] + [InlineData("-3000000000", -3000000000)] [InlineData("77u", 77)] [InlineData("77l", 77)] [InlineData("77ul", 77)] @@ -159,7 +161,7 @@ public void NumberParser_ParseIntegerLiteral(string text, double expected) public void NumberParser_ParseDecimalLiteral(string text, char qualifier, decimal expected) { // Act - var result = new NumberParser(_parsingConfig).ParseRealLiteral(text, qualifier, true) as ConstantExpression; + var result = new NumberParser(_parsingConfig).ParseRealLiteral(text, text, qualifier, true) as ConstantExpression; // Assert result?.Value.Should().Be(expected); @@ -175,7 +177,7 @@ public void NumberParser_ParseDoubleLiteral(string text, char qualifier, double // Arrange // Act - var result = new NumberParser(_parsingConfig).ParseRealLiteral(text, qualifier, true) as ConstantExpression; + var result = new NumberParser(_parsingConfig).ParseRealLiteral(text, text, qualifier, true) as ConstantExpression; // Assert result?.Value.Should().Be(expected); @@ -191,7 +193,7 @@ public void NumberParser_ParseFloatLiteral(string text, char qualifier, float ex // Arrange // Act - var result = new NumberParser(_parsingConfig).ParseRealLiteral(text, qualifier, true) as ConstantExpression; + var result = new NumberParser(_parsingConfig).ParseRealLiteral(text, text, qualifier, true) as ConstantExpression; // Assert result?.Value.Should().Be(expected); diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/TypeHelperTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/TypeHelperTests.cs index c7a534ba..e6286834 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/TypeHelperTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/TypeHelperTests.cs @@ -45,7 +45,7 @@ public void TypeHelper_IsCompatibleWith_SameTypes_True() } [Fact] - public void TypeHelper_IsCompatibleWith_True() + public void TypeHelper_IsCompatibleWith_Int_And_Long_Returns_True() { // Assign + Act var result = TypeHelper.IsCompatibleWith(typeof(int), typeof(long)); @@ -54,8 +54,52 @@ public void TypeHelper_IsCompatibleWith_True() Check.That(result).IsTrue(); } + [Theory] + + // True (enum underlying Int32 compatible targets) + [InlineData(typeof(DayOfWeek), true)] + [InlineData(typeof(DayOfWeek?), true)] + [InlineData(typeof(int), true)] + [InlineData(typeof(int?), true)] + [InlineData(typeof(long), true)] + [InlineData(typeof(long?), true)] + [InlineData(typeof(float), true)] + [InlineData(typeof(float?), true)] + [InlineData(typeof(double), true)] + [InlineData(typeof(double?), true)] + [InlineData(typeof(decimal), true)] + [InlineData(typeof(decimal?), true)] + [InlineData(typeof(object), true)] + + // False (not compatible with enum's Int32 widening rules or reference types) + [InlineData(typeof(char), false)] + [InlineData(typeof(char?), false)] + [InlineData(typeof(short), false)] + [InlineData(typeof(short?), false)] + [InlineData(typeof(byte), false)] + [InlineData(typeof(byte?), false)] + [InlineData(typeof(sbyte), false)] + [InlineData(typeof(sbyte?), false)] + [InlineData(typeof(ushort), false)] + [InlineData(typeof(ushort?), false)] + [InlineData(typeof(uint), false)] + [InlineData(typeof(uint?), false)] + [InlineData(typeof(ulong), false)] + [InlineData(typeof(ulong?), false)] + [InlineData(typeof(bool), false)] + [InlineData(typeof(bool?), false)] + [InlineData(typeof(string), false)] + public void TypeHelper_IsCompatibleWith_Enum(Type targetType, bool expected) + { + // Assign + Act + var result = TypeHelper.IsCompatibleWith(typeof(DayOfWeek), targetType); + + // Assert + result.Should().Be(expected); + } + [Fact] - public void TypeHelper_IsCompatibleWith_False() + public void TypeHelper_IsCompatibleWith_Long_And_Int_Returns_False() { // Assign + Act var result = TypeHelper.IsCompatibleWith(typeof(long), typeof(int)); diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs index 364419a3..ddd32a83 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Select.cs @@ -202,6 +202,32 @@ public void Select_Dynamic_Add_Strings() Assert.Equal(range.Select(x => x + "c").ToArray(), rangeResult.Cast().ToArray()); } + [Fact] + public void Select_Dynamic_Add_DayOfWeekEnum_And_Integer() + { + // Arrange + var range = new DayOfWeek[] { DayOfWeek.Monday }; + + // Act + var rangeResult = range.AsQueryable().Select("it + 1"); + + // Assert + Assert.Equal(range.Select(x => x + 1).ToArray(), rangeResult.Cast().ToArray()); + } + + [Fact] + public void Select_Dynamic_Add_Integer_And_DayOfWeekEnum() + { + // Arrange + var range = new int[] { 1 }; + + // Act + var rangeResult = range.AsQueryable().Select("it + DayOfWeek.Monday"); + + // Assert + Assert.Equal(range.Select(x => x + DayOfWeek.Monday).Cast().ToArray(), rangeResult.Cast().ToArray()); + } + [Fact] public void Select_Dynamic_WithIncludes() { @@ -445,7 +471,7 @@ public void Select_Dynamic_RenameParameterExpression_Is_True() Check.That(result).Equals("System.Int32[].Select(it => (it * it))"); } -#if NET461 || NET5_0 || NET6_0 || NET7_0 || NET8_0 || NET9_0 +#if NET461 || NET5_0 || NET6_0 || NET7_0 || NET8_0 || NET9_0 || NET10_0 [Fact(Skip = "Fails sometimes in GitHub CI build")] #else [Fact] diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs index 23f6a963..f5300898 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs @@ -157,8 +157,9 @@ public void Where_Dynamic_Exceptions() Assert.Throws(() => qry.Where((string?)null)); Assert.Throws(() => qry.Where("")); Assert.Throws(() => qry.Where(" ")); - var parsingConfigException = Assert.Throws(() => qry.Where("UserName == \"x\"", ParsingConfig.Default)); - Assert.Equal("The ParsingConfig should be provided as first argument to this method. (Parameter 'args')", parsingConfigException.Message); + + Action act = () => qry.Where("UserName == \"x\"", ParsingConfig.Default); + act.Should().Throw().WithMessage("The ParsingConfig should be provided as first argument to this method.*"); } [Fact] diff --git a/test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj b/test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj index 749ea06c..b76c8eb6 100644 --- a/test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj +++ b/test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj @@ -1,6 +1,6 @@  - net9.0 + net10.0 System.Linq.Dynamic.Core.Tests full True @@ -15,7 +15,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/version.xml b/version.xml index 61507fe2..d0d14392 100644 --- a/version.xml +++ b/version.xml @@ -1,5 +1,5 @@ - 9 + 1 \ No newline at end of file