diff --git a/BlazarTech.QueryableValues.sln b/BlazarTech.QueryableValues.sln index 72466ac..eb19b89 100644 --- a/BlazarTech.QueryableValues.sln +++ b/BlazarTech.QueryableValues.sln @@ -30,6 +30,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueryableValues.SqlServer.E EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueryableValues.SqlServer.Tests.EFCore7", "tests\QueryableValues.SqlServer.Tests.EFCore7\QueryableValues.SqlServer.Tests.EFCore7.csproj", "{D5293D2F-4649-4A2B-A50D-E7DBD4AA4A5C}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueryableValues.SqlServer.Benchmarks", "benchmarks\QueryableValues.SqlServer.Benchmarks\QueryableValues.SqlServer.Benchmarks.csproj", "{99FE31A0-BC7E-412C-82E2-DA19E8B68613}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -110,6 +112,12 @@ Global {D5293D2F-4649-4A2B-A50D-E7DBD4AA4A5C}.Test_All|Any CPU.Build.0 = Test_All|Any CPU {D5293D2F-4649-4A2B-A50D-E7DBD4AA4A5C}.Test|Any CPU.ActiveCfg = Test|Any CPU {D5293D2F-4649-4A2B-A50D-E7DBD4AA4A5C}.Test|Any CPU.Build.0 = Test|Any CPU + {99FE31A0-BC7E-412C-82E2-DA19E8B68613}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99FE31A0-BC7E-412C-82E2-DA19E8B68613}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99FE31A0-BC7E-412C-82E2-DA19E8B68613}.Release|Any CPU.Build.0 = Release|Any CPU + {99FE31A0-BC7E-412C-82E2-DA19E8B68613}.Test_All|Any CPU.ActiveCfg = Release|Any CPU + {99FE31A0-BC7E-412C-82E2-DA19E8B68613}.Test_All|Any CPU.Build.0 = Release|Any CPU + {99FE31A0-BC7E-412C-82E2-DA19E8B68613}.Test|Any CPU.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index c1ed033..4e752d9 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ [![GitHub Stars](https://badgen.net/github/stars/yv989c/BlazarTech.QueryableValues?icon=github)][Repository] [![Nuget Downloads](https://badgen.net/nuget/dt/BlazarTech.QueryableValues.SqlServer?icon=nuget)][NuGet Package] +> 🤔💭 TLDR; By using QueryableValues, you can incorporate in-memory collections into your EF queries with outstanding performance and flexibility. + This library allows you to efficiently compose an [IEnumerable<T>] in your [Entity Framework Core] queries when using the [SQL Server Database Provider]. This is accomplished by using the `AsQueryableValues` extension method available on the [DbContext] class. Everything is evaluated on the server with a single round trip, in a way that preserves the query's [execution plan], even when the values behind the [IEnumerable<T>] are changed on subsequent executions. The supported types for `T` are: @@ -19,6 +21,8 @@ The supported types for `T` are: For a detailed explanation of the problem solved by QueryableValues, please continue reading [here][readme-background]. +> ✅ QueryableValues boasts over 120 integration tests that are executed on every supported version of EF. These tests ensure reliability and compatibility, giving you added confidence. + > 💡 Still on Entity Framework 6 (non-core)? Then [QueryableValues `EF6 Edition`](https://github.com/yv989c/BlazarTech.QueryableValues.EF6) is what you need. ## When Should You Use It? @@ -79,6 +83,7 @@ public class Startup } } ``` +> 💡 Pro-tip: `UseQueryableValues` offers an optional `options` delegate for additional configurations. ## How Do You Use It? The `AsQueryableValues` extension method is provided by the `BlazarTech.QueryableValues` namespace; therefore, you must add the following `using` directive to your source code file for it to appear as a method of your [DbContext] instance: @@ -171,103 +176,120 @@ var myQuery = > :warning: There is a limit of up to 10 properties for any given simple type (e.g. cannot have more than 10 [Int32] properties). Exceeding that limit will cause an exception and may also suggest that you should rethink your strategy. # Benchmarks -The following [benchmarks] consist of simple EF Core queries that have a dependency on a random sequence of [Int32] and [Guid] values via the `Contains` LINQ method. It shows the performance differences between not using and using QueryableValues. In practice, the benefits of using QueryableValues will be more dramatic on complex EF Core queries and busy environments. +The following [benchmarks] consist of simple EF Core queries that have a dependency on a random sequence of [Int32], [Guid], and [String] values via the `Contains` LINQ method. It shows the performance differences between not using and using QueryableValues. In practice, the benefits of using QueryableValues are more dramatic on complex EF Core queries and busy environments. ### Benchmarked Libraries | Package | Version | | ------- |:-------:| -| Microsoft.EntityFrameworkCore.SqlServer | 6.0.1 | -| BlazarTech.QueryableValues.SqlServer | 6.3.0 | +| Microsoft.EntityFrameworkCore.SqlServer | 7.0.4 | +| BlazarTech.QueryableValues.SqlServer | 7.2.0 | -### BenchmarkDotNet Configuration and System Specs +### BenchmarkDotNet System Specs and Configuration ``` -BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19042.1466 (20H2/October2020Update) -Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores -.NET SDK=6.0.101 - [Host] : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT - Job-GMTUEM : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT +BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1413/22H2/2022Update/SunValley2) +AMD Ryzen 9 6900HS Creator Edition, 1 CPU, 16 logical and 8 physical cores +.NET SDK=7.0.202 + [Host] : .NET 6.0.15 (6.0.1523.11507), X64 RyuJIT AVX2 + Job-OFVMJD : .NET 6.0.15 (6.0.1523.11507), X64 RyuJIT AVX2 Server=True InvocationCount=200 IterationCount=25 RunStrategy=Monitoring UnrollFactor=1 WarmupCount=1 ``` ### SQL Server Instance Specs ``` -Microsoft SQL Server 2017 (RTM-GDR) (KB4583456) - 14.0.2037.2 (X64) -Nov 2 2020 19:19:59 -Copyright (C) 2017 Microsoft Corporation -Express Edition (64-bit) on Windows 10 Pro 10.0 (Build 19042: ) (Hypervisor) +Microsoft SQL Server 2022 (RTM) - 16.0.1000.6 (X64) +Oct 8 2022 05:58:25 +Copyright (C) 2022 Microsoft Corporation +Express Edition (64-bit) on Windows 10 Pro 10.0 (Build 22621: ) (Hypervisor) ``` -- The SQL Server instance was running in the same system where the benchmark was executed. +- The SQL Server instance was running in the same system where the benchmarks were executed. - Shared Memory is the only network protocol that's enabled on this instance. +### Query Duration - Without vs. With (XML) vs. With (JSON) -### Results for Int32 - -![Benchmarks Int32 Values][BenchmarksInt32] - -
- -| Method | Values | Mean (us) | Error (us) | Std Dev (us) | Median (us) | Ratio | RatioSD | Allocated | -|---------|--------|--------------|--------------|----------------|---------------|-------|---------|-----------| -| Without | 2 | 921.20 | 31.30 | 41.78 | 903.80 | 1.00 | 0.00 | 20 KB | -| With | 2 | 734.30 | 45.28 | 60.44 | 696.10 | 0.80 | 0.04 | 51 KB | -| Without | 4 | 997.80 | 31.79 | 42.44 | 981.10 | 1.00 | 0.00 | 21 KB | -| With | 4 | 779.40 | 47.22 | 63.04 | 738.70 | 0.78 | 0.05 | 51 KB | -| Without | 8 | 1,081.00 | 31.26 | 41.74 | 1,061.30 | 1.00 | 0.00 | 21 KB | -| With | 8 | 814.20 | 47.34 | 63.20 | 775.70 | 0.75 | 0.04 | 51 KB | -| Without | 16 | 1,331.70 | 88.81 | 118.56 | 1,283.40 | 1.00 | 0.00 | 23 KB | -| With | 16 | 872.70 | 42.46 | 56.68 | 840.30 | 0.66 | 0.06 | 52 KB | -| Without | 32 | 1,731.40 | 40.59 | 54.18 | 1,732.60 | 1.00 | 0.00 | 26 KB | -| With | 32 | 1,006.00 | 47.61 | 63.56 | 973.60 | 0.58 | 0.03 | 53 KB | -| Without | 64 | 2,615.40 | 103.77 | 138.53 | 2,540.20 | 1.00 | 0.00 | 31 KB | -| With | 64 | 1,264.20 | 36.95 | 49.33 | 1,239.90 | 0.48 | 0.03 | 55 KB | -| Without | 128 | 5,687.30 | 200.05 | 267.06 | 5,588.20 | 1.00 | 0.00 | 41 KB | -| With | 128 | 1,917.00 | 34.06 | 45.47 | 1,897.90 | 0.34 | 0.02 | 60 KB | -| Without | 256 | 10,565.00 | 186.05 | 248.37 | 10,473.00 | 1.00 | 0.00 | 63 KB | -| With | 256 | 2,977.00 | 29.38 | 39.23 | 2,964.50 | 0.28 | 0.01 | 69 KB | -| Without | 512 | 20,110.50 | 452.28 | 603.79 | 20,108.30 | 1.00 | 0.00 | 106 KB | -| With | 512 | 5,313.10 | 47.66 | 63.62 | 5,340.80 | 0.26 | 0.01 | 88 KB | -| Without | 1024 | 46,599.30 | 4,286.13 | 5,721.87 | 48,194.20 | 1.00 | 0.00 | 192 KB | -| With | 1024 | 11,614.40 | 85.81 | 114.55 | 11,619.80 | 0.25 | 0.03 | 128 KB | -| Without | 2048 | 105,096.90 | 5,359.60 | 7,154.92 | 106,405.10 | 1.00 | 0.00 | 363 KB | -| With | 2048 | 19,481.40 | 66.66 | 88.99 | 19,474.80 | 0.19 | 0.01 | 213 KB | -| Without | 4096 | 177,245.80 | 1,812.40 | 2,419.51 | 176,767.90 | 1.00 | 0.00 | 706 KB | -| With | 4096 | 38,743.00 | 2,422.07 | 3,233.40 | 37,414.70 | 0.22 | 0.02 | 368 KB | - -
+**Legend:** -### Results for Guid +- **Without:** Plain EF. +- **With (XML):** EF with QueryableValues using the XML serializer. +- **With (JSON):** EF with QueryableValues using the JSON serializer. -![Benchmarks Guid Values][BenchmarksGuid] +[![Benchmarks Chart][BenchmarksChart]][BenchmarksChartInteractive]
-| Method | Values | Mean (us) | Error (us) | Std Dev (us) | Median (us) | Ratio | RatioSD | Allocated | -|---------|--------|--------------|--------------|----------------|---------------|-------|---------|-----------| -| Without | 2 | 895.60 | 30.64 | 40.91 | 877.90 | 1.00 | 0.00 | 21 KB | -| With | 2 | 741.80 | 46.44 | 62.00 | 704.40 | 0.83 | 0.04 | 51 KB | -| Without | 4 | 968.90 | 33.69 | 44.97 | 950.40 | 1.00 | 0.00 | 22 KB | -| With | 4 | 727.00 | 43.20 | 57.68 | 689.80 | 0.75 | 0.04 | 52 KB | -| Without | 8 | 1,075.50 | 34.88 | 46.57 | 1,054.90 | 1.00 | 0.00 | 23 KB | -| With | 8 | 773.10 | 42.45 | 56.67 | 737.10 | 0.72 | 0.04 | 53 KB | -| Without | 16 | 1,372.60 | 66.21 | 88.39 | 1,383.80 | 1.00 | 0.00 | 26 KB | -| With | 16 | 808.90 | 40.12 | 53.55 | 777.80 | 0.59 | 0.06 | 55 KB | -| Without | 32 | 1,710.70 | 26.25 | 35.04 | 1,699.90 | 1.00 | 0.00 | 33 KB | -| With | 32 | 869.80 | 49.27 | 65.78 | 830.40 | 0.51 | 0.03 | 59 KB | -| Without | 64 | 2,656.60 | 30.28 | 40.43 | 2,652.30 | 1.00 | 0.00 | 47 KB | -| With | 64 | 1,038.70 | 58.99 | 78.75 | 994.40 | 0.39 | 0.03 | 67 KB | -| Without | 128 | 5,415.90 | 45.76 | 61.09 | 5,417.00 | 1.00 | 0.00 | 74 KB | -| With | 128 | 1,456.30 | 53.76 | 71.77 | 1,424.10 | 0.27 | 0.02 | 84 KB | -| Without | 256 | 9,461.50 | 45.09 | 60.20 | 9,469.10 | 1.00 | 0.00 | 128 KB | -| With | 256 | 2,156.00 | 36.01 | 48.07 | 2,139.30 | 0.23 | 0.00 | 120 KB | -| Without | 512 | 18,015.10 | 117.47 | 156.82 | 17,946.50 | 1.00 | 0.00 | 219 KB | -| With | 512 | 3,511.30 | 62.41 | 83.32 | 3,460.80 | 0.19 | 0.00 | 197 KB | -| Without | 1024 | 44,525.60 | 754.94 | 1,007.82 | 44,601.80 | 1.00 | 0.00 | 419 KB | -| With | 1024 | 7,825.80 | 72.45 | 96.72 | 7,808.20 | 0.18 | 0.00 | 319 KB | -| Without | 2048 | 83,843.30 | 778.80 | 1,039.68 | 83,954.70 | 1.00 | 0.00 | 801 KB | -| With | 2048 | 12,372.40 | 207.91 | 277.55 | 12,232.20 | 0.15 | 0.00 | 596 KB | -| Without | 4096 | 217,255.80 | 3,458.95 | 4,617.60 | 216,353.20 | 1.00 | 0.00 | 1,566 KB | -| With | 4096 | 24,981.10 | 274.10 | 365.92 | 25,116.70 | 0.12 | 0.00 | 1,132 KB | +| Method | Type | NumberOfValues | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio | +|--------- |------- |--------------- |-------------:|----------:|----------:|-------------:|------:|--------:|-------:|-------:|-------:|-----------:|------------:| +| Without | Int32 | 2 | 824.3 us | 26.03 us | 34.75 us | 808.9 us | 1.00 | 0.00 | - | - | - | 20.26 KB | 1.00 | +| WithXml | Int32 | 2 | 508.7 us | 32.46 us | 43.34 us | 504.3 us | 0.62 | 0.04 | - | - | - | 41.37 KB | 2.04 | +| WithJson | Int32 | 2 | 431.7 us | 35.52 us | 47.41 us | 446.8 us | 0.52 | 0.05 | - | - | - | 41.5 KB | 2.05 | +| | | | | | | | | | | | | | | +| Without | Int32 | 8 | 964.8 us | 25.05 us | 33.44 us | 954.6 us | 1.00 | 0.00 | - | - | - | 21.17 KB | 1.00 | +| WithXml | Int32 | 8 | 548.2 us | 34.29 us | 45.78 us | 537.0 us | 0.57 | 0.04 | - | - | - | 41.33 KB | 1.95 | +| WithJson | Int32 | 8 | 445.1 us | 34.28 us | 45.76 us | 453.6 us | 0.46 | 0.04 | - | - | - | 41.56 KB | 1.96 | +| | | | | | | | | | | | | | | +| Without | Int32 | 32 | 1,519.3 us | 34.23 us | 45.69 us | 1,494.4 us | 1.00 | 0.00 | - | - | - | 25.45 KB | 1.00 | +| WithXml | Int32 | 32 | 687.5 us | 32.29 us | 43.10 us | 664.9 us | 0.45 | 0.03 | - | - | - | 41.52 KB | 1.63 | +| WithJson | Int32 | 32 | 448.1 us | 38.22 us | 51.03 us | 425.9 us | 0.30 | 0.04 | - | - | - | 41.61 KB | 1.63 | +| | | | | | | | | | | | | | | +| Without | Int32 | 128 | 5,470.2 us | 25.34 us | 33.83 us | 5,473.2 us | 1.00 | 0.00 | - | - | - | 41.18 KB | 1.00 | +| WithXml | Int32 | 128 | 1,334.4 us | 37.80 us | 50.47 us | 1,316.5 us | 0.24 | 0.01 | - | - | - | 44.02 KB | 1.07 | +| WithJson | Int32 | 128 | 498.9 us | 33.69 us | 44.97 us | 498.1 us | 0.09 | 0.01 | - | - | - | 42.53 KB | 1.03 | +| | | | | | | | | | | | | | | +| Without | Int32 | 512 | 17,572.2 us | 68.50 us | 91.45 us | 17,566.4 us | 1.00 | 0.00 | - | - | - | 105.67 KB | 1.00 | +| WithXml | Int32 | 512 | 4,016.2 us | 30.74 us | 41.04 us | 4,014.4 us | 0.23 | 0.00 | - | - | - | 52.18 KB | 0.49 | +| WithJson | Int32 | 512 | 685.0 us | 30.40 us | 40.59 us | 661.9 us | 0.04 | 0.00 | - | - | - | 46.37 KB | 0.44 | +| | | | | | | | | | | | | | | +| Without | Int32 | 2048 | 71,616.8 us | 677.00 us | 903.77 us | 71,227.6 us | 1.00 | 0.00 | - | - | - | 363.17 KB | 1.00 | +| WithXml | Int32 | 2048 | 14,045.8 us | 50.55 us | 67.48 us | 14,029.9 us | 0.20 | 0.00 | - | - | - | 84.85 KB | 0.23 | +| WithJson | Int32 | 2048 | 1,577.1 us | 32.17 us | 42.95 us | 1,564.8 us | 0.02 | 0.00 | - | - | - | 61.07 KB | 0.17 | +| | | | | | | | | | | | | | | +| Without | Guid | 2 | 788.9 us | 20.31 us | 27.11 us | 778.1 us | 1.00 | 0.00 | - | - | - | 20.74 KB | 1.00 | +| WithXml | Guid | 2 | 487.6 us | 30.51 us | 40.74 us | 487.7 us | 0.62 | 0.04 | - | - | - | 41.23 KB | 1.99 | +| WithJson | Guid | 2 | 434.7 us | 33.42 us | 44.61 us | 443.3 us | 0.55 | 0.04 | - | - | - | 41.19 KB | 1.99 | +| | | | | | | | | | | | | | | +| Without | Guid | 8 | 939.1 us | 29.24 us | 39.04 us | 921.1 us | 1.00 | 0.00 | - | - | - | 23.49 KB | 1.00 | +| WithXml | Guid | 8 | 515.1 us | 32.95 us | 43.99 us | 509.2 us | 0.55 | 0.04 | - | - | - | 42.23 KB | 1.80 | +| WithJson | Guid | 8 | 450.0 us | 33.55 us | 44.79 us | 461.4 us | 0.48 | 0.04 | - | - | - | 41.98 KB | 1.79 | +| | | | | | | | | | | | | | | +| Without | Guid | 32 | 1,566.2 us | 43.12 us | 57.56 us | 1,551.3 us | 1.00 | 0.00 | - | - | - | 33.24 KB | 1.00 | +| WithXml | Guid | 32 | 607.3 us | 33.01 us | 44.07 us | 587.0 us | 0.39 | 0.03 | - | - | - | 43.58 KB | 1.31 | +| WithJson | Guid | 32 | 488.4 us | 32.86 us | 43.87 us | 487.3 us | 0.31 | 0.03 | - | - | - | 43.48 KB | 1.31 | +| | | | | | | | | | | | | | | +| Without | Guid | 128 | 5,140.0 us | 52.22 us | 69.71 us | 5,138.2 us | 1.00 | 0.00 | - | - | - | 74.11 KB | 1.00 | +| WithXml | Guid | 128 | 987.8 us | 37.30 us | 49.79 us | 965.0 us | 0.19 | 0.01 | - | - | - | 51.97 KB | 0.70 | +| WithJson | Guid | 128 | 665.9 us | 38.37 us | 51.23 us | 636.8 us | 0.13 | 0.01 | - | - | - | 51.12 KB | 0.69 | +| | | | | | | | | | | | | | | +| Without | Guid | 512 | 16,031.0 us | 74.08 us | 98.89 us | 16,023.7 us | 1.00 | 0.00 | - | - | - | 219.5 KB | 1.00 | +| WithXml | Guid | 512 | 2,528.8 us | 38.80 us | 51.79 us | 2,517.7 us | 0.16 | 0.00 | - | - | - | 84.36 KB | 0.38 | +| WithJson | Guid | 512 | 1,368.8 us | 22.42 us | 29.93 us | 1,355.1 us | 0.09 | 0.00 | - | - | - | 80.08 KB | 0.36 | +| | | | | | | | | | | | | | | +| Without | Guid | 2048 | 71,956.6 us | 688.35 us | 918.93 us | 72,148.6 us | 1.00 | 0.00 | - | - | - | 801.13 KB | 1.00 | +| WithXml | Guid | 2048 | 9,399.9 us | 76.33 us | 101.90 us | 9,359.8 us | 0.13 | 0.00 | 5.0000 | 5.0000 | 5.0000 | 213.42 KB | 0.27 | +| WithJson | Guid | 2048 | 4,463.6 us | 36.90 us | 49.26 us | 4,442.6 us | 0.06 | 0.00 | - | - | - | 197.4 KB | 0.25 | +| | | | | | | | | | | | | | | +| Without | String | 2 | 858.7 us | 23.34 us | 31.16 us | 846.2 us | 1.00 | 0.00 | - | - | - | 21.44 KB | 1.00 | +| WithXml | String | 2 | 637.4 us | 35.57 us | 47.48 us | 626.0 us | 0.74 | 0.04 | - | - | - | 55.52 KB | 2.59 | +| WithJson | String | 2 | 534.5 us | 30.81 us | 41.13 us | 528.7 us | 0.62 | 0.03 | - | - | - | 42.83 KB | 2.00 | +| | | | | | | | | | | | | | | +| Without | String | 8 | 1,028.9 us | 24.07 us | 32.13 us | 1,015.2 us | 1.00 | 0.00 | - | - | - | 25.55 KB | 1.00 | +| WithXml | String | 8 | 737.8 us | 44.23 us | 59.05 us | 727.5 us | 0.72 | 0.04 | - | - | - | 56.98 KB | 2.23 | +| WithJson | String | 8 | 641.8 us | 34.63 us | 46.23 us | 640.1 us | 0.62 | 0.04 | - | - | - | 43.64 KB | 1.71 | +| | | | | | | | | | | | | | | +| Without | String | 32 | 1,692.5 us | 23.43 us | 31.27 us | 1,684.7 us | 1.00 | 0.00 | - | - | - | 41.84 KB | 1.00 | +| WithXml | String | 32 | 1,016.7 us | 56.75 us | 75.76 us | 976.6 us | 0.60 | 0.04 | - | - | - | 60.35 KB | 1.44 | +| WithJson | String | 32 | 871.5 us | 39.02 us | 52.10 us | 843.8 us | 0.51 | 0.03 | - | - | - | 47.29 KB | 1.13 | +| | | | | | | | | | | | | | | +| Without | String | 128 | 7,665.5 us | 28.53 us | 38.09 us | 7,662.0 us | 1.00 | 0.00 | - | - | - | 103.65 KB | 1.00 | +| WithXml | String | 128 | 2,392.2 us | 35.64 us | 47.57 us | 2,379.7 us | 0.31 | 0.01 | - | - | - | 74.85 KB | 0.72 | +| WithJson | String | 128 | 2,063.6 us | 26.61 us | 35.53 us | 2,063.5 us | 0.27 | 0.01 | - | - | - | 61.2 KB | 0.59 | +| | | | | | | | | | | | | | | +| Without | String | 512 | 26,444.7 us | 102.44 us | 136.75 us | 26,421.0 us | 1.00 | 0.00 | - | - | - | 343.51 KB | 1.00 | +| WithXml | String | 512 | 8,134.2 us | 32.51 us | 43.41 us | 8,125.8 us | 0.31 | 0.00 | - | - | - | 132.34 KB | 0.39 | +| WithJson | String | 512 | 7,210.9 us | 33.10 us | 44.18 us | 7,199.6 us | 0.27 | 0.00 | - | - | - | 116.42 KB | 0.34 | +| | | | | | | | | | | | | | | +| Without | String | 2048 | 112,512.8 us | 443.78 us | 592.43 us | 112,461.1 us | 1.00 | 0.00 | 5.0000 | - | - | 1310.32 KB | 1.00 | +| WithXml | String | 2048 | 32,080.3 us | 138.18 us | 184.47 us | 32,075.1 us | 0.29 | 0.00 | - | - | - | 361.05 KB | 0.28 | +| WithJson | String | 2048 | 28,929.1 us | 84.67 us | 113.03 us | 28,917.8 us | 0.26 | 0.00 | - | - | - | 336.47 KB | 0.26 |
@@ -381,14 +403,17 @@ As expected, none of the queries in the orange section hit the cache. On the oth Now, focus your attention to the first query of the green section. Here you can observe that there's a cost associated with this technique, but this cost can be offset in the long run, especially when your queries are not trivial like the ones in these examples. ## What Makes This Work? 🤓 + +> 🎉 QueryableValues now supports JSON serialization, which improves its performance compared to using XML. By default, QueryableValues will attempt to use JSON if it is supported. + QueryableValues makes use of the XML parsing capabilities in SQL Server, which are available in all the supported versions of SQL Server to date. The provided sequence of values are serialized as XML and embedded in the underlying SQL query using a native XML parameter, then it uses SQL Server's XML type methods to project the query in a way that can be mapped by [Entity Framework Core]. This is a technique that I have not seen being used by other popular libraries that aim to solve this problem. It is superior from a latency standpoint because it resolves the query with a single round trip to the database and most importantly, it preserves the query's [execution plan] even when the content of the XML is changed. ## One More Thing 👀 -The `AsQueryableValues` extension method allows you to treat a sequence of values as you normally would if these were another entity in your [DbContext]. The type returned by the extension is an [IQueryable<T>] that can be composed with other entities in your query. +The `AsQueryableValues` extension method allows you to treat a sequence of values as you normally would if they were another entity in your [DbContext]. The type returned by the extension is an [IQueryable<T>] that can be composed with other entities in your query. -For example, you can do one or more joins like this and it is totally fine: +For example, you can perform one or more joins like this and it is completely fine: ```c# var myQuery = from i in dbContext.MyEntities @@ -400,6 +425,9 @@ var myQuery = i.PropA }; ``` + +Isn't that great? 🥰 + ## Did You Find a 🐛 or Have an 💡? PRs are welcome! 🙂 @@ -435,5 +463,5 @@ PRs are welcome! 🙂 [Repository]: https://github.com/yv989c/BlazarTech.QueryableValues [benchmarks]: /benchmarks/QueryableValues.SqlServer.Benchmarks -[BenchmarksInt32]: /docs/images/benchmarks/int32-v6.3.0.png -[BenchmarksGuid]: /docs/images/benchmarks/guid-v6.3.0.png +[BenchmarksChart]: /docs/images/benchmarks/v7.2.0.png +[BenchmarksChartInteractive]: https://chartbenchmark.net/?src=repo#shared=%7B%22results%22%3A%22BenchmarkDotNet%3Dv0.13.5%2C%20OS%3DWindows%2011%20(10.0.22621.1413%2F22H2%2F2022Update%2FSunValley2)%5CnAMD%20Ryzen%209%206900HS%20Creator%20Edition%2C%201%20CPU%2C%2016%20logical%20and%208%20physical%20cores%5Cn.NET%20SDK%3D7.0.202%5Cn%20%20%5BHost%5D%20%20%20%20%20%3A%20.NET%206.0.15%20(6.0.1523.11507)%2C%20X64%20RyuJIT%20AVX2%5Cn%20%20Job-OFVMJD%20%3A%20.NET%206.0.15%20(6.0.1523.11507)%2C%20X64%20RyuJIT%20AVX2%5Cn%5CnServer%3DTrue%20%20InvocationCount%3D200%20%20IterationCount%3D25%5CnRunStrategy%3DMonitoring%20%20UnrollFactor%3D1%20%20WarmupCount%3D1%5Cn%5Cn%7C%20%20%20Method%20%7C%20%20%20Type%20%7C%20NumberOfValues%20%7C%20%20%20%20%20%20%20%20%20Mean%20%7C%20%20%20%20%20Error%20%7C%20%20%20%20StdDev%20%7C%20%20%20%20%20%20%20Median%20%7C%20Ratio%20%7C%20RatioSD%20%7C%20%20%20Gen0%20%7C%20%20%20Gen1%20%7C%20%20%20Gen2%20%7C%20%20Allocated%20%7C%20Alloc%20Ratio%20%7C%5Cn%7C---------%20%7C-------%20%7C---------------%20%7C-------------%3A%7C----------%3A%7C----------%3A%7C-------------%3A%7C------%3A%7C--------%3A%7C-------%3A%7C-------%3A%7C-------%3A%7C-----------%3A%7C------------%3A%7C%5Cn%7C%20%20Without%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%202%20%7C%20%20%20%20%20824.3%20us%20%7C%20%2026.03%20us%20%7C%20%2034.75%20us%20%7C%20%20%20%20%20808.9%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2020.26%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%202%20%7C%20%20%20%20%20508.7%20us%20%7C%20%2032.46%20us%20%7C%20%2043.34%20us%20%7C%20%20%20%20%20504.3%20us%20%7C%20%200.62%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.37%20KB%20%7C%20%20%20%20%20%20%20%202.04%20%7C%5Cn%7C%20WithJson%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%202%20%7C%20%20%20%20%20431.7%20us%20%7C%20%2035.52%20us%20%7C%20%2047.41%20us%20%7C%20%20%20%20%20446.8%20us%20%7C%20%200.52%20%7C%20%20%20%200.05%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%2041.5%20KB%20%7C%20%20%20%20%20%20%20%202.05%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%208%20%7C%20%20%20%20%20964.8%20us%20%7C%20%2025.05%20us%20%7C%20%2033.44%20us%20%7C%20%20%20%20%20954.6%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2021.17%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%208%20%7C%20%20%20%20%20548.2%20us%20%7C%20%2034.29%20us%20%7C%20%2045.78%20us%20%7C%20%20%20%20%20537.0%20us%20%7C%20%200.57%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.33%20KB%20%7C%20%20%20%20%20%20%20%201.95%20%7C%5Cn%7C%20WithJson%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%208%20%7C%20%20%20%20%20445.1%20us%20%7C%20%2034.28%20us%20%7C%20%2045.76%20us%20%7C%20%20%20%20%20453.6%20us%20%7C%20%200.46%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.56%20KB%20%7C%20%20%20%20%20%20%20%201.96%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%2032%20%7C%20%20%201%2C519.3%20us%20%7C%20%2034.23%20us%20%7C%20%2045.69%20us%20%7C%20%20%201%2C494.4%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2025.45%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%2032%20%7C%20%20%20%20%20687.5%20us%20%7C%20%2032.29%20us%20%7C%20%2043.10%20us%20%7C%20%20%20%20%20664.9%20us%20%7C%20%200.45%20%7C%20%20%20%200.03%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.52%20KB%20%7C%20%20%20%20%20%20%20%201.63%20%7C%5Cn%7C%20WithJson%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%2032%20%7C%20%20%20%20%20448.1%20us%20%7C%20%2038.22%20us%20%7C%20%2051.03%20us%20%7C%20%20%20%20%20425.9%20us%20%7C%20%200.30%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.61%20KB%20%7C%20%20%20%20%20%20%20%201.63%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20128%20%7C%20%20%205%2C470.2%20us%20%7C%20%2025.34%20us%20%7C%20%2033.83%20us%20%7C%20%20%205%2C473.2%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.18%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20128%20%7C%20%20%201%2C334.4%20us%20%7C%20%2037.80%20us%20%7C%20%2050.47%20us%20%7C%20%20%201%2C316.5%20us%20%7C%20%200.24%20%7C%20%20%20%200.01%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2044.02%20KB%20%7C%20%20%20%20%20%20%20%201.07%20%7C%5Cn%7C%20WithJson%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20128%20%7C%20%20%20%20%20498.9%20us%20%7C%20%2033.69%20us%20%7C%20%2044.97%20us%20%7C%20%20%20%20%20498.1%20us%20%7C%20%200.09%20%7C%20%20%20%200.01%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2042.53%20KB%20%7C%20%20%20%20%20%20%20%201.03%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20512%20%7C%20%2017%2C572.2%20us%20%7C%20%2068.50%20us%20%7C%20%2091.45%20us%20%7C%20%2017%2C566.4%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20105.67%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20512%20%7C%20%20%204%2C016.2%20us%20%7C%20%2030.74%20us%20%7C%20%2041.04%20us%20%7C%20%20%204%2C014.4%20us%20%7C%20%200.23%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2052.18%20KB%20%7C%20%20%20%20%20%20%20%200.49%20%7C%5Cn%7C%20WithJson%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20512%20%7C%20%20%20%20%20685.0%20us%20%7C%20%2030.40%20us%20%7C%20%2040.59%20us%20%7C%20%20%20%20%20661.9%20us%20%7C%20%200.04%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2046.37%20KB%20%7C%20%20%20%20%20%20%20%200.44%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%202048%20%7C%20%2071%2C616.8%20us%20%7C%20677.00%20us%20%7C%20903.77%20us%20%7C%20%2071%2C227.6%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20363.17%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%202048%20%7C%20%2014%2C045.8%20us%20%7C%20%2050.55%20us%20%7C%20%2067.48%20us%20%7C%20%2014%2C029.9%20us%20%7C%20%200.20%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2084.85%20KB%20%7C%20%20%20%20%20%20%20%200.23%20%7C%5Cn%7C%20WithJson%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%202048%20%7C%20%20%201%2C577.1%20us%20%7C%20%2032.17%20us%20%7C%20%2042.95%20us%20%7C%20%20%201%2C564.8%20us%20%7C%20%200.02%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2061.07%20KB%20%7C%20%20%20%20%20%20%20%200.17%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%202%20%7C%20%20%20%20%20788.9%20us%20%7C%20%2020.31%20us%20%7C%20%2027.11%20us%20%7C%20%20%20%20%20778.1%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2020.74%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%202%20%7C%20%20%20%20%20487.6%20us%20%7C%20%2030.51%20us%20%7C%20%2040.74%20us%20%7C%20%20%20%20%20487.7%20us%20%7C%20%200.62%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.23%20KB%20%7C%20%20%20%20%20%20%20%201.99%20%7C%5Cn%7C%20WithJson%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%202%20%7C%20%20%20%20%20434.7%20us%20%7C%20%2033.42%20us%20%7C%20%2044.61%20us%20%7C%20%20%20%20%20443.3%20us%20%7C%20%200.55%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.19%20KB%20%7C%20%20%20%20%20%20%20%201.99%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%208%20%7C%20%20%20%20%20939.1%20us%20%7C%20%2029.24%20us%20%7C%20%2039.04%20us%20%7C%20%20%20%20%20921.1%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2023.49%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%208%20%7C%20%20%20%20%20515.1%20us%20%7C%20%2032.95%20us%20%7C%20%2043.99%20us%20%7C%20%20%20%20%20509.2%20us%20%7C%20%200.55%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2042.23%20KB%20%7C%20%20%20%20%20%20%20%201.80%20%7C%5Cn%7C%20WithJson%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%208%20%7C%20%20%20%20%20450.0%20us%20%7C%20%2033.55%20us%20%7C%20%2044.79%20us%20%7C%20%20%20%20%20461.4%20us%20%7C%20%200.48%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.98%20KB%20%7C%20%20%20%20%20%20%20%201.79%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%2032%20%7C%20%20%201%2C566.2%20us%20%7C%20%2043.12%20us%20%7C%20%2057.56%20us%20%7C%20%20%201%2C551.3%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2033.24%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%2032%20%7C%20%20%20%20%20607.3%20us%20%7C%20%2033.01%20us%20%7C%20%2044.07%20us%20%7C%20%20%20%20%20587.0%20us%20%7C%20%200.39%20%7C%20%20%20%200.03%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2043.58%20KB%20%7C%20%20%20%20%20%20%20%201.31%20%7C%5Cn%7C%20WithJson%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%2032%20%7C%20%20%20%20%20488.4%20us%20%7C%20%2032.86%20us%20%7C%20%2043.87%20us%20%7C%20%20%20%20%20487.3%20us%20%7C%20%200.31%20%7C%20%20%20%200.03%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2043.48%20KB%20%7C%20%20%20%20%20%20%20%201.31%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20128%20%7C%20%20%205%2C140.0%20us%20%7C%20%2052.22%20us%20%7C%20%2069.71%20us%20%7C%20%20%205%2C138.2%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2074.11%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20128%20%7C%20%20%20%20%20987.8%20us%20%7C%20%2037.30%20us%20%7C%20%2049.79%20us%20%7C%20%20%20%20%20965.0%20us%20%7C%20%200.19%20%7C%20%20%20%200.01%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2051.97%20KB%20%7C%20%20%20%20%20%20%20%200.70%20%7C%5Cn%7C%20WithJson%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20128%20%7C%20%20%20%20%20665.9%20us%20%7C%20%2038.37%20us%20%7C%20%2051.23%20us%20%7C%20%20%20%20%20636.8%20us%20%7C%20%200.13%20%7C%20%20%20%200.01%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2051.12%20KB%20%7C%20%20%20%20%20%20%20%200.69%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20512%20%7C%20%2016%2C031.0%20us%20%7C%20%2074.08%20us%20%7C%20%2098.89%20us%20%7C%20%2016%2C023.7%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20219.5%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20512%20%7C%20%20%202%2C528.8%20us%20%7C%20%2038.80%20us%20%7C%20%2051.79%20us%20%7C%20%20%202%2C517.7%20us%20%7C%20%200.16%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2084.36%20KB%20%7C%20%20%20%20%20%20%20%200.38%20%7C%5Cn%7C%20WithJson%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20512%20%7C%20%20%201%2C368.8%20us%20%7C%20%2022.42%20us%20%7C%20%2029.93%20us%20%7C%20%20%201%2C355.1%20us%20%7C%20%200.09%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2080.08%20KB%20%7C%20%20%20%20%20%20%20%200.36%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%202048%20%7C%20%2071%2C956.6%20us%20%7C%20688.35%20us%20%7C%20918.93%20us%20%7C%20%2072%2C148.6%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20801.13%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%202048%20%7C%20%20%209%2C399.9%20us%20%7C%20%2076.33%20us%20%7C%20101.90%20us%20%7C%20%20%209%2C359.8%20us%20%7C%20%200.13%20%7C%20%20%20%200.00%20%7C%205.0000%20%7C%205.0000%20%7C%205.0000%20%7C%20%20213.42%20KB%20%7C%20%20%20%20%20%20%20%200.27%20%7C%5Cn%7C%20WithJson%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%202048%20%7C%20%20%204%2C463.6%20us%20%7C%20%2036.90%20us%20%7C%20%2049.26%20us%20%7C%20%20%204%2C442.6%20us%20%7C%20%200.06%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20197.4%20KB%20%7C%20%20%20%20%20%20%20%200.25%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%202%20%7C%20%20%20%20%20858.7%20us%20%7C%20%2023.34%20us%20%7C%20%2031.16%20us%20%7C%20%20%20%20%20846.2%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2021.44%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%202%20%7C%20%20%20%20%20637.4%20us%20%7C%20%2035.57%20us%20%7C%20%2047.48%20us%20%7C%20%20%20%20%20626.0%20us%20%7C%20%200.74%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2055.52%20KB%20%7C%20%20%20%20%20%20%20%202.59%20%7C%5Cn%7C%20WithJson%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%202%20%7C%20%20%20%20%20534.5%20us%20%7C%20%2030.81%20us%20%7C%20%2041.13%20us%20%7C%20%20%20%20%20528.7%20us%20%7C%20%200.62%20%7C%20%20%20%200.03%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2042.83%20KB%20%7C%20%20%20%20%20%20%20%202.00%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%208%20%7C%20%20%201%2C028.9%20us%20%7C%20%2024.07%20us%20%7C%20%2032.13%20us%20%7C%20%20%201%2C015.2%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2025.55%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%208%20%7C%20%20%20%20%20737.8%20us%20%7C%20%2044.23%20us%20%7C%20%2059.05%20us%20%7C%20%20%20%20%20727.5%20us%20%7C%20%200.72%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2056.98%20KB%20%7C%20%20%20%20%20%20%20%202.23%20%7C%5Cn%7C%20WithJson%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%208%20%7C%20%20%20%20%20641.8%20us%20%7C%20%2034.63%20us%20%7C%20%2046.23%20us%20%7C%20%20%20%20%20640.1%20us%20%7C%20%200.62%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2043.64%20KB%20%7C%20%20%20%20%20%20%20%201.71%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%2032%20%7C%20%20%201%2C692.5%20us%20%7C%20%2023.43%20us%20%7C%20%2031.27%20us%20%7C%20%20%201%2C684.7%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.84%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%2032%20%7C%20%20%201%2C016.7%20us%20%7C%20%2056.75%20us%20%7C%20%2075.76%20us%20%7C%20%20%20%20%20976.6%20us%20%7C%20%200.60%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2060.35%20KB%20%7C%20%20%20%20%20%20%20%201.44%20%7C%5Cn%7C%20WithJson%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%2032%20%7C%20%20%20%20%20871.5%20us%20%7C%20%2039.02%20us%20%7C%20%2052.10%20us%20%7C%20%20%20%20%20843.8%20us%20%7C%20%200.51%20%7C%20%20%20%200.03%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2047.29%20KB%20%7C%20%20%20%20%20%20%20%201.13%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20128%20%7C%20%20%207%2C665.5%20us%20%7C%20%2028.53%20us%20%7C%20%2038.09%20us%20%7C%20%20%207%2C662.0%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20103.65%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20128%20%7C%20%20%202%2C392.2%20us%20%7C%20%2035.64%20us%20%7C%20%2047.57%20us%20%7C%20%20%202%2C379.7%20us%20%7C%20%200.31%20%7C%20%20%20%200.01%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2074.85%20KB%20%7C%20%20%20%20%20%20%20%200.72%20%7C%5Cn%7C%20WithJson%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20128%20%7C%20%20%202%2C063.6%20us%20%7C%20%2026.61%20us%20%7C%20%2035.53%20us%20%7C%20%20%202%2C063.5%20us%20%7C%20%200.27%20%7C%20%20%20%200.01%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%2061.2%20KB%20%7C%20%20%20%20%20%20%20%200.59%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20512%20%7C%20%2026%2C444.7%20us%20%7C%20102.44%20us%20%7C%20136.75%20us%20%7C%20%2026%2C421.0%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20343.51%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20512%20%7C%20%20%208%2C134.2%20us%20%7C%20%2032.51%20us%20%7C%20%2043.41%20us%20%7C%20%20%208%2C125.8%20us%20%7C%20%200.31%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20132.34%20KB%20%7C%20%20%20%20%20%20%20%200.39%20%7C%5Cn%7C%20WithJson%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20512%20%7C%20%20%207%2C210.9%20us%20%7C%20%2033.10%20us%20%7C%20%2044.18%20us%20%7C%20%20%207%2C199.6%20us%20%7C%20%200.27%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20116.42%20KB%20%7C%20%20%20%20%20%20%20%200.34%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%202048%20%7C%20112%2C512.8%20us%20%7C%20443.78%20us%20%7C%20592.43%20us%20%7C%20112%2C461.1%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%205.0000%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%201310.32%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%202048%20%7C%20%2032%2C080.3%20us%20%7C%20138.18%20us%20%7C%20184.47%20us%20%7C%20%2032%2C075.1%20us%20%7C%20%200.29%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20361.05%20KB%20%7C%20%20%20%20%20%20%20%200.28%20%7C%5Cn%7C%20WithJson%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%202048%20%7C%20%2028%2C929.1%20us%20%7C%20%2084.67%20us%20%7C%20113.03%20us%20%7C%20%2028%2C917.8%20us%20%7C%20%200.26%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20336.47%20KB%20%7C%20%20%20%20%20%20%20%200.26%20%7C%5Cn%22%2C%22settings%22%3A%7B%22display%22%3A%22Duration%22%2C%22scale%22%3A%22Log2%22%2C%22theme%22%3A%22Dark%22%7D%7D "Click for interactive chart" \ No newline at end of file diff --git a/Version.xml b/Version.xml index 9aebfcd..58cb6c2 100644 --- a/Version.xml +++ b/Version.xml @@ -1,8 +1,8 @@  - 3.6.0 - 5.6.0 - 6.6.0 - 7.1.0 + 3.7.0 + 5.7.0 + 6.7.0 + 7.2.0 \ No newline at end of file diff --git a/benchmarks/QueryableValues.SqlServer.Benchmarks/ContainsBenchmarks.cs b/benchmarks/QueryableValues.SqlServer.Benchmarks/ContainsBenchmarks.cs index 20b0a2b..7296a47 100644 --- a/benchmarks/QueryableValues.SqlServer.Benchmarks/ContainsBenchmarks.cs +++ b/benchmarks/QueryableValues.SqlServer.Benchmarks/ContainsBenchmarks.cs @@ -4,22 +4,41 @@ using BlazarTech.QueryableValues; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; +using System.Text; namespace QueryableValues.SqlServer.Benchmarks; -[SimpleJob(RunStrategy.Monitoring, warmupCount: 1, targetCount: 25, invocationCount: 200)] +[SimpleJob(RunStrategy.Monitoring, warmupCount: 1, iterationCount: 25, invocationCount: 200)] [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] [GcServer(true), MemoryDiagnoser] public class ContainsBenchmarks { -#pragma warning disable CS8618 - private IQueryable _int32Query; - private IQueryable _guidQuery; - private IQueryable _queryableValuesInt32Query; - private IQueryable _queryableValuesGuidQuery; -#pragma warning restore CS8618 - - [Params(2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096)] + private IQueryable _int32Query = default!; + private IQueryable _guidQuery = default!; + private IQueryable _stringQuery = default!; + + private IQueryable _queryableValuesJsonInt32Query = default!; + private IQueryable _queryableValuesJsonGuidQuery = default!; + private IQueryable _queryableValuesJsonStringQuery = default!; + + private IQueryable _queryableValuesXmlInt32Query = default!; + private IQueryable _queryableValuesXmlGuidQuery = default!; + private IQueryable _queryableValuesXmlStringQuery = default!; + + public enum DataType + { + Int32, + Guid, + String + } + + [Params(DataType.Int32, DataType.Guid, DataType.String)] + //[Params(DataType.String)] + public DataType Type { get; set; } + + //[Params(512)] + //[Params(2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096)] + [Params(2, 8, 32, 128, 512, 2048)] public int NumberOfValues { get; set; } private IEnumerable GetIntValues() @@ -38,26 +57,59 @@ private IEnumerable GetGuidValues() } } + private IEnumerable GetStringValues() + { + var sb = new StringBuilder(); + + for (int i = 0; i < NumberOfValues; i++) + { + sb.Clear(); + var length = Random.Shared.Next(0, 50); + for (int x = 0; x < length; x++) + { + sb.Append((char)Random.Shared.Next(32, 126)); + } + yield return sb.ToString(); + } + } + [GlobalSetup] public void GlobalSetup() { Console.WriteLine("Initializing..."); - var dbContext = new MyDbContext(); + var dbContextXml = new MyDbContext(SqlServerSerialization.UseXml); + var dbContextJson = new MyDbContext(SqlServerSerialization.UseJson); #region Init db { - var wasCreated = dbContext.Database.EnsureCreated(); + var wasCreated = dbContextXml.Database.EnsureCreated(); if (wasCreated) { - for (int i = 0; i < 1000; i++) + const int itemsCount = 1000; + + for (int i = 0; i < itemsCount; i++) + { + dbContextXml.Add(new Int32Entity()); + dbContextXml.Add(new GuidEntity()); + } + + var i2 = 0; + + foreach (var value in GetStringValues()) { - dbContext.Add(new Int32Entity()); - dbContext.Add(new GuidEntity()); + i2++; + + dbContextXml.Add(new StringEntity { Id = $"{value}{i2}" }); + + if (i2 == itemsCount) + { + break; + } } - dbContext.SaveChanges(); + dbContextXml.SaveChanges(); } var versionParam = new SqlParameter("@Version", System.Data.SqlDbType.NVarChar, -1) @@ -65,11 +117,11 @@ public void GlobalSetup() Direction = System.Data.ParameterDirection.Output }; - dbContext.Database.ExecuteSqlRaw("SET @Version = @@VERSION;", versionParam); + dbContextXml.Database.ExecuteSqlRaw("SET @Version = @@VERSION;", versionParam); Console.WriteLine(versionParam.Value); - dbContext.Database.ExecuteSqlRaw("DBCC FREEPROCCACHE; DBCC DROPCLEANBUFFERS;"); + dbContextXml.Database.ExecuteSqlRaw("DBCC FREEPROCCACHE; DBCC DROPCLEANBUFFERS;"); } #endregion @@ -77,11 +129,14 @@ public void GlobalSetup() { var intValues = GetIntValues(); - _int32Query = dbContext.Int32Entities + _int32Query = dbContextXml.Int32Entities .Where(i => intValues.Contains(i.Id)); - _queryableValuesInt32Query = dbContext.Int32Entities - .Where(i => dbContext.AsQueryableValues(intValues).Contains(i.Id)); + _queryableValuesXmlInt32Query = dbContextXml.Int32Entities + .Where(i => dbContextXml.AsQueryableValues(intValues).Contains(i.Id)); + + _queryableValuesJsonInt32Query = dbContextJson.Int32Entities + .Where(i => dbContextJson.AsQueryableValues(intValues).Contains(i.Id)); } #endregion @@ -89,36 +144,87 @@ public void GlobalSetup() { var guidValues = GetGuidValues(); - _guidQuery = dbContext.GuidEntities + _guidQuery = dbContextXml.GuidEntities .Where(i => guidValues.Contains(i.Id)); - _queryableValuesGuidQuery = dbContext.GuidEntities - .Where(i => dbContext.AsQueryableValues(guidValues).Contains(i.Id)); + _queryableValuesXmlGuidQuery = dbContextXml.GuidEntities + .Where(i => dbContextXml.AsQueryableValues(guidValues).Contains(i.Id)); + + _queryableValuesJsonGuidQuery = dbContextJson.GuidEntities + .Where(i => dbContextJson.AsQueryableValues(guidValues).Contains(i.Id)); } #endregion - } - [Benchmark(Baseline = true), BenchmarkCategory("Int32")] - public void Without_Int32() - { - _int32Query.Any(); + #region String Queries + { + var stringValues = GetStringValues(); + + _stringQuery = dbContextXml.StringEntities + .Where(i => stringValues.Contains(i.Id)); + + _queryableValuesXmlStringQuery = dbContextXml.StringEntities + .Where(i => dbContextXml.AsQueryableValues(stringValues, true).Contains(i.Id)); + + _queryableValuesJsonStringQuery = dbContextJson.StringEntities + .Where(i => dbContextJson.AsQueryableValues(stringValues, true).Contains(i.Id)); + } + #endregion } - [Benchmark, BenchmarkCategory("Int32")] - public void With_Int32() + [Benchmark(Baseline = true)] + public void Without() { - _queryableValuesInt32Query.Any(); + switch (Type) + { + case DataType.Int32: + _int32Query.Any(); + break; + case DataType.Guid: + _guidQuery.Any(); + break; + case DataType.String: + _stringQuery.Any(); + break; + default: + throw new NotImplementedException(); + } } - [Benchmark(Baseline = true), BenchmarkCategory("Guid")] - public void Without_Guid() + [Benchmark] + public void WithXml() { - _guidQuery.Any(); + switch (Type) + { + case DataType.Int32: + _queryableValuesXmlInt32Query.Any(); + break; + case DataType.Guid: + _queryableValuesXmlGuidQuery.Any(); + break; + case DataType.String: + _queryableValuesXmlStringQuery.Any(); + break; + default: + throw new NotImplementedException(); + } } - [Benchmark, BenchmarkCategory("Guid")] - public void With_Guid() + [Benchmark] + public void WithJson() { - _queryableValuesGuidQuery.Any(); + switch (Type) + { + case DataType.Int32: + _queryableValuesJsonInt32Query.Any(); + break; + case DataType.Guid: + _queryableValuesJsonGuidQuery.Any(); + break; + case DataType.String: + _queryableValuesJsonStringQuery.Any(); + break; + default: + throw new NotImplementedException(); + } } -} +} \ No newline at end of file diff --git a/benchmarks/QueryableValues.SqlServer.Benchmarks/MyDbContext.cs b/benchmarks/QueryableValues.SqlServer.Benchmarks/MyDbContext.cs index 074201f..6fdc15e 100644 --- a/benchmarks/QueryableValues.SqlServer.Benchmarks/MyDbContext.cs +++ b/benchmarks/QueryableValues.SqlServer.Benchmarks/MyDbContext.cs @@ -1,20 +1,27 @@ using BlazarTech.QueryableValues; using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; namespace QueryableValues.SqlServer.Benchmarks { public class MyDbContext : DbContext { -#pragma warning disable CS8618 - public DbSet Int32Entities { get; set; } - public DbSet GuidEntities { get; set; } -#pragma warning restore CS8618 + private readonly SqlServerSerialization _serializationOption; + + public DbSet Int32Entities { get; set; } = default!; + public DbSet GuidEntities { get; set; } = default!; + public DbSet StringEntities { get; set; } = default!; + + public MyDbContext(SqlServerSerialization serializationOption) + { + _serializationOption = serializationOption; + } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer( - @"Server=.\SQLEXPRESS;Integrated Security=true;Database=QueryableValuesBenchmarks", - builder => builder.UseQueryableValues() + @"Server=.\SQLEXPRESS;Integrated Security=true;Database=QueryableValuesBenchmarks;Encrypt=False;", + builder => builder.UseQueryableValues(options => options.Serialization(_serializationOption)) ); } @@ -33,4 +40,10 @@ public class GuidEntity { public Guid Id { get; set; } } + + public class StringEntity + { + [MaxLength(100)] + public string Id { get; set; } = default!; + } } diff --git a/benchmarks/QueryableValues.SqlServer.Benchmarks/Program.cs b/benchmarks/QueryableValues.SqlServer.Benchmarks/Program.cs index 64d8299..ae4fb51 100644 --- a/benchmarks/QueryableValues.SqlServer.Benchmarks/Program.cs +++ b/benchmarks/QueryableValues.SqlServer.Benchmarks/Program.cs @@ -1,4 +1,5 @@ -using BenchmarkDotNet.Running; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Running; namespace QueryableValues.SqlServer.Benchmarks; @@ -6,6 +7,11 @@ class Program { static void Main(string[] args) { - var summary = BenchmarkRunner.Run(); + var config = new ManualConfig(); + + config.Add(DefaultConfig.Instance); + config.WithOptions(ConfigOptions.DisableOptimizationsValidator); + + BenchmarkRunner.Run(config); } } \ No newline at end of file diff --git a/benchmarks/QueryableValues.SqlServer.Benchmarks/QueryableValues.SqlServer.Benchmarks.csproj b/benchmarks/QueryableValues.SqlServer.Benchmarks/QueryableValues.SqlServer.Benchmarks.csproj index d713035..40380b1 100644 --- a/benchmarks/QueryableValues.SqlServer.Benchmarks/QueryableValues.SqlServer.Benchmarks.csproj +++ b/benchmarks/QueryableValues.SqlServer.Benchmarks/QueryableValues.SqlServer.Benchmarks.csproj @@ -8,9 +8,12 @@ - - - + + + + + + diff --git a/docs/README.md b/docs/README.md index 43a0ff3..dd2b4f7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,6 +4,8 @@ [![GitHub Stars](https://badgen.net/github/stars/yv989c/BlazarTech.QueryableValues?icon=github)][Repository] [![Nuget Downloads](https://badgen.net/nuget/dt/BlazarTech.QueryableValues.SqlServer?icon=nuget)][NuGet Package] +> 🤔💭 TLDR; By using QueryableValues, you can incorporate in-memory collections into your EF queries with outstanding performance and flexibility. + This library allows you to efficiently compose an [IEnumerable<T>] in your [Entity Framework Core] queries when using the [SQL Server Database Provider]. This is accomplished by using the `AsQueryableValues` extension method available on the [DbContext] class. Everything is evaluated on the server with a single round trip, in a way that preserves the query's [execution plan], even when the values behind the [IEnumerable<T>] are changed on subsequent executions. The supported types for `T` are: @@ -15,6 +17,8 @@ The supported types for `T` are: For a detailed explanation of the problem solved by QueryableValues, please continue reading [here][readme-background]. +> ✅ QueryableValues boasts over 120 integration tests that are executed on every supported version of EF. These tests ensure reliability and compatibility, giving you added confidence. + > 💡 Still on Entity Framework 6 (non-core)? Then [QueryableValues `EF6 Edition`](https://github.com/yv989c/BlazarTech.QueryableValues.EF6) is what you need. ## When Should You Use It? @@ -75,6 +79,7 @@ public class Startup } } ``` +> 💡 Pro-tip: `UseQueryableValues` offers an optional `options` delegate for additional configurations. ## How Do You Use It? The `AsQueryableValues` extension method is provided by the `BlazarTech.QueryableValues` namespace; therefore, you must add the following `using` directive to your source code file for it to appear as a method of your [DbContext] instance: diff --git a/docs/images/benchmarks/v7.2.0.png b/docs/images/benchmarks/v7.2.0.png new file mode 100644 index 0000000..1b88840 Binary files /dev/null and b/docs/images/benchmarks/v7.2.0.png differ diff --git a/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs b/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs index 8147f6f..ce49e9d 100644 --- a/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs +++ b/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs @@ -1,5 +1,6 @@ #if EFCORE using BlazarTech.QueryableValues.Builders; +using BlazarTech.QueryableValues.SqlServer; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using System; @@ -13,20 +14,6 @@ namespace BlazarTech.QueryableValues /// public static class QueryableValuesDbContextExtensions { - private static void EnsureConfigured(DbContext dbContext) - { - var options = dbContext.GetService(); - var extension = options.FindExtension(); - - if (extension is null) - { - var message = $"{nameof(QueryableValues)} have not been configured for {dbContext.GetType().Name}. " + - "More info: https://github.com/yv989c/BlazarTech.QueryableValues#configuration"; - - throw new InvalidOperationException(message); - } - } - private static void ValidateParameters(DbContext dbContext, IEnumerable values) { if (dbContext is null) @@ -38,13 +25,21 @@ private static void ValidateParameters(DbContext dbContext, IEnumerable va { throw new ArgumentNullException(nameof(values)); } - - EnsureConfigured(dbContext); } private static IQueryableFactory GetQueryableFactory(DbContext dbContext) { - return dbContext.GetService(); + try + { + return dbContext.GetService()?.Create(dbContext) ?? throw new InvalidOperationException(); + } + catch (InvalidOperationException) + { + var message = $"{nameof(QueryableValues)} have not been configured for {dbContext.GetType().Name}. " + + "More info: https://github.com/yv989c/BlazarTech.QueryableValues#configuration"; + + throw new InvalidOperationException(message); + } } /// diff --git a/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs b/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs index 2c0d757..132a330 100644 --- a/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs +++ b/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs @@ -1,4 +1,5 @@ #if EFCORE +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; using System; @@ -49,14 +50,12 @@ public void ApplyServices(IServiceCollection services) } services.AddSingleton(); - - services.AddScoped(sp => - { - var options = sp.GetRequiredService(); - var extension = options.FindExtension() ?? throw new InvalidOperationException(); - var xmlSerializer = sp.GetRequiredService(); - return new SqlServer.XmlQueryableFactory(xmlSerializer, extension.Options); - }); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } public void Validate(IDbContextOptions options) @@ -80,11 +79,11 @@ public override void PopulateDebugInfo(IDictionary debugInfo) { } -#if EFCORE6 || EFCORE7 +#if EFCORE3 || EFCORE5 + public override long GetServiceProviderHashCode() => 0; +#else public override int GetServiceProviderHashCode() => 0; public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) => true; -#else - public override long GetServiceProviderHashCode() => 0; #endif } } diff --git a/src/QueryableValues.SqlServer/QueryableValuesSqlServerOptions.cs b/src/QueryableValues.SqlServer/QueryableValuesSqlServerOptions.cs index 0ee1ab7..1acee93 100644 --- a/src/QueryableValues.SqlServer/QueryableValuesSqlServerOptions.cs +++ b/src/QueryableValues.SqlServer/QueryableValuesSqlServerOptions.cs @@ -10,6 +10,7 @@ public sealed class QueryableValuesSqlServerOptions { internal bool WithUseSelectTopOptimization { get; private set; } = true; internal bool WithUseDeferredEnumeration { get; private set; } = true; + internal SqlServerSerialization WithSerializationOption { get; private set; } = SqlServerSerialization.Auto; /// /// When possible, uses a TOP(n) clause in the underlying SELECT statement to assist SQL Server memory grant estimation. The default is . @@ -26,6 +27,25 @@ public QueryableValuesSqlServerOptions UseSelectTopOptimization(bool useSelectTo return this; } + /// + /// Configures the serialization format to be used when sending data to SQL Server. The default is . + /// + /// Serialization options. + /// The same instance so subsequent configurations can be chained. + public QueryableValuesSqlServerOptions Serialization(SqlServerSerialization option = SqlServerSerialization.Auto) + { + if (Enum.IsDefined(typeof(SqlServerSerialization), option)) + { + WithSerializationOption = option; + } + else + { + throw new ArgumentOutOfRangeException(nameof(option)); + } + + return this; + } + #if !EFCORE3 /// /// If , the provided to any of the AsQueryableValues methods will be enumerated when the query is materialized; otherwise, it will be immediately enumerated once. The default is . diff --git a/src/QueryableValues.SqlServer/Serializers/IJsonSerializer.cs b/src/QueryableValues.SqlServer/Serializers/IJsonSerializer.cs new file mode 100644 index 0000000..fe4ef74 --- /dev/null +++ b/src/QueryableValues.SqlServer/Serializers/IJsonSerializer.cs @@ -0,0 +1,6 @@ +namespace BlazarTech.QueryableValues.Serializers +{ + internal interface IJsonSerializer : ISerializer + { + } +} diff --git a/src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs b/src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs new file mode 100644 index 0000000..311d30e --- /dev/null +++ b/src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs @@ -0,0 +1,225 @@ +using Microsoft.IO; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; + +namespace BlazarTech.QueryableValues.Serializers +{ + internal sealed class JsonSerializer : IJsonSerializer + { + private static readonly RecyclableMemoryStreamManager MemoryStreamManager = new RecyclableMemoryStreamManager(); + + private static string SerializePrivate(T values) + { + return System.Text.Json.JsonSerializer.Serialize(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values, IReadOnlyList propertyMappings) + where T : notnull + { + var properties = new PropertyWriter[propertyMappings.Count]; + + for (var i = 0; i < properties.Length; i++) + { + properties[i] = new PropertyWriter(propertyMappings[i]); + } + + void writeEntity(Utf8JsonWriter writer, T entity) + { + for (int i = 0; i < properties.Length; i++) + { + properties[i].WriteValue(writer, entity); + } + } + + static bool mustSkipValue(T v) => v is null; + + return GetJson(values, writeEntity, mustSkipValue); + + static string GetJson(IEnumerable values, Action writeValue, Func? mustSkipValue = null) + { + using var stream = (RecyclableMemoryStream)MemoryStreamManager.GetStream(); + + using (var jsonWriter = new Utf8JsonWriter((IBufferWriter)stream)) + { + jsonWriter.WriteStartArray(); + + foreach (var value in values) + { + if (mustSkipValue?.Invoke(value) == true) + { + continue; + } + + jsonWriter.WriteStartObject(); + + writeValue(jsonWriter, value); + + jsonWriter.WriteEndObject(); + } + + jsonWriter.WriteEndArray(); + } + +#if NETSTANDARD2_0 + var streamInt32Length = (int)stream.Length; + return Encoding.UTF8.GetString(stream.GetBuffer(), 0, streamInt32Length); +#elif NETSTANDARD2_1_OR_GREATER + stream.Position = 0; + var streamInt32Length = (int)stream.Length; + return Encoding.UTF8.GetString(stream.GetSpan()[..streamInt32Length]); +#else + return Encoding.UTF8.GetString(stream.GetReadOnlySequence()); +#endif + } + } + + private sealed class PropertyWriter + { + private static readonly Action WriteBool = (Utf8JsonWriter writer, bool value) => writer.WriteBooleanValue(value); + private static readonly Action WriteByte = (Utf8JsonWriter writer, byte value) => writer.WriteNumberValue(value); + private static readonly Action WriteInt16 = (Utf8JsonWriter writer, short value) => writer.WriteNumberValue(value); + private static readonly Action WriteInt32 = (Utf8JsonWriter writer, int value) => writer.WriteNumberValue(value); + private static readonly Action WriteInt64 = (Utf8JsonWriter writer, long value) => writer.WriteNumberValue(value); + private static readonly Action WriteDecimal = (Utf8JsonWriter writer, decimal value) => writer.WriteNumberValue(value); + private static readonly Action WriteSingle = (Utf8JsonWriter writer, float value) => writer.WriteNumberValue(value); + private static readonly Action WriteDouble = (Utf8JsonWriter writer, double value) => writer.WriteNumberValue(value); + + private static readonly Action WriteDateTime = (Utf8JsonWriter writer, DateTime value) => + { + if (value.Kind != DateTimeKind.Unspecified) + { + writer.WriteStringValue(DateTime.SpecifyKind(value, DateTimeKind.Unspecified)); + } + else + { + writer.WriteStringValue(value); + } + }; + + private static readonly Action WriteDateTimeOffset = (Utf8JsonWriter writer, DateTimeOffset value) => writer.WriteStringValue(value); + private static readonly Action WriteGuid = (Utf8JsonWriter writer, Guid value) => writer.WriteStringValue(value); + private static readonly Action WriteChar = (Utf8JsonWriter writer, char value) => writer.WriteStringValue(stackalloc[] { value }); + + private readonly string _targetName; + private readonly Action? _writeValue; + + public EntityPropertyMapping Mapping { get; } + + public PropertyWriter(EntityPropertyMapping mapping) + { + Mapping = mapping; + + _targetName = mapping.Target.Name; + + _writeValue = mapping.TypeName switch + { + EntityPropertyTypeName.Boolean => (writer, value) => WriteAttribute(writer, (bool?)value, WriteBool), + EntityPropertyTypeName.Byte => (writer, value) => WriteAttribute(writer, (byte?)value, WriteByte), + EntityPropertyTypeName.Int16 => (writer, value) => WriteAttribute(writer, (short?)value, WriteInt16), + EntityPropertyTypeName.Int32 => (writer, value) => WriteAttribute(writer, (int?)value, WriteInt32), + EntityPropertyTypeName.Int64 => (writer, value) => WriteAttribute(writer, (long?)value, WriteInt64), + EntityPropertyTypeName.Decimal => (writer, value) => WriteAttribute(writer, (decimal?)value, WriteDecimal), + EntityPropertyTypeName.Single => (writer, value) => WriteAttribute(writer, (float?)value, WriteSingle), + EntityPropertyTypeName.Double => (writer, value) => WriteAttribute(writer, (double?)value, WriteDouble), + EntityPropertyTypeName.DateTime => (writer, value) => WriteAttribute(writer, (DateTime?)value, WriteDateTime), + EntityPropertyTypeName.DateTimeOffset => (writer, value) => WriteAttribute(writer, (DateTimeOffset?)value, WriteDateTimeOffset), + EntityPropertyTypeName.Guid => (writer, value) => WriteAttribute(writer, (Guid?)value, WriteGuid), + EntityPropertyTypeName.Char => (writer, value) => WriteAttribute(writer, (char?)value, WriteChar), + EntityPropertyTypeName.String => (writer, value) => WriteStringAttribute(writer, (string?)value), + _ => throw new NotImplementedException(mapping.TypeName.ToString()), + }; + } + + private void WriteAttribute(Utf8JsonWriter writer, TValue? value, Action writeValue) + where TValue : struct + { + if (value.HasValue) + { + writer.WritePropertyName(_targetName); + writeValue(writer, value.Value); + } + } + + private void WriteStringAttribute(Utf8JsonWriter writer, string? value) + { + if (value != null) + { + writer.WritePropertyName(_targetName); + writer.WriteStringValue(value); + } + } + + public void WriteValue(Utf8JsonWriter writer, object entity) + { + if (_writeValue != null) + { + var value = Mapping.Source.GetValue(entity); + _writeValue.Invoke(writer, value); + } + } + } + } +} diff --git a/src/QueryableValues.SqlServer/SqlServer/ExtensionOptions.cs b/src/QueryableValues.SqlServer/SqlServer/ExtensionOptions.cs new file mode 100644 index 0000000..e3401b6 --- /dev/null +++ b/src/QueryableValues.SqlServer/SqlServer/ExtensionOptions.cs @@ -0,0 +1,17 @@ +#if EFCORE +using Microsoft.EntityFrameworkCore.Infrastructure; +using System; + +namespace BlazarTech.QueryableValues.SqlServer +{ + internal sealed class ExtensionOptions + { + public QueryableValuesSqlServerOptions Options { get; } + + public ExtensionOptions(IDbContextOptions dbContextOptions) + { + Options = (dbContextOptions.FindExtension()?.Options) ?? throw new InvalidOperationException(); + } + } +} +#endif diff --git a/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs b/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs new file mode 100644 index 0000000..6028f5a --- /dev/null +++ b/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs @@ -0,0 +1,232 @@ +#if EFCORE +using BlazarTech.QueryableValues.Builders; +using BlazarTech.QueryableValues.Serializers; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.Infrastructure; +using System; +using System.Collections.Generic; +using System.Data; + +namespace BlazarTech.QueryableValues.SqlServer +{ + internal sealed class JsonQueryableFactory : QueryableFactory + { + public JsonQueryableFactory(IJsonSerializer serializer, IDbContextOptions dbContextOptions) + : base(serializer, dbContextOptions) + { + } + + protected override SqlParameter GetValuesParameter() + { + return new SqlParameter(null, SqlDbType.NVarChar, -1); + } + + private string GetSqlForSimpleTypes(string sqlType, DeferredValues deferredValues, (int Precision, int Scale)? precisionScale = null) + where T : notnull + { + var useSelectTopOptimization = UseSelectTopOptimization(deferredValues); + var cacheKeyProperties = new + { + SqlType = sqlType, + UseSelectTopOptimization = useSelectTopOptimization, + PrecisionScale = precisionScale + }; + + var cacheKey = GetCacheKey(cacheKeyProperties); + + if (SqlCache.TryGetValue(cacheKey, out string? sql)) + { + return sql; + } + + var sqlPrefix = useSelectTopOptimization ? SqlSelectTop : SqlSelect; + var sqlTypeArguments = precisionScale.HasValue ? $"({precisionScale.Value.Precision},{precisionScale.Value.Scale})" : null; + + sql = + $"{sqlPrefix} [V] " + + $"FROM OPENJSON({{0}}) WITH ([V] {sqlType}{sqlTypeArguments} '$', [_] BIT '$._') ORDER BY [_]"; + + SqlCache.TryAdd(cacheKey, sql); + + return sql; + } + + protected override string GetSqlForSimpleTypesByte(DeferredValues deferredValues) + { + return GetSqlForSimpleTypes("tinyint", deferredValues); + } + + protected override string GetSqlForSimpleTypesInt16(DeferredValues deferredValues) + { + return GetSqlForSimpleTypes("smallint", deferredValues); + } + + protected override string GetSqlForSimpleTypesInt32(DeferredValues deferredValues) + { + return GetSqlForSimpleTypes("int", deferredValues); + } + + protected override string GetSqlForSimpleTypesInt64(DeferredValues deferredValues) + { + return GetSqlForSimpleTypes("bigint", deferredValues); + } + + protected override string GetSqlForSimpleTypesDecimal(DeferredValues deferredValues, (int Precision, int Scale) precisionScale) + { + return GetSqlForSimpleTypes("decimal", deferredValues, precisionScale: precisionScale); + } + + protected override string GetSqlForSimpleTypesSingle(DeferredValues deferredValues) + { + return GetSqlForSimpleTypes("real", deferredValues); + } + + protected override string GetSqlForSimpleTypesDouble(DeferredValues deferredValues) + { + return GetSqlForSimpleTypes("float", deferredValues); + } + + protected override string GetSqlForSimpleTypesDateTime(DeferredValues deferredValues) + { + return GetSqlForSimpleTypes("datetime2", deferredValues); + } + + protected override string GetSqlForSimpleTypesDateTimeOffset(DeferredValues deferredValues) + { + return GetSqlForSimpleTypes("datetimeoffset", deferredValues); + } + + protected override string GetSqlForSimpleTypesChar(DeferredValues deferredValues, bool isUnicode) + { + return GetSqlForSimpleTypes(isUnicode ? "nvarchar(1)" : "varchar(1)", deferredValues); + } + + protected override string GetSqlForSimpleTypesString(DeferredValues deferredValues, bool isUnicode) + { + return GetSqlForSimpleTypes(isUnicode ? "nvarchar(max)" : "varchar(max)", deferredValues); + } + + protected override string GetSqlForSimpleTypesGuid(DeferredValues deferredValues) + { + return GetSqlForSimpleTypes("uniqueidentifier", deferredValues); + } + + protected override string GetSqlForComplexTypes(IEntityOptionsBuilder entityOptions, bool useSelectTopOptimization, IReadOnlyList mappings) + { + var sb = StringBuilderPool.Get(); + + try + { + if (useSelectTopOptimization) + { + sb.Append(SqlSelectTop); + } + else + { + sb.Append(SqlSelect); + } + + sb.Append(' '); + + for (var i = 0; i < mappings.Count; i++) + { + if (i > 0) + { + sb.Append(", "); + } + + sb.Append('[').Append(mappings[i].Target.Name).Append(']'); + } + + sb.AppendLine(); + sb.Append("FROM OPENJSON({0}) WITH ("); + + for (var i = 0; i < mappings.Count; i++) + { + var mapping = mappings[i]; + var propertyOptions = entityOptions.GetPropertyOptions(mapping.Source); + + if (i > 0) + { + sb.Append(", "); + } + + var targetName = mapping.Target.Name; + + sb.Append('[').Append(targetName).Append("] "); + + switch (mapping.TypeName) + { + case EntityPropertyTypeName.Boolean: + sb.Append("bit"); + break; + case EntityPropertyTypeName.Byte: + sb.Append("tinyint"); + break; + case EntityPropertyTypeName.Int16: + sb.Append("smallint"); + break; + case EntityPropertyTypeName.Int32: + sb.Append("int"); + break; + case EntityPropertyTypeName.Int64: + sb.Append("bigint"); + break; + case EntityPropertyTypeName.Decimal: + { + var numberOfDecimals = propertyOptions?.NumberOfDecimals ?? entityOptions.DefaultForNumberOfDecimals; + sb.Append("decimal(38, ").Append(numberOfDecimals).Append(')'); + } + break; + case EntityPropertyTypeName.Single: + sb.Append("real"); + break; + case EntityPropertyTypeName.Double: + sb.Append("float"); + break; + case EntityPropertyTypeName.DateTime: + sb.Append("datetime2"); + break; + case EntityPropertyTypeName.DateTimeOffset: + sb.Append("datetimeoffset"); + break; + case EntityPropertyTypeName.Guid: + sb.Append("uniqueidentifier"); + break; + case EntityPropertyTypeName.Char: + if ((propertyOptions?.IsUnicode ?? entityOptions.DefaultForIsUnicode) == true) + { + sb.Append("nvarchar(1)"); + } + else + { + sb.Append("varchar(1)"); + } + break; + case EntityPropertyTypeName.String: + if ((propertyOptions?.IsUnicode ?? entityOptions.DefaultForIsUnicode) == true) + { + sb.Append("nvarchar(max)"); + } + else + { + sb.Append("varchar(max)"); + } + break; + default: + throw new NotImplementedException(mapping.TypeName.ToString()); + } + } + + sb.Append(", [_] BIT '$._') ORDER BY [_]"); + + return sb.ToString(); + } + finally + { + StringBuilderPool.Return(sb); + } + } + } +} +#endif \ No newline at end of file diff --git a/src/QueryableValues.SqlServer/SqlServer/JsonSupportConnectionInterceptor.cs b/src/QueryableValues.SqlServer/SqlServer/JsonSupportConnectionInterceptor.cs new file mode 100644 index 0000000..d2aec15 --- /dev/null +++ b/src/QueryableValues.SqlServer/SqlServer/JsonSupportConnectionInterceptor.cs @@ -0,0 +1,106 @@ +#if EFCORE +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Concurrent; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace BlazarTech.QueryableValues.SqlServer +{ + sealed class JsonSupportConnectionInterceptor : DbConnectionInterceptor + { + private static readonly ConcurrentDictionary ConnectionStringJsonSupport = new(); + + private readonly ILogger _logger; + private readonly QueryableValuesSqlServerOptions _options; + + public JsonSupportConnectionInterceptor(ILoggerFactory loggerFactory, ExtensionOptions extensionOptions) + { + _logger = loggerFactory.CreateLogger(new DbLoggerCategory.Database.Command()); + _options = extensionOptions.Options; + } + + private static string GetKey(DbConnection connection) + { + return connection.ConnectionString; + } + + public static bool? HasJsonSupport(DbContext dbContext) + { + var connection = dbContext.Database.GetDbConnection(); + + if (ConnectionStringJsonSupport.TryGetValue(GetKey(connection), out var hasJsonSupport)) + { + return hasJsonSupport; + } + + return null; + } + + private bool MustDetect(DbConnection connection) + { + return + _options.WithSerializationOption == SqlServerSerialization.Auto && + !ConnectionStringJsonSupport.ContainsKey(GetKey(connection)); + } + + public override async Task ConnectionOpenedAsync(DbConnection connection, ConnectionEndEventData eventData, CancellationToken cancellationToken = default) + { + if (connection is SqlConnection sqlConnection && MustDetect(sqlConnection)) + { + await DetectJsonSupportAsync(sqlConnection).ConfigureAwait(false); + } + } + + public override void ConnectionOpened(DbConnection connection, ConnectionEndEventData eventData) + { + if (connection is SqlConnection sqlConnection && MustDetect(sqlConnection)) + { + DetectJsonSupportAsync(sqlConnection) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + } + } + + private async ValueTask DetectJsonSupportAsync(SqlConnection connection) + { + var hasJsonSupport = false; + + try + { + var majorVersionNumber = getMajorVersionNumber(connection.ServerVersion); + + // https://learn.microsoft.com/en-us/sql/t-sql/functions/openjson-transact-sql + if (majorVersionNumber >= 13) + { + using var cm = new SqlCommand("SELECT [compatibility_level] FROM [sys].[databases] WHERE [database_id] = DB_ID()", connection); + var compatibilityLevelObject = await cm.ExecuteScalarAsync().ConfigureAwait(false); + var compatibilityLevel = Convert.ToInt32(compatibilityLevelObject); + hasJsonSupport = compatibilityLevel >= 130; + } + } + catch (Exception ex) + { + _logger.LogError(ex, ""); + } + + ConnectionStringJsonSupport.TryAdd(GetKey(connection), hasJsonSupport); + + static int getMajorVersionNumber(string? serverVersion) + { + if (Version.TryParse(serverVersion, out var version)) + { + return version.Major; + } + + return 0; + } + } + } +} +#endif \ No newline at end of file diff --git a/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs b/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs new file mode 100644 index 0000000..0a355e0 --- /dev/null +++ b/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs @@ -0,0 +1,480 @@ +#if EFCORE +using BlazarTech.QueryableValues.Builders; +using BlazarTech.QueryableValues.Serializers; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.ObjectPool; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace BlazarTech.QueryableValues.SqlServer +{ + internal abstract class QueryableFactory : IQueryableFactory + { + protected const string SqlSelect = "SELECT TOP(2147483647)"; + protected const string SqlSelectTop = "SELECT TOP({1})"; + + protected static readonly ConcurrentDictionary SqlCache = new(); + private static readonly ConcurrentDictionary SelectorExpressionCache = new(); + + protected static readonly DefaultObjectPool StringBuilderPool = new DefaultObjectPool( + new StringBuilderPooledObjectPolicy + { + InitialCapacity = 1024, + MaximumRetainedCapacity = 16384 + }); + + private readonly ISerializer _serializer; + private readonly QueryableValuesSqlServerOptions _options; + private readonly string _cacheScopeName; + + public QueryableFactory(ISerializer serializer, IDbContextOptions dbContextOptions) + { + if (serializer is null) + { + throw new ArgumentNullException(nameof(serializer)); + } + + if (dbContextOptions is null) + { + throw new ArgumentNullException(nameof(dbContextOptions)); + } + + var extension = dbContextOptions.FindExtension() ?? throw new InvalidOperationException($"{nameof(QueryableValuesSqlServerExtension)} not found."); + + _serializer = serializer; + _options = extension.Options; + _cacheScopeName = GetType().Name ?? throw new InvalidOperationException(); + } + + protected object GetCacheKey(object properties) + { + return new + { + Scope = _cacheScopeName, + Properties = properties + }; + } + + /// + /// Used to optimize the generated SQL by providing a TOP(n) on the SELECT statement. + /// In my tests, I observed improved memory grant estimation by SQL Server's query engine. + /// + protected bool UseSelectTopOptimization(DeferredValues deferredValues) + where T : notnull + { +#if EFCORE3 + // In my EF Core 3 tests, it seems that on the first execution of the query, + // it is caching the values from the parameters provided to the FromSqlRaw method. + // This imposes a problem when trying to optimize the SQL using the HasCount property in this class. + // It is critical to know the exact number of elements behind "values" at execution time, + // this is because the number of items behind "values" can change between executions of the query, + // therefore, this optimization cannot be done in a reliable way under EF Core 3. + // + // Under EF Core 5 and 6 this is not an issue. The parameters are always evaluated on each execution. + return false; +#else + return + _options.WithUseSelectTopOptimization && + deferredValues.HasCount; +#endif + } + + protected abstract SqlParameter GetValuesParameter(); + + private SqlParameter[] GetSqlParameters(DeferredValues deferredValues) + where T : notnull + { + SqlParameter[] sqlParameters; + + var valuesParameter = GetValuesParameter(); + + // Missing parameter names are auto-generated (p0, p1, etc.) by FromSqlRaw based on its position in the array. + valuesParameter.ParameterName = null; + + // DeferredValues allows us to defer the enumeration of values until the query is materialized. + valuesParameter.Value = _options.WithUseDeferredEnumeration ? deferredValues : deferredValues.ToString(null); + + if (UseSelectTopOptimization(deferredValues)) + { + // bigint to avoid implicit casting by the TOP operation (observed in the execution plan). + var countParameter = new SqlParameter(null, SqlDbType.BigInt) + { + Value = _options.WithUseDeferredEnumeration ? deferredValues : deferredValues.ToInt64(null) + }; + + sqlParameters = new[] { valuesParameter, countParameter }; + } + else + { + sqlParameters = new[] { valuesParameter }; + } + + return sqlParameters; + } + + protected abstract string GetSqlForSimpleTypesByte(DeferredValues deferredValues); + protected abstract string GetSqlForSimpleTypesInt16(DeferredValues deferredValues); + protected abstract string GetSqlForSimpleTypesInt32(DeferredValues deferredValues); + protected abstract string GetSqlForSimpleTypesInt64(DeferredValues deferredValues); + protected abstract string GetSqlForSimpleTypesDecimal(DeferredValues deferredValues, (int Precision, int Scale) precisionScale); + protected abstract string GetSqlForSimpleTypesSingle(DeferredValues deferredValues); + protected abstract string GetSqlForSimpleTypesDouble(DeferredValues deferredValues); + protected abstract string GetSqlForSimpleTypesDateTime(DeferredValues deferredValues); + protected abstract string GetSqlForSimpleTypesDateTimeOffset(DeferredValues deferredValues); + protected abstract string GetSqlForSimpleTypesChar(DeferredValues deferredValues, bool isUnicode); + protected abstract string GetSqlForSimpleTypesString(DeferredValues deferredValues, bool isUnicode); + protected abstract string GetSqlForSimpleTypesGuid(DeferredValues deferredValues); + + private IQueryable Create(DbContext dbContext, string sql, DeferredValues deferredValues) + where TValue : notnull + { + var sqlParameters = GetSqlParameters(deferredValues); + + var queryableValues = dbContext + .Set>() + .FromSqlRaw(sql, sqlParameters); + + return queryableValues.Select(i => i.V); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredByteValues(_serializer, values); + var sql = GetSqlForSimpleTypesByte(deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredInt16Values(_serializer, values); + var sql = GetSqlForSimpleTypesInt16(deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredInt32Values(_serializer, values); + var sql = GetSqlForSimpleTypesInt32(deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredInt64Values(_serializer, values); + var sql = GetSqlForSimpleTypesInt64(deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values, int numberOfDecimals = 4) + { + var deferredValues = new DeferredDecimalValues(_serializer, values); + var precisionScale = (38, numberOfDecimals); + var sql = GetSqlForSimpleTypesDecimal(deferredValues, precisionScale); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredSingleValues(_serializer, values); + var sql = GetSqlForSimpleTypesSingle(deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredDoubleValues(_serializer, values); + var sql = GetSqlForSimpleTypesDouble(deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredDateTimeValues(_serializer, values); + var sql = GetSqlForSimpleTypesDateTime(deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredDateTimeOffsetValues(_serializer, values); + var sql = GetSqlForSimpleTypesDateTimeOffset(deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values, bool isUnicode = false) + { + var deferredValues = new DeferredCharValues(_serializer, values); + var sql = GetSqlForSimpleTypesChar(deferredValues, isUnicode); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values, bool isUnicode = false) + { + var deferredValues = new DeferredStringValues(_serializer, values); + var sql = GetSqlForSimpleTypesString(deferredValues, isUnicode); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredGuidValues(_serializer, values); + var sql = GetSqlForSimpleTypesGuid(deferredValues); + return Create(dbContext, sql, deferredValues); + } + + protected abstract string GetSqlForComplexTypes( + IEntityOptionsBuilder entityOptions, + bool useSelectTopOptimization, + IReadOnlyList mappings + ); + + public IQueryable Create(DbContext dbContext, IEnumerable values, Action>? configure) + where TSource : notnull + { + var simpleTypeQueryable = getSimpleTypeQueryable(dbContext, values); + + if (simpleTypeQueryable != null) + { + return simpleTypeQueryable; + } + + var mappings = EntityPropertyMapping.GetMappings(); + var deferredValues = new DeferredEntityValues(_serializer, values, mappings); + var useSelectTopOptimization = UseSelectTopOptimization(deferredValues); + var sql = getSql(mappings, configure, useSelectTopOptimization); + var sqlParameters = GetSqlParameters(deferredValues); + + var source = dbContext + .Set() + .FromSqlRaw(sql, sqlParameters); + + var projected = projectQueryable(source, mappings); + + return projected; + + string getSql(IReadOnlyList mappings, Action>? configure, bool useSelectTopOptimization) + { + IEntityOptionsBuilder entityOptions; + + if (configure != null) + { + var entityOptionsHelper = new EntityOptionsBuilder(); + configure?.Invoke(entityOptionsHelper); + entityOptions = entityOptionsHelper; + } + else + { + entityOptions = new EntityOptionsBuilder(); + } + + var cacheKeyProperties = new + { + Options = entityOptions, + UseSelectTopOptimization = useSelectTopOptimization + }; + + var cacheKey = GetCacheKey(cacheKeyProperties); + + if (SqlCache.TryGetValue(cacheKey, out string? sqlFromCache)) + { + return sqlFromCache; + } + + var sql = GetSqlForComplexTypes(entityOptions, useSelectTopOptimization, mappings); + + SqlCache.TryAdd(cacheKey, sql); + + return sql; + } + + static IQueryable projectQueryable(IQueryable source, IReadOnlyList mappings) + { + Type sourceType = typeof(TSource); + + var queryable = getFromCache(sourceType, source); + if (queryable != null) + { + return queryable; + } + + Expression body; + var parameterExpression = Expression.Parameter(typeof(QueryableValuesEntity), "i"); + + var useConstructor = !mappings.All(i => i.Source.CanWrite); + + // Mainly for anonymous types. + if (useConstructor) + { + var constructor = sourceType.GetConstructors().FirstOrDefault(); + + if (constructor == null) + { + throw new InvalidOperationException($"Cannot find a suitable constructor in {sourceType.FullName}."); + } + + var arguments = new Expression[mappings.Count]; + var members = new MemberInfo[mappings.Count]; + + for (int i = 0; i < mappings.Count; i++) + { + var mapping = mappings[i]; + + arguments[i] = getTargetPropertyExpression(parameterExpression, mapping); + + var methodInfo = mapping.Source.GetGetMethod(true); + + if (methodInfo == null) + { + throw new InvalidOperationException($"Property {mapping.Source.Name} must have a Get accessor."); + } + + members[i] = methodInfo; + } + + body = Expression.New(constructor, arguments, members); + } + else + { + var bindings = new MemberBinding[mappings.Count]; + + for (int i = 0; i < mappings.Count; i++) + { + var mapping = mappings[i]; + + var methodInfo = mapping.Source.GetSetMethod(); + + if (methodInfo == null) + { + throw new InvalidOperationException($"Property {mapping.Source.Name} must have a Set accessor."); + } + + bindings[i] = Expression.Bind( + methodInfo, + getTargetPropertyExpression(parameterExpression, mapping) + ); + } + + var newExpression = Expression.New(sourceType); + body = Expression.MemberInit(newExpression, bindings); + } + + var bodyParameteters = new[] + { + parameterExpression + }; + + var selector = Expression.Lambda>(body, bodyParameteters); + + SelectorExpressionCache.TryAdd(sourceType, selector); + + queryable = Queryable.Select(source, selector); + + return queryable; + + #region Helpers + + static Expression getTargetPropertyExpression(ParameterExpression parameterExpression, EntityPropertyMapping mapping) + { + var propertyExpression = Expression.Property(parameterExpression, mapping.Target.Name); + + if (mapping.Source.PropertyType == mapping.Target.PropertyType) + { + return propertyExpression; + } + else + { + return Expression.Convert(propertyExpression, mapping.Source.PropertyType); + } + } + + static IQueryable? getFromCache(Type sourceType, IQueryable source) + { + if (SelectorExpressionCache.TryGetValue(sourceType, out object? selectorFromCache)) + { + var selector = (Expression>)selectorFromCache; + var queryable = Queryable.Select(source, selector); + return queryable; + } + else + { + return null; + } + } + + #endregion + } + + IQueryable? getSimpleTypeQueryable(DbContext dbContext, IEnumerable values) + { + if (EntityPropertyMapping.IsSimpleType(typeof(TSource))) + { + if (values is IEnumerable byteValues) + { + return (IQueryable)Create(dbContext, byteValues); + } + else if (values is IEnumerable int16Values) + { + return (IQueryable)Create(dbContext, int16Values); + } + else if (values is IEnumerable int32Values) + { + return (IQueryable)Create(dbContext, int32Values); + } + else if (values is IEnumerable int64Values) + { + return (IQueryable)Create(dbContext, int64Values); + } + else if (values is IEnumerable decimalValues) + { + return (IQueryable)Create(dbContext, decimalValues); + } + else if (values is IEnumerable singleValues) + { + return (IQueryable)Create(dbContext, singleValues); + } + else if (values is IEnumerable doubleValues) + { + return (IQueryable)Create(dbContext, doubleValues); + } + else if (values is IEnumerable dateTimeValues) + { + return (IQueryable)Create(dbContext, dateTimeValues); + } + else if (values is IEnumerable dateTimeOffsetValues) + { + return (IQueryable)Create(dbContext, dateTimeOffsetValues); + } + else if (values is IEnumerable guidValues) + { + return (IQueryable)Create(dbContext, guidValues); + } + else if (values is IEnumerable charValues) + { + return (IQueryable)Create(dbContext, charValues); + } + else if (values is IEnumerable stringValues) + { + return (IQueryable)Create(dbContext, stringValues); + } + else + { + throw new NotImplementedException(typeof(TSource).FullName); + } + } + else + { + return null; + } + } + } + } +} +#endif \ No newline at end of file diff --git a/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs b/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs new file mode 100644 index 0000000..811c944 --- /dev/null +++ b/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs @@ -0,0 +1,40 @@ +#if EFCORE +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace BlazarTech.QueryableValues.SqlServer +{ + internal sealed class QueryableFactoryFactory + { + private readonly IServiceProvider _serviceProvider; + private readonly QueryableValuesSqlServerOptions _options; + + public QueryableFactoryFactory(IServiceProvider serviceProvider, ExtensionOptions extensionOptions) + { + _serviceProvider = serviceProvider; + _options = extensionOptions.Options; + } + + public IQueryableFactory Create(DbContext dbContext) + { + var useJson = _options.WithSerializationOption switch + { + SqlServerSerialization.Auto => JsonSupportConnectionInterceptor.HasJsonSupport(dbContext).GetValueOrDefault(), + SqlServerSerialization.UseJson => true, + SqlServerSerialization.UseXml => false, + _ => throw new NotImplementedException(), + }; + + if (useJson) + { + return _serviceProvider.GetRequiredService(); + } + else + { + return _serviceProvider.GetRequiredService(); + } + } + } +} +#endif diff --git a/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs b/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs index 654f432..36e85cd 100644 --- a/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs +++ b/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs @@ -2,112 +2,30 @@ using BlazarTech.QueryableValues.Builders; using BlazarTech.QueryableValues.Serializers; using Microsoft.Data.SqlClient; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.ObjectPool; +using Microsoft.EntityFrameworkCore.Infrastructure; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; namespace BlazarTech.QueryableValues.SqlServer { - internal sealed class XmlQueryableFactory : IQueryableFactory + internal sealed class XmlQueryableFactory : QueryableFactory { - private const string SqlSelect = "SELECT"; - private const string SqlSelectTop = "SELECT TOP({1})"; - - private static readonly ConcurrentDictionary SqlCache = new(); - private static readonly ConcurrentDictionary SelectorExpressionCache = new(); - - private static readonly DefaultObjectPool StringBuilderPool = new DefaultObjectPool( - new StringBuilderPooledObjectPolicy - { - InitialCapacity = 1024, - MaximumRetainedCapacity = 16384 - }); - - private readonly IXmlSerializer _xmlSerializer; - private readonly QueryableValuesSqlServerOptions _options; - - public XmlQueryableFactory(IXmlSerializer xmlSerializer, QueryableValuesSqlServerOptions options) - { - if (xmlSerializer is null) - { - throw new ArgumentNullException(nameof(xmlSerializer)); - } - - if (options is null) - { - throw new ArgumentNullException(nameof(options)); - } - - _xmlSerializer = xmlSerializer; - _options = options; - } - - /// - /// Used to optimize the generated SQL by providing a TOP(n) on the SELECT statement. - /// In my tests, I observed improved memory grant estimation by SQL Server's query engine. - /// - private bool UseSelectTopOptimization(DeferredValues deferredValues) - where T : notnull + public XmlQueryableFactory(IXmlSerializer serializer, IDbContextOptions dbContextOptions) + : base(serializer, dbContextOptions) { -#if EFCORE3 - // In my EF Core 3 tests, it seems that on the first execution of the query, - // it is caching the values from the parameters provided to the FromSqlRaw method. - // This imposes a problem when trying to optimize the SQL using the HasCount property in this class. - // It is critical to know the exact number of elements behind "values" at execution time, - // this is because the number of items behind "values" can change between executions of the query, - // therefore, this optimization cannot be done in a reliable way under EF Core 3. - // - // Under EF Core 5 and 6 this is not an issue. The parameters are always evaluated on each execution. - return false; -#else - return - _options.WithUseSelectTopOptimization && - deferredValues.HasCount; -#endif } - private SqlParameter[] GetSqlParameters(DeferredValues deferredValues) - where T : notnull + protected override SqlParameter GetValuesParameter() { - SqlParameter[] sqlParameters; - - // Missing parameter names are auto-generated (p0, p1, etc.) by FromSqlRaw based on its position in the array. - var xmlParameter = new SqlParameter(null, SqlDbType.Xml) - { - // DeferredValues allows us to defer the enumeration of values until the query is materialized. - Value = _options.WithUseDeferredEnumeration ? deferredValues : deferredValues.ToString(null) - }; - - if (UseSelectTopOptimization(deferredValues)) - { - // bigint to avoid implicit casting by the TOP operation (observed in the execution plan). - var countParameter = new SqlParameter(null, SqlDbType.BigInt) - { - Value = _options.WithUseDeferredEnumeration ? deferredValues : deferredValues.ToInt64(null) - }; - - sqlParameters = new[] { xmlParameter, countParameter }; - } - else - { - sqlParameters = new[] { xmlParameter }; - } - - return sqlParameters; + return new SqlParameter(null, SqlDbType.Xml); } private string GetSqlForSimpleTypes(string xmlType, string sqlType, DeferredValues deferredValues, (int Precision, int Scale)? precisionScale = null) where T : notnull { var useSelectTopOptimization = UseSelectTopOptimization(deferredValues); - var cacheKey = new + var cacheKeyProperties = new { XmlType = xmlType, SqlType = sqlType, @@ -115,6 +33,8 @@ private string GetSqlForSimpleTypes(string xmlType, string sqlType, DeferredV PrecisionScale = precisionScale }; + var cacheKey = GetCacheKey(cacheKeyProperties); + if (SqlCache.TryGetValue(cacheKey, out string? sql)) { return sql; @@ -124,7 +44,7 @@ private string GetSqlForSimpleTypes(string xmlType, string sqlType, DeferredV var sqlTypeArguments = precisionScale.HasValue ? $"({precisionScale.Value.Precision},{precisionScale.Value.Scale})" : null; sql = - $"{sqlPrefix} I.value('. cast as xs:{xmlType}?', '{sqlType}{sqlTypeArguments}') AS V " + + $"{sqlPrefix} I.value('. cast as xs:{xmlType}?', '{sqlType}{sqlTypeArguments}') [V] " + "FROM {0}.nodes('/R/V') N(I)"; SqlCache.TryAdd(cacheKey, sql); @@ -132,459 +52,170 @@ private string GetSqlForSimpleTypes(string xmlType, string sqlType, DeferredV return sql; } - private IQueryable Create(DbContext dbContext, string sql, DeferredValues deferredValues) - where TValue : notnull - { - var sqlParameters = GetSqlParameters(deferredValues); - - var queryableValues = dbContext - .Set>() - .FromSqlRaw(sql, sqlParameters); - - return queryableValues.Select(i => i.V); - } - - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesByte(DeferredValues deferredValues) { - var deferredValues = new DeferredByteValues(_xmlSerializer, values); - var sql = GetSqlForSimpleTypes("unsignedByte", "tinyint", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("unsignedByte", "tinyint", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesInt16(DeferredValues deferredValues) { - var deferredValues = new DeferredInt16Values(_xmlSerializer, values); - var sql = GetSqlForSimpleTypes("short", "smallint", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("short", "smallint", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesInt32(DeferredValues deferredValues) { - var deferredValues = new DeferredInt32Values(_xmlSerializer, values); - var sql = GetSqlForSimpleTypes("integer", "int", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("integer", "int", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesInt64(DeferredValues deferredValues) { - var deferredValues = new DeferredInt64Values(_xmlSerializer, values); - var sql = GetSqlForSimpleTypes("integer", "bigint", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("integer", "bigint", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values, int numberOfDecimals = 4) + protected override string GetSqlForSimpleTypesDecimal(DeferredValues deferredValues, (int Precision, int Scale) precisionScale) { - var deferredValues = new DeferredDecimalValues(_xmlSerializer, values); - var precisionScale = (38, numberOfDecimals); - var sql = GetSqlForSimpleTypes("decimal", "decimal", deferredValues, precisionScale: precisionScale); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("decimal", "decimal", deferredValues, precisionScale: precisionScale); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesSingle(DeferredValues deferredValues) { - var deferredValues = new DeferredSingleValues(_xmlSerializer, values); - var sql = GetSqlForSimpleTypes("float", "real", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("float", "real", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesDouble(DeferredValues deferredValues) { - var deferredValues = new DeferredDoubleValues(_xmlSerializer, values); - var sql = GetSqlForSimpleTypes("double", "float", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("double", "float", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesDateTime(DeferredValues deferredValues) { - var deferredValues = new DeferredDateTimeValues(_xmlSerializer, values); - var sql = GetSqlForSimpleTypes("dateTime", "datetime2", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("dateTime", "datetime2", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesDateTimeOffset(DeferredValues deferredValues) { - var deferredValues = new DeferredDateTimeOffsetValues(_xmlSerializer, values); - var sql = GetSqlForSimpleTypes("dateTime", "datetimeoffset", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("dateTime", "datetimeoffset", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values, bool isUnicode = false) + protected override string GetSqlForSimpleTypesChar(DeferredValues deferredValues, bool isUnicode) { - string sql; - var deferredValues = new DeferredCharValues(_xmlSerializer, values); - - if (isUnicode) - { - sql = GetSqlForSimpleTypes("string", "nvarchar(1)", deferredValues); - } - else - { - sql = GetSqlForSimpleTypes("string", "varchar(1)", deferredValues); - } - - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("string", isUnicode ? "nvarchar(1)" : "varchar(1)", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values, bool isUnicode = false) + protected override string GetSqlForSimpleTypesString(DeferredValues deferredValues, bool isUnicode) { - string sql; - var deferredValues = new DeferredStringValues(_xmlSerializer, values); - - if (isUnicode) - { - sql = GetSqlForSimpleTypes("string", "nvarchar(max)", deferredValues); - } - else - { - sql = GetSqlForSimpleTypes("string", "varchar(max)", deferredValues); - } - - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("string", isUnicode ? "nvarchar(max)" : "varchar(max)", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesGuid(DeferredValues deferredValues) { - var deferredValues = new DeferredGuidValues(_xmlSerializer, values); - var sql = GetSqlForSimpleTypes("string", "uniqueidentifier", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("string", "uniqueidentifier", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values, Action>? configure) where TSource : notnull + protected override string GetSqlForComplexTypes(IEntityOptionsBuilder entityOptions, bool useSelectTopOptimization, IReadOnlyList mappings) { - var simpleTypeQueryable = getSimpleTypeQueryable(dbContext, values); - - if (simpleTypeQueryable != null) - { - return simpleTypeQueryable; - } - - var mappings = EntityPropertyMapping.GetMappings(); - var deferredValues = new DeferredEntityValues(_xmlSerializer, values, mappings); - var useSelectTopOptimization = UseSelectTopOptimization(deferredValues); - var sql = getSql(mappings, configure, useSelectTopOptimization); - var sqlParameters = GetSqlParameters(deferredValues); + var sb = StringBuilderPool.Get(); - var source = dbContext - .Set() - .FromSqlRaw(sql, sqlParameters); - - var projected = projectQueryable(source, mappings); - - return projected; - - static string getSql(IReadOnlyList mappings, Action>? configure, bool useSelectTopOptimization) + try { - IEntityOptionsBuilder entityOptions; - - if (configure != null) + if (useSelectTopOptimization) { - var entityOptionsHelper = new EntityOptionsBuilder(); - configure?.Invoke(entityOptionsHelper); - entityOptions = entityOptionsHelper; + sb.Append(SqlSelectTop); } else { - entityOptions = new EntityOptionsBuilder(); + sb.Append(SqlSelect); } - var cacheKey = new - { - Options = entityOptions, - UseSelectTopOptimization = useSelectTopOptimization - }; + sb.AppendLine(); - if (SqlCache.TryGetValue(cacheKey, out string? sqlFromCache)) + for (int i = 0; i < mappings.Count; i++) { - return sqlFromCache; + var mapping = mappings[i]; + var propertyOptions = entityOptions.GetPropertyOptions(mapping.Source); + + if (i > 0) + { + sb.Append(',').AppendLine(); + } + + var targetName = mapping.Target.Name; + + sb.Append("\tI.value('@").Append(targetName).Append("[1] cast as "); + + switch (mapping.TypeName) + { + case EntityPropertyTypeName.Boolean: + sb.Append("xs:boolean?', 'bit'"); + break; + case EntityPropertyTypeName.Byte: + sb.Append("xs:unsignedByte?', 'tinyint'"); + break; + case EntityPropertyTypeName.Int16: + sb.Append("xs:short?', 'smallint'"); + break; + case EntityPropertyTypeName.Int32: + sb.Append("xs:integer?', 'int'"); + break; + case EntityPropertyTypeName.Int64: + sb.Append("xs:integer?', 'bigint'"); + break; + case EntityPropertyTypeName.Decimal: + { + var numberOfDecimals = propertyOptions?.NumberOfDecimals ?? entityOptions.DefaultForNumberOfDecimals; + sb.Append("xs:decimal?', 'decimal(38, ").Append(numberOfDecimals).Append(")'"); + } + break; + case EntityPropertyTypeName.Single: + sb.Append("xs:float?', 'real'"); + break; + case EntityPropertyTypeName.Double: + sb.Append("xs:double?', 'float'"); + break; + case EntityPropertyTypeName.DateTime: + sb.Append("xs:dateTime?', 'datetime2'"); + break; + case EntityPropertyTypeName.DateTimeOffset: + sb.Append("xs:dateTime?', 'datetimeoffset'"); + break; + case EntityPropertyTypeName.Guid: + sb.Append("xs:string?', 'uniqueidentifier'"); + break; + case EntityPropertyTypeName.Char: + if ((propertyOptions?.IsUnicode ?? entityOptions.DefaultForIsUnicode) == true) + { + sb.Append("xs:string?', 'nvarchar(1)'"); + } + else + { + sb.Append("xs:string?', 'varchar(1)'"); + } + break; + case EntityPropertyTypeName.String: + if ((propertyOptions?.IsUnicode ?? entityOptions.DefaultForIsUnicode) == true) + { + sb.Append("xs:string?', 'nvarchar(max)'"); + } + else + { + sb.Append("xs:string?', 'varchar(max)'"); + } + break; + default: + throw new NotImplementedException(mapping.TypeName.ToString()); + } + + sb.Append(") AS [").Append(targetName).Append(']'); } - var sb = StringBuilderPool.Get(); - - try - { - if (useSelectTopOptimization) - { - sb.Append(SqlSelectTop); - } - else - { - sb.Append(SqlSelect); - } - - sb.AppendLine(); - - for (int i = 0; i < mappings.Count; i++) - { - var mapping = mappings[i]; - var propertyOptions = entityOptions.GetPropertyOptions(mapping.Source); - - if (i > 0) - { - sb.Append(',').AppendLine(); - } - - var targetName = mapping.Target.Name; - - sb.Append("\tI.value('@").Append(targetName).Append("[1] cast as "); - - switch (mapping.TypeName) - { - case EntityPropertyTypeName.Boolean: - sb.Append("xs:boolean?', 'bit'"); - break; - case EntityPropertyTypeName.Byte: - sb.Append("xs:unsignedByte?', 'tinyint'"); - break; - case EntityPropertyTypeName.Int16: - sb.Append("xs:short?', 'smallint'"); - break; - case EntityPropertyTypeName.Int32: - sb.Append("xs:integer?', 'int'"); - break; - case EntityPropertyTypeName.Int64: - sb.Append("xs:integer?', 'bigint'"); - break; - case EntityPropertyTypeName.Decimal: - { - var numberOfDecimals = propertyOptions?.NumberOfDecimals ?? entityOptions.DefaultForNumberOfDecimals; - sb.Append("xs:decimal?', 'decimal(38, ").Append(numberOfDecimals).Append(")'"); - } - break; - case EntityPropertyTypeName.Single: - sb.Append("xs:float?', 'real'"); - break; - case EntityPropertyTypeName.Double: - sb.Append("xs:double?', 'float'"); - break; - case EntityPropertyTypeName.DateTime: - sb.Append("xs:dateTime?', 'datetime2'"); - break; - case EntityPropertyTypeName.DateTimeOffset: - sb.Append("xs:dateTime?', 'datetimeoffset'"); - break; - case EntityPropertyTypeName.Guid: - sb.Append("xs:string?', 'uniqueidentifier'"); - break; - case EntityPropertyTypeName.Char: - if ((propertyOptions?.IsUnicode ?? entityOptions.DefaultForIsUnicode) == true) - { - sb.Append("xs:string?', 'nvarchar(1)'"); - } - else - { - sb.Append("xs:string?', 'varchar(1)'"); - } - break; - case EntityPropertyTypeName.String: - if ((propertyOptions?.IsUnicode ?? entityOptions.DefaultForIsUnicode) == true) - { - sb.Append("xs:string?', 'nvarchar(max)'"); - } - else - { - sb.Append("xs:string?', 'varchar(max)'"); - } - break; - default: - throw new NotImplementedException(mapping.TypeName.ToString()); - } - - sb.Append(") AS [").Append(targetName).Append(']'); - } - - sb.AppendLine(); - sb.Append("FROM {0}.nodes('/R/V') N(I)"); - - var sql = sb.ToString(); - - SqlCache.TryAdd(cacheKey, sql); + sb.AppendLine(); + sb.Append("FROM {0}.nodes('/R/V') N(I)"); - return sql; - } - finally - { - StringBuilderPool.Return(sb); - } + return sb.ToString(); } - - static IQueryable projectQueryable(IQueryable source, IReadOnlyList mappings) + finally { - Type sourceType = typeof(TSource); - - var queryable = getFromCache(sourceType, source); - if (queryable != null) - { - return queryable; - } - - Expression body; - var parameterExpression = Expression.Parameter(typeof(QueryableValuesEntity), "i"); - - var useConstructor = !mappings.All(i => i.Source.CanWrite); - - // Mainly for anonymous types. - if (useConstructor) - { - var constructor = sourceType.GetConstructors().FirstOrDefault(); - - if (constructor == null) - { - throw new InvalidOperationException($"Cannot find a suitable constructor in {sourceType.FullName}."); - } - - var arguments = new Expression[mappings.Count]; - var members = new MemberInfo[mappings.Count]; - - for (int i = 0; i < mappings.Count; i++) - { - var mapping = mappings[i]; - - arguments[i] = getTargetPropertyExpression(parameterExpression, mapping); - - var methodInfo = mapping.Source.GetGetMethod(true); - - if (methodInfo == null) - { - throw new InvalidOperationException($"Property {mapping.Source.Name} must have a Get accessor."); - } - - members[i] = methodInfo; - } - - body = Expression.New(constructor, arguments, members); - } - else - { - var bindings = new MemberBinding[mappings.Count]; - - for (int i = 0; i < mappings.Count; i++) - { - var mapping = mappings[i]; - - var methodInfo = mapping.Source.GetSetMethod(); - - if (methodInfo == null) - { - throw new InvalidOperationException($"Property {mapping.Source.Name} must have a Set accessor."); - } - - bindings[i] = Expression.Bind( - methodInfo, - getTargetPropertyExpression(parameterExpression, mapping) - ); - } - - var newExpression = Expression.New(sourceType); - body = Expression.MemberInit(newExpression, bindings); - } - - var bodyParameteters = new[] - { - parameterExpression - }; - - var selector = Expression.Lambda>(body, bodyParameteters); - - SelectorExpressionCache.TryAdd(sourceType, selector); - - queryable = Queryable.Select(source, selector); - - return queryable; - - #region Helpers - - static Expression getTargetPropertyExpression(ParameterExpression parameterExpression, EntityPropertyMapping mapping) - { - var propertyExpression = Expression.Property(parameterExpression, mapping.Target.Name); - - if (mapping.Source.PropertyType == mapping.Target.PropertyType) - { - return propertyExpression; - } - else - { - return Expression.Convert(propertyExpression, mapping.Source.PropertyType); - } - } - - static IQueryable? getFromCache(Type sourceType, IQueryable source) - { - if (SelectorExpressionCache.TryGetValue(sourceType, out object? selectorFromCache)) - { - var selector = (Expression>)selectorFromCache; - var queryable = Queryable.Select(source, selector); - return queryable; - } - else - { - return null; - } - } - - #endregion - } - - IQueryable? getSimpleTypeQueryable(DbContext dbContext, IEnumerable values) - { - if (EntityPropertyMapping.IsSimpleType(typeof(TSource))) - { - if (values is IEnumerable byteValues) - { - return (IQueryable)Create(dbContext, byteValues); - } - else if (values is IEnumerable int16Values) - { - return (IQueryable)Create(dbContext, int16Values); - } - else if (values is IEnumerable int32Values) - { - return (IQueryable)Create(dbContext, int32Values); - } - else if (values is IEnumerable int64Values) - { - return (IQueryable)Create(dbContext, int64Values); - } - else if (values is IEnumerable decimalValues) - { - return (IQueryable)Create(dbContext, decimalValues); - } - else if (values is IEnumerable singleValues) - { - return (IQueryable)Create(dbContext, singleValues); - } - else if (values is IEnumerable doubleValues) - { - return (IQueryable)Create(dbContext, doubleValues); - } - else if (values is IEnumerable dateTimeValues) - { - return (IQueryable)Create(dbContext, dateTimeValues); - } - else if (values is IEnumerable dateTimeOffsetValues) - { - return (IQueryable)Create(dbContext, dateTimeOffsetValues); - } - else if (values is IEnumerable guidValues) - { - return (IQueryable)Create(dbContext, guidValues); - } - else if (values is IEnumerable charValues) - { - return (IQueryable)Create(dbContext, charValues); - } - else if (values is IEnumerable stringValues) - { - return (IQueryable)Create(dbContext, stringValues); - } - else - { - throw new NotImplementedException(typeof(TSource).FullName); - } - } - else - { - return null; - } + StringBuilderPool.Return(sb); } } } diff --git a/src/QueryableValues.SqlServer/SqlServerSerialization.cs b/src/QueryableValues.SqlServer/SqlServerSerialization.cs new file mode 100644 index 0000000..fbbb0c6 --- /dev/null +++ b/src/QueryableValues.SqlServer/SqlServerSerialization.cs @@ -0,0 +1,43 @@ +#if EFCORE +namespace BlazarTech.QueryableValues +{ + /// + /// Specifies the serialization format to be used when sending data to SQL Server. + /// + public enum SqlServerSerialization + { + /// + /// Automatically chooses between JSON and XML serialization based on server and database compatibility. + /// + /// + /// + /// This option may cause an additional round-trip to the database to check for JSON compatibility, + /// but only once per unique connection string for the life of the process. If JSON serialization is not supported, XML is used instead. + /// + /// + /// Caveat: If the very first query sent to the server is a QueryableValues enabled one, it will use XML and then switch to JSON (if supported) afterward. + /// + /// + Auto = 0, + + /// + /// Uses the JSON serializer for better performance. + /// + /// + /// + /// Using JSON is faster than XML, but requires SQL Server 2016 or newer and a database compatibility level of 130 or higher.
+ /// More info: . + ///
+ /// + /// WARNING: If JSON serialization is not supported, an error will occur at runtime. + /// + ///
+ UseJson = 1, + + /// + /// Uses the XML serializer, which is compatible with all supported versions of SQL Server to date. + /// + UseXml = 2 + } +} +#endif \ No newline at end of file diff --git a/src/SharedProjectProperties.xml b/src/SharedProjectProperties.xml index 9dac96c..b539465 100644 --- a/src/SharedProjectProperties.xml +++ b/src/SharedProjectProperties.xml @@ -1,62 +1,64 @@  - - BlazarTech.QueryableValues - BlazarTech.QueryableValues.SqlServer - true - https://github.com/yv989c/BlazarTech.QueryableValues - Carlos Villegas - BlazarTech.QueryableValues - BlazarTech.QueryableValues.SqlServer - Allows you to efficiently compose an IEnumerable<T> in your Entity Framework Core queries when using the SQL Server Database Provider. This is accomplished by using the AsQueryableValues extension method available on the DbContext class. Everything is evaluated on the server with a single round trip, in a way that preserves the query's execution plan, even when the values behind the IEnumerable<T> are changed on subsequent executions. - MIT - https://github.com/yv989c/BlazarTech.QueryableValues - Entity EF EFCore EntityFramework EntityFrameworkCore entity-framework-core Data ORM SQLServer sql-server IQueryable IEnumerable Queryable Values MemoryJoin BulkInsertTempTableAsync WhereBulkContains Extension Extensions Memory Join Contains Performance LINQ - icon.png - README.md - $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - enable - + + BlazarTech.QueryableValues + BlazarTech.QueryableValues.SqlServer + true + https://github.com/yv989c/BlazarTech.QueryableValues + Carlos Villegas + BlazarTech.QueryableValues + BlazarTech.QueryableValues.SqlServer + Allows you to efficiently compose an IEnumerable<T> in your Entity Framework Core queries when using the SQL Server Database Provider. This is accomplished by using the AsQueryableValues extension method available on the DbContext class. Everything is evaluated on the server with a single round trip, in a way that preserves the query's execution plan, even when the values behind the IEnumerable<T> are changed on subsequent executions. + MIT + https://github.com/yv989c/BlazarTech.QueryableValues + Entity EF EFCore EntityFramework EntityFrameworkCore entity-framework-core Data ORM SQLServer sql-server IQueryable IEnumerable Queryable Values MemoryJoin BulkInsertTempTableAsync WhereBulkContains Extension Extensions Memory Join Contains Performance LINQ + icon.png + README.md + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + enable + - - - + + + + + - - - True - \ - - - True - \ - - - True - \ - - + + + True + \ + + + True + \ + + + True + \ + + - - true - + + true + - - - - Builders\%(RecursiveDir)\%(FileName)%(Extension) - - - Serializers\%(RecursiveDir)\%(FileName)%(Extension) - - - SqlServer\%(RecursiveDir)\%(FileName)%(Extension) - - + + + + Builders\%(RecursiveDir)\%(FileName)%(Extension) + + + Serializers\%(RecursiveDir)\%(FileName)%(Extension) + + + SqlServer\%(RecursiveDir)\%(FileName)%(Extension) + + - - - - - - + + + + + + \ No newline at end of file diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/ComplexTypeTests.cs b/tests/QueryableValues.SqlServer.Tests/Integration/ComplexTypeTests.cs index 0b7ac33..93552ac 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/ComplexTypeTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/ComplexTypeTests.cs @@ -9,9 +9,9 @@ namespace BlazarTech.QueryableValues.SqlServer.Tests.Integration { [Collection("DbContext")] - public class ComplexTypeTests + public abstract class ComplexTypeTests { - private readonly IMyDbContext _db; + protected readonly IMyDbContext _db; public class TestType { @@ -796,5 +796,23 @@ public async Task MustMatchCount() Assert.Equal(expectedItemCount, actualItemCount); } } + + [Collection("DbContext")] + public class JsonComplexTypeTests : ComplexTypeTests + { + public JsonComplexTypeTests(DbContextFixture contextFixture) : base(contextFixture) + { + _db.Options.Serialization(SqlServerSerialization.UseJson); + } + } + + [Collection("DbContext")] + public class XmlComplexTypeTests : ComplexTypeTests + { + public XmlComplexTypeTests(DbContextFixture contextFixture) : base(contextFixture) + { + _db.Options.Serialization(SqlServerSerialization.UseXml); + } + } } #endif \ No newline at end of file diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs b/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs index d671ade..02a1a8b 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs @@ -1,5 +1,6 @@ #if TESTS && TEST_ALL using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; @@ -54,19 +55,23 @@ public void OnlyWorksOnDbContext() } #if !EFCORE3 - [Fact] - public async Task MustControlSelectTopOptimization() + [Theory] + [InlineData(SqlServerSerialization.UseJson)] + [InlineData(SqlServerSerialization.UseXml)] + public async Task MustControlSelectTopOptimization(SqlServerSerialization serializationOption) { var services = new ServiceCollection(); services.AddDbContext(); - services.AddDbContext(); + services.AddDbContext(); using var serviceProvider = services.BuildServiceProvider(); var optimizedDb = serviceProvider.GetRequiredService(); + optimizedDb.Options.Serialization(serializationOption); Assert.True(await isOptimizationEnabledSimpleType(optimizedDb)); Assert.True(await isOptimizationEnabledComplexType(optimizedDb)); - var notOptimizedDb = serviceProvider.GetRequiredService(); + var notOptimizedDb = serviceProvider.GetRequiredService(); + notOptimizedDb.Options.Serialization(serializationOption); Assert.False(await isOptimizationEnabledComplexType(notOptimizedDb)); Assert.False(await isOptimizationEnabledSimpleType(notOptimizedDb)); @@ -78,7 +83,7 @@ async Task isOptimizationEnabledSimpleType(MyDbContextBase db) var result = await db.AsQueryableValues(values).ToListAsync(); Assert.Equal(values.Length, result.Count); var logEntry = logEntries.Single(i => i.Contains("RelationalEventId.CommandExecuted")); - return Regex.IsMatch(logEntry, @"SELECT TOP\(@\w+\)\s+I.value\("); + return Regex.IsMatch(logEntry, @"SELECT TOP\(@\w+\)\s"); } async Task isOptimizationEnabledComplexType(MyDbContextBase db) @@ -94,10 +99,147 @@ async Task isOptimizationEnabledComplexType(MyDbContextBase db) var result = await db.AsQueryableValues(values).ToListAsync(); Assert.Equal(values.Length, result.Count); var logEntry = logEntries.Single(i => i.Contains("RelationalEventId.CommandExecuted")); - return Regex.IsMatch(logEntry, @"SELECT TOP\(@\w+\)\s+I.value\("); + return Regex.IsMatch(logEntry, @"SELECT TOP\(@\w+\)\s"); + } + } + + [Theory] + [InlineData(SqlServerSerialization.UseJson)] + [InlineData(SqlServerSerialization.UseXml)] + public async Task MustControlSerializationFormat(SqlServerSerialization serializationOption) + { + var services = new ServiceCollection(); + services.AddDbContext(); + + using var serviceProvider = services.BuildServiceProvider(); + + var db = serviceProvider.GetRequiredService(); + db.Options.Serialization(serializationOption); + + Assert.True(await isRightSerializationFormatSimpleType(db)); + Assert.True(await isRightSerializationFormatComplexType(db)); + + async Task isRightSerializationFormatSimpleType(MyDbContextBase db) + { + var values = new[] { 1, 2, 3 }; + var logEntries = new List(); + db.LogEntryEmitted += logEntry => logEntries.Add(logEntry); + var result = await db.AsQueryableValues(values).ToListAsync(); + Assert.Equal(values.Length, result.Count); + var logEntry = logEntries.Single(i => i.Contains("RelationalEventId.CommandExecuted")); + return isRightFormat(logEntry); + } + + async Task isRightSerializationFormatComplexType(MyDbContextBase db) + { + var values = new[] + { + new { Id = 1 }, + new { Id = 2 }, + new { Id = 3 } + }; + var logEntries = new List(); + db.LogEntryEmitted += logEntry => logEntries.Add(logEntry); + var result = await db.AsQueryableValues(values).ToListAsync(); + Assert.Equal(values.Length, result.Count); + var logEntry = logEntries.Single(i => i.Contains("RelationalEventId.CommandExecuted")); + return isRightFormat(logEntry); + } + + bool isRightFormat(string logEntry) + { + switch (serializationOption) + { + case SqlServerSerialization.UseJson: + return logEntry.Contains("OPENJSON("); + case SqlServerSerialization.UseXml: + return logEntry.Contains(".nodes("); + default: + throw new NotImplementedException(); + } + } + } + + [Theory] + [InlineData(SqlServerSerialization.Auto)] + [InlineData(SqlServerSerialization.UseJson)] + [InlineData(SqlServerSerialization.UseXml)] + public async Task JsonSupportDetection(SqlServerSerialization serializationOption) + { + var services = new ServiceCollection(); + services.AddDbContext(); + + using var serviceProvider = services.BuildServiceProvider(); + + var db = serviceProvider.GetRequiredService(); + db.Options.Serialization(serializationOption); + + switch (serializationOption) + { + case SqlServerSerialization.Auto: + { + forceJsonDetection(); + Assert.Null(JsonSupportConnectionInterceptor.HasJsonSupport(db)); + await db.TestData.FirstAsync(); + Assert.True(JsonSupportConnectionInterceptor.HasJsonSupport(db)); + + forceJsonDetection(); + Assert.Null(JsonSupportConnectionInterceptor.HasJsonSupport(db)); + db.TestData.First(); + Assert.True(JsonSupportConnectionInterceptor.HasJsonSupport(db)); + } + break; + case SqlServerSerialization.UseJson: + case SqlServerSerialization.UseXml: + { + forceJsonDetection(); + Assert.Null(JsonSupportConnectionInterceptor.HasJsonSupport(db)); + db.TestData.First(); + Assert.Null(JsonSupportConnectionInterceptor.HasJsonSupport(db)); + } + break; + default: + throw new NotImplementedException(); + } + + void forceJsonDetection() + { + db.Database.SetConnectionString(db.Database.GetConnectionString() + $";Application Name={Guid.NewGuid()};"); } } #endif + + [Theory] + [InlineData(SqlServerSerialization.UseJson)] + [InlineData(SqlServerSerialization.UseXml)] + [InlineData(SqlServerSerialization.Auto)] + public void MustCreateQueryableFactory(SqlServerSerialization serializationOption) + { + var services = new ServiceCollection(); + services.AddDbContext(); + + using var serviceProvider = services.BuildServiceProvider(); + + var dbContext = serviceProvider.GetRequiredService(); + dbContext.Options.Serialization(serializationOption); + + var queryableFactory = dbContext.GetService().Create(dbContext); + + Assert.NotNull(queryableFactory); + + switch (serializationOption) + { + case SqlServerSerialization.UseJson: + case SqlServerSerialization.Auto: + Assert.IsType(queryableFactory); + break; + case SqlServerSerialization.UseXml: + Assert.IsType(queryableFactory); + break; + default: + throw new NotImplementedException(); + } + } } class NotADbContext : IQueryableValuesEnabledDbContext diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/MyDbContext.cs b/tests/QueryableValues.SqlServer.Tests/Integration/MyDbContext.cs index 82fcc3b..0dc5e2b 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/MyDbContext.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/MyDbContext.cs @@ -1,5 +1,6 @@ #if TESTS using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; namespace BlazarTech.QueryableValues.SqlServer.Tests.Integration { @@ -18,11 +19,17 @@ internal static class DatabaseName public class MyDbContext : MyDbContextBase, IMyDbContext { - public MyDbContext() : base(DatabaseName.Name) { } + public QueryableValuesSqlServerOptions Options { get; } + + public MyDbContext(bool useSelectTopOptimization = true) : base(DatabaseName.Name, useSelectTopOptimization: useSelectTopOptimization) + { + Options = this.GetService().FindExtension()!.Options; + } } public interface IMyDbContext : IQueryableValuesEnabledDbContext { + QueryableValuesSqlServerOptions Options { get; } DbSet TestData { get; set; } } @@ -31,9 +38,9 @@ public class NotConfiguredDbContext : MyDbContextBase public NotConfiguredDbContext() : base(DatabaseName.Name, useQueryableValues: false) { } } - public class NotOptimizedDbContext : MyDbContextBase + public class NotOptimizedMyDbContext : MyDbContext { - public NotOptimizedDbContext() : base(DatabaseName.Name, useSelectTopOptimization: false) { } + public NotOptimizedMyDbContext() : base(useSelectTopOptimization: false) { } } } #endif \ No newline at end of file diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs b/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs index cbca5a4..13064d5 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs @@ -8,10 +8,9 @@ namespace BlazarTech.QueryableValues.SqlServer.Tests.Integration { - [Collection("DbContext")] - public class SimpleTypeTests + public abstract class SimpleTypeTests { - private readonly IMyDbContext _db; + protected readonly IMyDbContext _db; public SimpleTypeTests(DbContextFixture contextFixture) { @@ -209,16 +208,36 @@ public async Task MustMatchSequenceOfChar() { var values = new[] { 'A', 'a', 'ᴭ', ' ', '\n', '\0', '\u0001' }; + if (_db.Options.WithSerializationOption == SqlServerSerialization.UseXml) { - var expected = new[] { 'A', 'a', '?', ' ', '\n', '?', '?' }; - var actual = await _db.AsQueryableValues(values, isUnicode: false).ToListAsync(); - Assert.Equal(expected, actual); + { + var expected = new[] { 'A', 'a', '?', ' ', '\n', '?', '?' }; + var actual = await _db.AsQueryableValues(values, isUnicode: false).ToListAsync(); + Assert.Equal(expected, actual); + } + + { + var expected = new[] { 'A', 'a', 'ᴭ', ' ', '\n', '?', '?' }; + var actual = await _db.AsQueryableValues(values, isUnicode: true).ToListAsync(); + Assert.Equal(expected, actual); + } } + else if (_db.Options.WithSerializationOption == SqlServerSerialization.UseJson) + { + { + var expected = new[] { 'A', 'a', '?', ' ', '\n', '\0', '\u0001' }; + var actual = await _db.AsQueryableValues(values, isUnicode: false).ToListAsync(); + Assert.Equal(expected, actual); + } + { + var actual = await _db.AsQueryableValues(values, isUnicode: true).ToListAsync(); + Assert.Equal(values, actual); + } + } + else { - var expected = new[] { 'A', 'a', 'ᴭ', ' ', '\n', '?', '?' }; - var actual = await _db.AsQueryableValues(values, isUnicode: true).ToListAsync(); - Assert.Equal(expected, actual); + throw new NotImplementedException(); } { @@ -233,29 +252,55 @@ public async Task MustMatchSequenceOfString() { var values = new[] { "\0 ", "\u0001", "Test 1", "Test <2>", "Test &3", "😀", "ᴭ", "", " ", "\n", " \n", " \n ", "\r", "\r ", " Test\t1 ", "\U00010330" }; + if (_db.Options.WithSerializationOption == SqlServerSerialization.UseXml) { - var expected = new string[values.Length]; - values.CopyTo(expected, 0); - expected[0] = "? "; - expected[1] = "?"; - expected[5] = "??"; - expected[6] = "?"; - expected[15] = "??"; + { + var expected = new string[values.Length]; + values.CopyTo(expected, 0); + expected[0] = "? "; + expected[1] = "?"; + expected[5] = "??"; + expected[6] = "?"; + expected[15] = "??"; - var actual = await _db.AsQueryableValues(values, isUnicode: false).ToListAsync(); + var actual = await _db.AsQueryableValues(values, isUnicode: false).ToListAsync(); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } + + { + var expected = new string[values.Length]; + values.CopyTo(expected, 0); + expected[0] = "? "; + expected[1] = "?"; + var actual = await _db.AsQueryableValues(values, isUnicode: true).ToListAsync(); + + Assert.Equal(expected, actual); + } + } + else if (_db.Options.WithSerializationOption == SqlServerSerialization.UseJson) { - var expected = new string[values.Length]; - values.CopyTo(expected, 0); - expected[0] = "? "; - expected[1] = "?"; + { + var expected = new string[values.Length]; + values.CopyTo(expected, 0); + expected[5] = "??"; + expected[6] = "?"; + expected[15] = "??"; - var actual = await _db.AsQueryableValues(values, isUnicode: true).ToListAsync(); + var actual = await _db.AsQueryableValues(values, isUnicode: false).ToListAsync(); - Assert.Equal(expected, actual); + Assert.Equal(expected, actual); + } + + { + var actual = await _db.AsQueryableValues(values, isUnicode: true).ToListAsync(); + Assert.Equal(values, actual); + } + } + else + { + throw new NotImplementedException(); } } @@ -684,5 +729,23 @@ async Task AssertCount(Func getValue) } } } + + [Collection("DbContext")] + public class JsonSimpleTypeTests : SimpleTypeTests + { + public JsonSimpleTypeTests(DbContextFixture contextFixture) : base(contextFixture) + { + _db.Options.Serialization(SqlServerSerialization.UseJson); + } + } + + [Collection("DbContext")] + public class XmlSimpleTypeTests : SimpleTypeTests + { + public XmlSimpleTypeTests(DbContextFixture contextFixture) : base(contextFixture) + { + _db.Options.Serialization(SqlServerSerialization.UseXml); + } + } } #endif \ No newline at end of file