diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e101102da..b90b4b141 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -51,7 +51,7 @@ jobs: run: make check - name: Run unit tests - run: TARGET_FRAMEWORK=net9 make test-unit + run: TARGET_FRAMEWORK=net9.0 make test-unit integration-tests: needs: linting-and-basic-unit-tests @@ -60,17 +60,17 @@ jobs: matrix: include: - scylla-version: LTS-LATEST - target-framework: net9 + target-framework: net9.0 dotnet-version: '9.0.x' - scylla-version: LTS-PRIOR - target-framework: net8 + target-framework: net8.0 dotnet-version: '8.0.x' - - scylla-version: LATEST - target-framework: net7 - dotnet-version: '7.0.x' - scylla-version: PRIOR - target-framework: net6 - dotnet-version: '6.0.x' + target-framework: net8.0 + dotnet-version: '8.0.x' + - scylla-version: LATEST + target-framework: net9.0 + dotnet-version: '9.0.x' fail-fast: false env: diff --git a/.gitignore b/.gitignore index 20727719a..ae4c702db 100644 --- a/.gitignore +++ b/.gitignore @@ -88,4 +88,4 @@ AGENTS.md claude.log claude_history.json claude_config.json -CLAUDE.md \ No newline at end of file +CLAUDE.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 41381aac3..7fc674065 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -120,29 +120,26 @@ The command `dotnet build src/Cassandra.sln` should succeed. Otherwise, you need You can build specific projects against specific target frameworks on any platform like this: ```bash -dotnet build src/Cassandra/Cassandra.csproj -f netstandard2.0 -dotnet build src/Cassandra.Tests/Cassandra.Tests.csproj -f net8 -dotnet build src/Cassandra.IntegrationTests/Cassandra.IntegrationTests.csproj -f net8 +dotnet build src/Cassandra/Cassandra.csproj -f net8.0 +dotnet build src/Cassandra.Tests/Cassandra.Tests.csproj -f net8.0 +dotnet build src/Cassandra.IntegrationTests/Cassandra.IntegrationTests.csproj -f net8.0 ``` -Alternatively you can set the `BuildCoreOnly` environment variable which will cause the projects to support .NET Core / .NET Standard targets only (you can see the conditions on the `.csproj` files). - ### Running Unit Tests ```bash -dotnet test src/Cassandra.Tests/Cassandra.Tests.csproj -f net8 +dotnet test src/Cassandra.Tests/Cassandra.Tests.csproj -f net8.0 ``` -The target framework supported by the test projects is `net8` (by default). If you set the `BuildAllTargets` environment variable, the test projects will support these targets: +The target framework supported by the test projects is `net8.0` (by default). If you set the `BuildAllTargets` environment variable, the test projects will support these targets: -- `net6` -- `net7`(not LTS, might be removed at some point) -- `net8` +- `net8.0` +- `net9.0` Running the unit tests for a single target should take no more than 5 minutes (usually less): ```bash -dotnet test src/Cassandra.Tests/Cassandra.Tests.csproj -c Release -f net8 -l "console;verbosity=detailed" +dotnet test src/Cassandra.Tests/Cassandra.Tests.csproj -c Release -f net8.0 -l "console;verbosity=detailed" ``` ### Running Integration Tests diff --git a/README.md b/README.md index 58085ea11..91a9a0924 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ScyllaDB's fork of a modern, [feature-rich][features] and highly tunable C# client library for Scylla using Cassandra's binary protocol and Cassandra Query Language v3. -The driver targets .NET Standard 2.0. For more detailed information about platform compatibility, check [this section](#compatibility). +The driver targets .NET 8.0 and .NET 9.0. For more detailed information about platform compatibility, check [this section](#compatibility). ## Installation @@ -237,20 +237,13 @@ ICluster cluster = Cluster.Builder() - Apache Cassandra versions 2.0 and above. - ScyllaDB 5.x and above. - ScyllaDB Enterprise 2021.x and above. -- The driver targets .NET Standard 2.0 +- The driver targets .NET 8.0 and .NET 9.0 -Here is a list of platforms and .NET targets that Datastax uses when testing this driver: +Here is a list of platforms and .NET targets tested: -| Platform | net6 | net7 | net8 | -|-----------------------|------|------|-------| -| Windows Server 2019³ | ✓² | ✓¹ | ✓ | -| Ubuntu 18.04 | ✓ | ✓ | ✓ | - -¹ No tests are run for the `net7` target on the Windows platform but `net7` is still considered fully supported. - -² Only unit tests are ran for the `net6` target on the windows platform but `net6` is still considered fully supported. - -³ Appveyor's `Visual Studio 2022` image is used for these tests. +| Platform | net8 | net9 | +|-----------------------|------|------| +| Ubuntu 24.04 | ✓ | ✓ | Note: Big-endian systems are not supported. diff --git a/docs/source/api-docs/docfx.json b/docs/source/api-docs/docfx.json index bb6d5fc5f..28f07f753 100644 --- a/docs/source/api-docs/docfx.json +++ b/docs/source/api-docs/docfx.json @@ -15,7 +15,7 @@ "disableGitFeatures": false, "disableDefaultFilter": false, "properties": { - "TargetFramework": "netstandard2.0" + "TargetFramework": "net8.0" } } ], diff --git a/docs/source/index.md b/docs/source/index.md index 3503348f2..ab318aec6 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -2,7 +2,7 @@ ScyllaDB's fork of a modern, [feature-rich][features] and highly tunable C# client library for Scylla using Cassandra's binary protocol and Cassandra Query Language v3. -The driver targets .NET Standard 2.0. For more detailed information about platform compatibility, check [this section](#compatibility). +The driver targets .NET 8.0 and .NET 9.0. For more detailed information about platform compatibility, check [this section](#compatibility). ## Installation @@ -235,20 +235,13 @@ ICluster cluster = Cluster.Builder() - ScyllaDB 2025.1 and above. - ScyllaDB 5.x and above. - ScyllaDB Enterprise 2021.x and above. -- The driver targets .NET Standard 2.0 +- The driver targets .NET 8.0 and .NET 9.0 -Here is a list of platforms and .NET targets that Datastax uses when testing this driver: +Here is a list of platforms and .NET targets tested: -| Platform | net6 | net7 | net8 | -|-----------------------|------|------|-------| -| Windows Server 2019³ | ✓² | ✓¹ | ✓ | -| Ubuntu 18.04 | ✓ | ✓ | ✓ | - -¹ No tests are run for the `net7` target on the Windows platform but `net7` is still considered fully supported. - -² Only unit tests are ran for the `net6` target on the windows platform but `net6` is still considered fully supported. - -³ Appveyor's `Visual Studio 2022` image is used for these tests. +| Platform | net8 | net9 | +|-----------------------|------|------| +| Ubuntu 24.04 | ✓ | ✓ | Note: Big-endian systems are not supported. diff --git a/docs/source/upgrade-guide/index.md b/docs/source/upgrade-guide/index.md index dbd6d510e..05251dba0 100644 --- a/docs/source/upgrade-guide/index.md +++ b/docs/source/upgrade-guide/index.md @@ -49,6 +49,42 @@ Example: `3.4.1` These releases only contain bug fixes so they will never contain changes to the driver's public API. +## 4.0 - Target framework changes + +### Breaking: Minimum target framework is now .NET 8 + +The NuGet package targets have changed from `netstandard2.0` to `net8.0;net9.0`. This means: + +- **.NET 8** and **.NET 9** applications are fully supported. +- **.NET 6**, **.NET 7**, and **.NET Framework** applications can no longer consume this package. + +Applications targeting older runtimes should remain on the 3.x driver series. + +This change also applies to the extension packages `Cassandra.AppMetrics` and `Cassandra.OpenTelemetry`, which now target `net8.0;net9.0` instead of `netstandard2.0`. + +This change enables the driver to take advantage of modern .NET APIs and performance improvements that are not available under `netstandard2.0`. + +### Breaking: `[Serializable]` attribute and serialization constructors removed + +The `[Serializable]` attribute has been removed from `DriverException` and `NoHostAvailableException`. The serialization constructors (accepting `SerializationInfo` and `StreamingContext`) have been removed from `DriverException`, `FunctionFailureException`, and `NoHostAvailableException`. + +Binary serialization has been obsolete since .NET 5 and is disabled by default in .NET 8+. If your application relies on binary serialization of driver exceptions (e.g., cross-AppDomain marshalling or certain logging frameworks), you will need to update your code. + +### Breaking: SSL default protocol changed + +The default SSL protocol has changed from `SslProtocols.Tls` (TLS 1.0) to `SslProtocols.None` (OS-negotiated, typically TLS 1.2+). This is the recommended setting for modern .NET applications. If you connect to servers that only support TLS 1.0, you must explicitly set `SSLOptions.SslProtocol` to `SslProtocols.Tls`. + +### Breaking: Public API removals + +The following public APIs have been removed: + +- `PlatformHelper.GetTargetFramework()` +- `PlatformHelper.RuntimeSupportsCloudTlsSettings()` +- `PlatformHelper.GetNetCoreVersion()` +- `CloudSupportedAttribute` + +These APIs were used internally for multi-target framework support and are no longer needed now that the driver targets only .NET 8+. + ## 3.13.0 - Unified driver A new load balancing policy has been added: `DefaultLoadBalancingPolicy`. This is the new default load balancing policy in the driver. The behavior is the same as the previous default policy except for some DSE specific workloads so there is no impact for existing applications. diff --git a/examples/ColumnEncryption/ColumnEncryptionExample/ColumnEncryptionExample.csproj b/examples/ColumnEncryption/ColumnEncryptionExample/ColumnEncryptionExample.csproj index 98dadb94d..a00302a2d 100644 --- a/examples/ColumnEncryption/ColumnEncryptionExample/ColumnEncryptionExample.csproj +++ b/examples/ColumnEncryption/ColumnEncryptionExample/ColumnEncryptionExample.csproj @@ -8,16 +8,14 @@ You can should replace the project reference with the package reference of the d --> Exe - net8 + net8.0 ColumnEncryptionExample ColumnEncryptionExample - - TargetFramework=netstandard2.0 - + diff --git a/examples/ConcurrentExecutions/ExecuteInLoop/ExecuteInLoop.csproj b/examples/ConcurrentExecutions/ExecuteInLoop/ExecuteInLoop.csproj index d6028ffd6..31cca91f0 100644 --- a/examples/ConcurrentExecutions/ExecuteInLoop/ExecuteInLoop.csproj +++ b/examples/ConcurrentExecutions/ExecuteInLoop/ExecuteInLoop.csproj @@ -8,16 +8,14 @@ You can should replace the project reference with the package reference of the d --> Exe - net8 + net8.0 ExecuteInLoop ExecuteInLoop - - TargetFramework=netstandard2.0 - + diff --git a/examples/Mapper/MultipleKeyspacesSingleSession/MultipleKeyspacesSingleSession.csproj b/examples/Mapper/MultipleKeyspacesSingleSession/MultipleKeyspacesSingleSession.csproj index 68fc86ba9..4a26ab6c5 100644 --- a/examples/Mapper/MultipleKeyspacesSingleSession/MultipleKeyspacesSingleSession.csproj +++ b/examples/Mapper/MultipleKeyspacesSingleSession/MultipleKeyspacesSingleSession.csproj @@ -2,13 +2,11 @@ Exe - net8 + net8.0 - - TargetFramework=netstandard2.0 - + diff --git a/examples/Metrics/AppMetricsGraphite/AppMetricsGraphite.csproj b/examples/Metrics/AppMetricsGraphite/AppMetricsGraphite.csproj index 0d48a6786..f1e3741e0 100644 --- a/examples/Metrics/AppMetricsGraphite/AppMetricsGraphite.csproj +++ b/examples/Metrics/AppMetricsGraphite/AppMetricsGraphite.csproj @@ -2,7 +2,7 @@ Exe - net8 + net8.0 @@ -12,12 +12,8 @@ - - TargetFramework=netstandard2.0 - - - TargetFramework=netstandard2.0 - + + diff --git a/examples/OpenTelemetry/DistributedTracing/Api/Api.csproj b/examples/OpenTelemetry/DistributedTracing/Api/Api.csproj index 7019bfbd6..4235055d5 100644 --- a/examples/OpenTelemetry/DistributedTracing/Api/Api.csproj +++ b/examples/OpenTelemetry/DistributedTracing/Api/Api.csproj @@ -15,12 +15,8 @@ - - TargetFramework=netstandard2.0 - - - TargetFramework=netstandard2.0 - + + diff --git a/examples/OpenTelemetry/Exporter/ConsoleExporter/ConsoleExporter.csproj b/examples/OpenTelemetry/Exporter/ConsoleExporter/ConsoleExporter.csproj index bfc011186..07c63c1c8 100644 --- a/examples/OpenTelemetry/Exporter/ConsoleExporter/ConsoleExporter.csproj +++ b/examples/OpenTelemetry/Exporter/ConsoleExporter/ConsoleExporter.csproj @@ -12,12 +12,8 @@ - - TargetFramework=netstandard2.0 - - - TargetFramework=netstandard2.0 - + + diff --git a/examples/SecureConnectionBundle/MinimalExample/MinimalExample.csproj b/examples/SecureConnectionBundle/MinimalExample/MinimalExample.csproj index 3f0640513..76c2d580f 100644 --- a/examples/SecureConnectionBundle/MinimalExample/MinimalExample.csproj +++ b/examples/SecureConnectionBundle/MinimalExample/MinimalExample.csproj @@ -8,7 +8,7 @@ You can should replace the project reference with the package reference of the d --> Exe - net8 + net8.0 diff --git a/examples/Ssl/SslServerAuthOnly/SslServerAuthOnly.csproj b/examples/Ssl/SslServerAuthOnly/SslServerAuthOnly.csproj index 425100e94..ff2e57ac5 100644 --- a/examples/Ssl/SslServerAuthOnly/SslServerAuthOnly.csproj +++ b/examples/Ssl/SslServerAuthOnly/SslServerAuthOnly.csproj @@ -2,7 +2,7 @@ Exe - net8 + net8.0 diff --git a/examples/Ssl/SslTwoWayAuth/SslTwoWayAuth.csproj b/examples/Ssl/SslTwoWayAuth/SslTwoWayAuth.csproj index 425100e94..ff2e57ac5 100644 --- a/examples/Ssl/SslTwoWayAuth/SslTwoWayAuth.csproj +++ b/examples/Ssl/SslTwoWayAuth/SslTwoWayAuth.csproj @@ -2,7 +2,7 @@ Exe - net8 + net8.0 diff --git a/src/Cassandra.IntegrationTests/Cassandra.IntegrationTests.csproj b/src/Cassandra.IntegrationTests/Cassandra.IntegrationTests.csproj index 3b036059a..8a13cd469 100644 --- a/src/Cassandra.IntegrationTests/Cassandra.IntegrationTests.csproj +++ b/src/Cassandra.IntegrationTests/Cassandra.IntegrationTests.csproj @@ -1,7 +1,7 @@  - net9;net8;net7;net6 - net9 + net8.0;net9.0 + net9.0 $(BuildTarget) true NU1901;NU1902;NU1903;NU1904 @@ -11,25 +11,13 @@ true Cassandra.IntegrationTests true - 7.1 + latest - - $(DefineConstants);NETCOREAPP - - - - TargetFramework=netstandard2.0 - - - TargetFramework=$(TargetFramework) - - - TargetFramework=netstandard2.0 - - - TargetFramework=netstandard2.0 - + + + + @@ -38,21 +26,15 @@ - + - - - - - - - + diff --git a/src/Cassandra.IntegrationTests/Core/PoolShortTests.cs b/src/Cassandra.IntegrationTests/Core/PoolShortTests.cs index 41c5f4ce9..a3b597db9 100644 --- a/src/Cassandra.IntegrationTests/Core/PoolShortTests.cs +++ b/src/Cassandra.IntegrationTests/Core/PoolShortTests.cs @@ -236,10 +236,9 @@ public void Connect_With_Ssl_Test() //use ssl var testCluster = TestClusterManager.CreateNew(1, new TestClusterOptions { UseSsl = true }); -#pragma warning disable SYSLIB0039 // Type or member is obsolete using (var cluster = ClusterBuilder() .AddContactPoint(testCluster.InitialContactPoint) - .WithSSL(new SSLOptions(SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, false, (a, b, c, d) => true)) + .WithSSL(new SSLOptions(SslProtocols.Tls12, false, (a, b, c, d) => true)) .Build()) { Assert.DoesNotThrow(() => @@ -248,7 +247,6 @@ public void Connect_With_Ssl_Test() TestHelper.Invoke(() => session.Execute("SELECT * FROM system.local WHERE key='local'"), 10); }); } -#pragma warning restore SYSLIB0039 // Type or member is obsolete } finally { diff --git a/src/Cassandra.IntegrationTests/Policies/Tests/LoadBalancingPolicyShortTests.cs b/src/Cassandra.IntegrationTests/Policies/Tests/LoadBalancingPolicyShortTests.cs index 3fd11d5da..952b498cc 100644 --- a/src/Cassandra.IntegrationTests/Policies/Tests/LoadBalancingPolicyShortTests.cs +++ b/src/Cassandra.IntegrationTests/Policies/Tests/LoadBalancingPolicyShortTests.cs @@ -75,7 +75,7 @@ public void TokenAware_TargetPartition_NoHops() var traces = new List(); for (var i = -10; i < 10; i++) { - var partitionKey = BitConverter.GetBytes(i).Reverse().ToArray(); + var partitionKey = BitConverter.GetBytes(i).AsEnumerable().Reverse().ToArray(); var statement = new SimpleStatement(string.Format("INSERT INTO " + policyTestTools.TableName + " (k, i) VALUES ({0}, {0})", i)) .SetRoutingKey(new RoutingKey() { RawRoutingKey = partitionKey }) .EnableTracing(); @@ -152,7 +152,7 @@ public void TokenAware_Composite_NoHops() var statement = new SimpleStatement(string.Format("INSERT INTO " + policyTestTools.TableName + " (k1, k2, i) VALUES ('{0}', {0}, {0})", i)) .SetRoutingKey( new RoutingKey() { RawRoutingKey = Encoding.UTF8.GetBytes(i.ToString()) }, - new RoutingKey() { RawRoutingKey = BitConverter.GetBytes(i).Reverse().ToArray() }) + new RoutingKey() { RawRoutingKey = BitConverter.GetBytes(i).AsEnumerable().Reverse().ToArray() }) .EnableTracing(); var rs = session.Execute(statement); traces.Add(rs.Info.QueryTrace); diff --git a/src/Cassandra.Tests/Cassandra.Tests.csproj b/src/Cassandra.Tests/Cassandra.Tests.csproj index cc260a25e..9c2554a0a 100644 --- a/src/Cassandra.Tests/Cassandra.Tests.csproj +++ b/src/Cassandra.Tests/Cassandra.Tests.csproj @@ -1,7 +1,7 @@  - net9;net8;net7;net6 - net9 + net8.0;net9.0 + net9.0 $(BuildTarget) true NU1901;NU1902;NU1903;NU1904 @@ -11,36 +11,24 @@ true Cassandra.Tests true - 7.1 - - - $(DefineConstants);NETCOREAPP + latest - - TargetFramework=netstandard2.0 - - - TargetFramework=netstandard2.0 - + + - + - + - - TargetFramework=netstandard2.0 - - - - - + + diff --git a/src/Cassandra.Tests/EndianBitConverterTests.cs b/src/Cassandra.Tests/EndianBitConverterTests.cs index b6d03f5c1..93d3e4458 100644 --- a/src/Cassandra.Tests/EndianBitConverterTests.cs +++ b/src/Cassandra.Tests/EndianBitConverterTests.cs @@ -40,10 +40,10 @@ public void ToInt32_SetBytes_Test() EndianBitConverter.SetBytes(false, buffer, 0, v.Item1); CollectionAssert.AreEqual(v.Item2, buffer); EndianBitConverter.SetBytes(true, buffer, 0, v.Item1); - CollectionAssert.AreEqual(v.Item2.Reverse(), buffer); + CollectionAssert.AreEqual(v.Item2.AsEnumerable().Reverse(), buffer); var decoded = EndianBitConverter.ToInt32(false, v.Item2, 0); Assert.AreEqual(v.Item1, decoded); - decoded = EndianBitConverter.ToInt32(true, v.Item2.Reverse().ToArray(), 0); + decoded = EndianBitConverter.ToInt32(true, v.Item2.AsEnumerable().Reverse().ToArray(), 0); Assert.AreEqual(v.Item1, decoded); } } @@ -65,10 +65,10 @@ public void ToDouble_SetBytes_Test() EndianBitConverter.SetBytes(false, buffer, 0, v.Item1); CollectionAssert.AreEqual(v.Item2, buffer); EndianBitConverter.SetBytes(true, buffer, 0, v.Item1); - CollectionAssert.AreEqual(v.Item2.Reverse(), buffer); + CollectionAssert.AreEqual(v.Item2.AsEnumerable().Reverse(), buffer); var decoded = EndianBitConverter.ToDouble(false, v.Item2, 0); Assert.AreEqual(v.Item1, decoded); - decoded = EndianBitConverter.ToDouble(true, v.Item2.Reverse().ToArray(), 0); + decoded = EndianBitConverter.ToDouble(true, v.Item2.AsEnumerable().Reverse().ToArray(), 0); Assert.AreEqual(v.Item1, decoded); } } diff --git a/src/Cassandra.Tests/IOUnitTests.cs b/src/Cassandra.Tests/IOUnitTests.cs index 26c2227d9..5cf5be12c 100644 --- a/src/Cassandra.Tests/IOUnitTests.cs +++ b/src/Cassandra.Tests/IOUnitTests.cs @@ -68,7 +68,7 @@ public void OperationState_Can_Concurrently_Get_Timeout_And_Response() if ((counter++) % 2 == 0) { //invert order - actions = actions.Reverse().ToArray(); + actions = actions.AsEnumerable().Reverse().ToArray(); } TestHelper.ParallelInvoke(actions); }, times); diff --git a/src/Cassandra.Tests/Mapping/Linq/LinqExpressionEvaluationTests.cs b/src/Cassandra.Tests/Mapping/Linq/LinqExpressionEvaluationTests.cs new file mode 100644 index 000000000..c5a34ba13 --- /dev/null +++ b/src/Cassandra.Tests/Mapping/Linq/LinqExpressionEvaluationTests.cs @@ -0,0 +1,141 @@ +// +// Copyright (C) DataStax Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using System.Linq; +using Cassandra.Data.Linq; +using Cassandra.Tests.Mapping.Pocos; +using NUnit.Framework; + +namespace Cassandra.Tests.Mapping.Linq +{ + /// + /// Tests that exercise the EvaluateExpression code path in CqlExpressionVisitor + /// with various expression types (int, string, int?, collections). + /// These also cover the IsByRefLike unwrapping path on .NET 9+. + /// + [TestFixture] + public class LinqExpressionEvaluationTests : MappingTestBase + { + [Test] + public void Where_With_Int_Constant_Generates_Correct_Cql() + { + var table = SessionExtensions.GetTable(GetSession((_, __) => { })); + var query = table.Where(e => e.ck2 == 42); + Assert.That(query.ToString(), Is.EqualTo( + @"SELECT ""x_ck1"", ""x_ck2"", ""x_f1"", ""x_pk"" FROM ""x_t"" WHERE ""x_ck2"" = ? ALLOW FILTERING")); + } + + [Test] + public void Where_With_Int_Variable_Generates_Correct_Cql() + { + var table = SessionExtensions.GetTable(GetSession((_, __) => { })); + var value = 42; + var query = table.Where(e => e.ck2 == value); + Assert.That(query.ToString(), Is.EqualTo( + @"SELECT ""x_ck1"", ""x_ck2"", ""x_f1"", ""x_pk"" FROM ""x_t"" WHERE ""x_ck2"" = ? ALLOW FILTERING")); + } + + [Test] + public void Where_With_String_Constant_Generates_Correct_Cql() + { + var table = SessionExtensions.GetTable(GetSession((_, __) => { })); + var query = table.Where(e => e.pk == "test"); + Assert.That(query.ToString(), Is.EqualTo( + @"SELECT ""x_ck1"", ""x_ck2"", ""x_f1"", ""x_pk"" FROM ""x_t"" WHERE ""x_pk"" = ? ALLOW FILTERING")); + } + + [Test] + public void Where_With_String_Variable_Generates_Correct_Cql() + { + var table = SessionExtensions.GetTable(GetSession((_, __) => { })); + var pk = "hello"; + var query = table.Where(e => e.pk == pk); + Assert.That(query.ToString(), Is.EqualTo( + @"SELECT ""x_ck1"", ""x_ck2"", ""x_f1"", ""x_pk"" FROM ""x_t"" WHERE ""x_pk"" = ? ALLOW FILTERING")); + } + + [Test] + public void Where_With_Nullable_Int_Variable_Generates_Correct_Cql() + { + var table = SessionExtensions.GetTable(GetSession((_, __) => { })); + int? value = 10; + var query = table.Where(e => e.ck1 == value); + Assert.That(query.ToString(), Is.EqualTo( + @"SELECT ""x_ck1"", ""x_ck2"", ""x_f1"", ""x_pk"" FROM ""x_t"" WHERE ""x_ck1"" = ? ALLOW FILTERING")); + } + + [Test] + public void Where_With_Contains_On_Int_Array_Generates_In_Clause() + { + var table = SessionExtensions.GetTable(GetSession((_, __) => { })); + var ids = new[] { 10, 20, 30 }; + var query = table.Where(e => ids.Contains(e.ck2)); + Assert.That(query.ToString(), Is.EqualTo( + @"SELECT ""x_ck1"", ""x_ck2"", ""x_f1"", ""x_pk"" FROM ""x_t"" WHERE ""x_ck2"" IN ? ALLOW FILTERING")); + } + + [Test] + public void Where_With_Contains_On_List_Generates_In_Clause() + { + var table = SessionExtensions.GetTable(GetSession((_, __) => { })); + var ids = new List { 1, 2, 3 }; + var query = table.Where(e => ids.Contains(e.ck2)); + Assert.That(query.ToString(), Is.EqualTo( + @"SELECT ""x_ck1"", ""x_ck2"", ""x_f1"", ""x_pk"" FROM ""x_t"" WHERE ""x_ck2"" IN ? ALLOW FILTERING")); + } + + [Test] + public void Where_With_Contains_On_String_List_Generates_In_Clause() + { + var table = SessionExtensions.GetTable(GetSession((_, __) => { })); + var keys = new List { "a", "b", "c" }; + var query = table.Where(e => keys.Contains(e.pk)); + Assert.That(query.ToString(), Is.EqualTo( + @"SELECT ""x_ck1"", ""x_ck2"", ""x_f1"", ""x_pk"" FROM ""x_t"" WHERE ""x_pk"" IN ? ALLOW FILTERING")); + } + + [Test] + public void Where_With_Contains_On_Empty_Array_Generates_In_Clause() + { + var table = SessionExtensions.GetTable(GetSession((_, __) => { })); + var ids = new int[0]; + var query = table.Where(e => ids.Contains(e.ck2)); + Assert.That(query.ToString(), Is.EqualTo( + @"SELECT ""x_ck1"", ""x_ck2"", ""x_f1"", ""x_pk"" FROM ""x_t"" WHERE ""x_ck2"" IN ? ALLOW FILTERING")); + } + + [Test] + public void Where_With_Contains_On_Nullable_Int_Array_Generates_In_Clause() + { + var table = SessionExtensions.GetTable(GetSession((_, __) => { })); + var ids = new int?[] { 10, 30, 40 }; + var query = table.Where(e => ids.Contains(e.ck1)); + Assert.That(query.ToString(), Is.EqualTo( + @"SELECT ""x_ck1"", ""x_ck2"", ""x_f1"", ""x_pk"" FROM ""x_t"" WHERE ""x_ck1"" IN ? ALLOW FILTERING")); + } + + [Test] + public void Where_With_Contains_And_Additional_Condition() + { + var table = SessionExtensions.GetTable(GetSession((_, __) => { })); + var ids = new[] { 1, 2, 3 }; + var query = table.Where(e => ids.Contains(e.ck2) && e.pk == "x"); + Assert.That(query.ToString(), Is.EqualTo( + @"SELECT ""x_ck1"", ""x_ck2"", ""x_f1"", ""x_pk"" FROM ""x_t"" WHERE ""x_ck2"" IN ? AND ""x_pk"" = ? ALLOW FILTERING")); + } + } +} diff --git a/src/Cassandra.Tests/Mapping/SafeInvokeConverterTests.cs b/src/Cassandra.Tests/Mapping/SafeInvokeConverterTests.cs new file mode 100644 index 000000000..45e04bb95 --- /dev/null +++ b/src/Cassandra.Tests/Mapping/SafeInvokeConverterTests.cs @@ -0,0 +1,110 @@ +// +// Copyright (C) DataStax Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using Cassandra.Mapping.TypeConversion; +using NUnit.Framework; + +namespace Cassandra.Tests.Mapping +{ + [TestFixture] + public class SafeInvokeConverterTests + { + [Test] + public void SafeInvokeConverter_Should_Invoke_Func_Int_To_String() + { + Func converter = i => i.ToString(); + var result = TypeConverter.SafeInvokeConverter(converter, 42, typeof(int), typeof(string)); + Assert.That(result, Is.EqualTo("42")); + } + + [Test] + public void SafeInvokeConverter_Should_Invoke_Func_String_To_Int() + { + Func converter = s => int.Parse(s); + var result = TypeConverter.SafeInvokeConverter(converter, "123", typeof(string), typeof(int)); + Assert.That(result, Is.EqualTo(123)); + } + + [Test] + public void SafeInvokeConverter_Should_Handle_Nullable_Source() + { + Func converter = i => i.HasValue ? (long)i.Value : -1L; + var result = TypeConverter.SafeInvokeConverter(converter, (int?)5, typeof(int?), typeof(long)); + Assert.That(result, Is.EqualTo(5L)); + } + + [Test] + public void SafeInvokeConverter_Should_Handle_Null_Nullable_Source() + { + Func converter = i => i.HasValue ? (long)i.Value : -1L; + var result = TypeConverter.SafeInvokeConverter(converter, null, typeof(int?), typeof(long)); + Assert.That(result, Is.EqualTo(-1L)); + } + + [Test] + public void SafeInvokeConverter_Should_Handle_Nullable_Dest() + { + Func converter = i => (long?)i; + var result = TypeConverter.SafeInvokeConverter(converter, 7, typeof(int), typeof(long?)); + Assert.That(result, Is.EqualTo(7L)); + } + + [Test] + public void SafeInvokeConverter_Should_Box_Value_Types_Correctly() + { + Func converter = i => i * 2; + var result = TypeConverter.SafeInvokeConverter(converter, 21, typeof(int), typeof(int)); + Assert.That(result, Is.EqualTo(42)); + } + + [Test] + public void SafeInvokeConverter_Should_Handle_Reference_Type_Round_Trip() + { + Func converter = s => s?.ToUpperInvariant(); + var result = TypeConverter.SafeInvokeConverter(converter, "hello", typeof(string), typeof(string)); + Assert.That(result, Is.EqualTo("HELLO")); + } + + [Test] + public void SafeInvokeConverter_Should_Handle_Guid_To_TimeUuid() + { + var guid = Guid.NewGuid(); + Func converter = g => g.ToString(); + var result = TypeConverter.SafeInvokeConverter(converter, guid, typeof(Guid), typeof(string)); + Assert.That(result, Is.EqualTo(guid.ToString())); + } + + [Test] + public void SafeInvokeConverter_Should_Rebind_Non_Func_Delegate() + { + // Use a delegate created via Delegate.CreateDelegate that is not a Func + // but has a compatible signature, exercising the fallback path in InvokeConverter. + var method = typeof(SafeInvokeConverterTests).GetMethod(nameof(ConvertIntToString), + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + // Create as a generic Delegate (Action-like) — not directly a Func + var del = Delegate.CreateDelegate(typeof(Converter), method); + + var result = TypeConverter.SafeInvokeConverter(del, 99, typeof(int), typeof(string)); + Assert.That(result, Is.EqualTo("99")); + } + + private static string ConvertIntToString(int value) + { + return value.ToString(); + } + } +} diff --git a/src/Cassandra.Tests/TargetTests.cs b/src/Cassandra.Tests/TargetTests.cs index 12f899125..c85c16b67 100644 --- a/src/Cassandra.Tests/TargetTests.cs +++ b/src/Cassandra.Tests/TargetTests.cs @@ -24,25 +24,18 @@ namespace Cassandra.Tests [TestFixture] public class TargetTests { -#if NETCOREAPP [Test] - public void Should_TargetNetstandard15_When_TestsTargetNetcore20() + public void Should_TargetModernNet() { var framework = Assembly .GetAssembly(typeof(ISession))? .GetCustomAttribute()? .FrameworkName; - Assert.AreEqual(".NETStandard,Version=v2.0", framework); + Assert.IsNotNull(framework); + Assert.IsTrue( + framework.StartsWith(".NETCoreApp,Version=v"), + $"Expected .NETCoreApp target but got: {framework}"); } -#else - [Test] - public void Should_FailTest_When_TestsTargetDifferentTarget() - { - Assert.Fail("Something changed in the test project targets. "+ - "Please review these tests to make sure the change is intended "+ - "and if so please add new tests for the new targets.") - } -#endif } -} \ No newline at end of file +} diff --git a/src/Cassandra.Tests/TestAttributes/CloudSupportedAttribute.cs b/src/Cassandra.Tests/TestAttributes/CloudSupportedAttribute.cs deleted file mode 100644 index a4017b62c..000000000 --- a/src/Cassandra.Tests/TestAttributes/CloudSupportedAttribute.cs +++ /dev/null @@ -1,41 +0,0 @@ -// -// Copyright (C) DataStax Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using Cassandra.Helpers; -using NUnit.Framework; -using NUnit.Framework.Interfaces; - -namespace Cassandra.Tests.TestAttributes -{ - public class CloudSupportedAttribute : NUnitAttribute, IApplyToTest - { - public bool Supported { get; set; } = true; - - public void ApplyToTest(NUnit.Framework.Internal.Test test) - { - if (Supported != CloudSupported()) - { - test.RunState = RunState.Ignored; - test.Properties.Set("_SKIPREASON", $"Test designed to run with CloudSupport = {Supported} but it isn't the case."); - } - } - - private bool CloudSupported() - { - return PlatformHelper.RuntimeSupportsCloudTlsSettings(); - } - } -} \ No newline at end of file diff --git a/src/Cassandra/Cassandra.csproj b/src/Cassandra/Cassandra.csproj index d6a54c203..936b1cac2 100644 --- a/src/Cassandra/Cassandra.csproj +++ b/src/Cassandra/Cassandra.csproj @@ -3,11 +3,11 @@ A modern, feature-rich and highly tunable C# client library for ScyllaDB and Apache Cassandra Copyright © by DataStax and ScyllaDB ScyllaDB C# Driver for ScyllaDB - 3.99.0.0 - 3.22.0.2 + 4.0.0.0 + 4.0.0.0 false DataStax and ScyllaDB - netstandard2.0 + net8.0;net9.0 $(NoWarn);1591 true NU1901;NU1902;NU1903;NU1904 @@ -23,13 +23,9 @@ LICENSE.md https://github.com/scylladb/csharp-driver https://github.com/scylladb/csharp-driver - 7.1 + latest false - - $(DefineConstants);NETCOREAPP - - @@ -41,7 +37,7 @@ - + diff --git a/src/Cassandra/Data/Linq/CqlExpressionVisitor.cs b/src/Cassandra/Data/Linq/CqlExpressionVisitor.cs index a6e23b6b4..d878a6232 100644 --- a/src/Cassandra/Data/Linq/CqlExpressionVisitor.cs +++ b/src/Cassandra/Data/Linq/CqlExpressionVisitor.cs @@ -22,6 +22,7 @@ using System.Reflection; using System.Text; using Cassandra.Data.Linq.ExpressionParsing; +using Cassandra.Helpers; using Cassandra.Mapping; using Cassandra.Mapping.Utils; @@ -60,6 +61,78 @@ internal class CqlExpressionVisitor : ExpressionVisitor private static readonly ICqlIdentifierHelper CqlIdentifierHelper = new CqlIdentifierHelper(); + private static readonly MethodInfo InvokeFuncMethod = typeof(CqlExpressionVisitor) + .GetMethod(nameof(InvokeFunc), BindingFlags.NonPublic | BindingFlags.Static) + ?? throw new InvalidOperationException("InvokeFunc method not found via reflection."); + + /// + /// Compiles and evaluates an expression tree without using DynamicInvoke, + /// which throws on .NET 9 for dynamically compiled delegates. + /// Also unwraps ref struct types (e.g. ReadOnlySpan) that .NET 9+ + /// introduces when resolving MemoryExtensions.Contains. + /// + private static object EvaluateExpression(Expression expression) + { + // Unwrap ref struct wrappers (ReadOnlySpan, Span, etc.) + // that can't be boxed, used as generic arguments, or invoked via reflection. + // + // .NET 9+ resolves collection.Contains(item) as + // MemoryExtensions.Contains(collection.AsSpan(), item), producing + // ReadOnlySpan intermediaries. We peel these back to the underlying + // collection; the Contains semantics are handled separately by + // EvaluateContainsMethod. + const int maxUnwrapIterations = 10; + for (var i = 0; i < maxUnwrapIterations && expression.Type.IsByRefLike; i++) + { + if (expression is UnaryExpression unary) + { + expression = unary.Operand; + } + else if (expression is MethodCallExpression call + && call.Method.DeclaringType?.FullName == "System.MemoryExtensions" + && call.Arguments.Count > 0) + { + // MemoryExtensions static methods take the source collection as the + // first argument (e.g. AsSpan(list)). Unwrap to that collection so + // the caller can evaluate it normally. + expression = call.Arguments[0]; + } + else + { + throw new InvalidOperationException( + $"Cannot evaluate expression of ref struct type '{expression.Type}'. " + + $"Expression: {expression}. " + + (expression is MethodCallExpression mc + ? $"Unsupported method: {mc.Method.DeclaringType?.FullName}.{mc.Method.Name}. " + : "") + + "This LINQ expression pattern is not supported."); + } + } + + if (expression.Type.IsByRefLike) + { + throw new InvalidOperationException( + $"Failed to unwrap ref struct type '{expression.Type}' after {maxUnwrapIterations} iterations. " + + $"Expression: {expression}. This LINQ expression pattern is not supported."); + } + + var compiled = Expression.Lambda(expression).Compile(); + var invoker = ReflectionDelegateCache>.GetOrAdd( + expression.Type, InvokeFuncMethod, t => new[] { t }); + return invoker(compiled); + } + + private static object InvokeFunc(Delegate del) + { + if (del is Func func) + { + return func(); + } + throw new InvalidOperationException( + $"Compiled expression delegate was {del.GetType()}, expected Func<{typeof(T)}>. " + + "This LINQ expression pattern may not be supported."); + } + private readonly IList> _projections = new List>(); private readonly IList> _orderBy = new List>(); @@ -577,7 +650,7 @@ private Expression AddProjection(Expression node, PocoColumn column = null) } else { - value = Expression.Lambda(node).Compile().DynamicInvoke(); + value = EvaluateExpression(node); } if (column == null) { @@ -607,7 +680,7 @@ private Expression EvaluateConditionFunction(MethodCallExpression node) case nameof(string.StartsWith) when node.Method.DeclaringType == typeof(string): Visit(node.Object); var startsWithArgument = node.Arguments[0]; - var startString = (string)Expression.Lambda(startsWithArgument).Compile().DynamicInvoke(); + var startString = (string)EvaluateExpression(startsWithArgument); var endString = startString + CqlExpressionVisitor.Utf8MaxValue; // Create 2 conditions, ie: WHERE col1 >= startString AND col2 < endString var column = condition.Column; @@ -650,7 +723,7 @@ private Expression EvaluateConditionFunction(MethodCallExpression node) return node; } // Try to invoke to obtain the parameter value - condition.SetParameter(Expression.Lambda(node).Compile().DynamicInvoke()); + condition.SetParameter(EvaluateExpression(node)); return node; } @@ -678,7 +751,7 @@ private void EvaluateContainsMethod(MethodCallExpression node) EvaluateCompositeColumn(columnExpression); } - var values = Expression.Lambda(parameterExpression).Compile().DynamicInvoke() as IEnumerable; + var values = EvaluateExpression(parameterExpression) as IEnumerable; if (values == null) { throw new InvalidOperationException("Contains parameter must be IEnumerable"); @@ -751,17 +824,17 @@ private bool EvaluateOperatorMethod(MethodCallExpression node) } // Use the last argument (valid for maps and list/sets) var argument = node.Arguments[node.Arguments.Count - 1]; - var value = Expression.Lambda(argument).Compile().DynamicInvoke(); + var value = EvaluateExpression(argument); _projections.Add(Tuple.Create(column, value, expressionType)); return true; } private static Expression DropNullableConversion(Expression node) { - if (node is UnaryExpression && node.NodeType == ExpressionType.Convert && node.Type.GetTypeInfo().IsGenericType && - string.Compare(node.Type.Name, "Nullable`1", StringComparison.Ordinal) == 0) + if (node is UnaryExpression unary && unary.NodeType == ExpressionType.Convert && + Nullable.GetUnderlyingType(unary.Type) != null) { - return (node as UnaryExpression).Operand; + return unary.Operand; } return node; } @@ -788,7 +861,7 @@ protected override Expression VisitUnary(UnaryExpression node) } else { - var val = Expression.Lambda(node).Compile().DynamicInvoke(); + var val = EvaluateExpression(node); condition.SetParameter(val); } return node; @@ -803,7 +876,7 @@ protected override Expression VisitUnary(UnaryExpression node) } if (column != null && column.IsCounter) { - var value = Expression.Lambda(node).Compile().DynamicInvoke(); + var value = EvaluateExpression(node); if (!(value is long || value is int)) { throw new ArgumentException("Only Int64 and Int32 values are supported as counter increment of decrement values"); @@ -876,7 +949,7 @@ protected override Expression VisitBinary(BinaryExpression node) if (!CqlExpressionVisitor.CqlUnsupTags.Contains(node.NodeType)) { - condition.SetParameter(Expression.Lambda(node).Compile().DynamicInvoke()); + condition.SetParameter(EvaluateExpression(node)); return node; } } @@ -1115,7 +1188,7 @@ private static object GetClosureValue(MemberExpression node) } else { - value = Expression.Lambda(node).Compile().DynamicInvoke(); + value = EvaluateExpression(node); } return value; } diff --git a/src/Cassandra/Exceptions/DriverException.cs b/src/Cassandra/Exceptions/DriverException.cs index ec0c598b5..0fad92834 100644 --- a/src/Cassandra/Exceptions/DriverException.cs +++ b/src/Cassandra/Exceptions/DriverException.cs @@ -15,14 +15,12 @@ // using System; -using System.Runtime.Serialization; namespace Cassandra { /// /// Top level class for exceptions thrown by the driver. /// - [Serializable] public class DriverException : Exception { public DriverException(string message) @@ -34,11 +32,5 @@ public DriverException(string message, Exception innerException) : base(message, innerException) { } - - protected DriverException(SerializationInfo info, StreamingContext context) : - base(info, context) - { - - } } } diff --git a/src/Cassandra/Exceptions/FunctionFailureException.cs b/src/Cassandra/Exceptions/FunctionFailureException.cs index 348d16e74..282d31c9c 100644 --- a/src/Cassandra/Exceptions/FunctionFailureException.cs +++ b/src/Cassandra/Exceptions/FunctionFailureException.cs @@ -15,7 +15,6 @@ // using System; -using System.Runtime.Serialization; // ReSharper disable once CheckNamespace namespace Cassandra @@ -47,9 +46,5 @@ public FunctionFailureException(string message) : base(message) public FunctionFailureException(string message, Exception innerException) : base(message, innerException) { } - - protected FunctionFailureException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } } } diff --git a/src/Cassandra/Exceptions/NoHostAvailableException.cs b/src/Cassandra/Exceptions/NoHostAvailableException.cs index 0b67d77bf..ee430a9a2 100644 --- a/src/Cassandra/Exceptions/NoHostAvailableException.cs +++ b/src/Cassandra/Exceptions/NoHostAvailableException.cs @@ -18,7 +18,6 @@ using System.Linq; using System.Collections.Generic; using System.Net; -using System.Runtime.Serialization; using System.Text; namespace Cassandra @@ -31,7 +30,6 @@ namespace Cassandra /// purpose, the list of hosts that have been tried along with the failure cause /// can be retrieved using the #errors method. /// - [Serializable] public class NoHostAvailableException : DriverException { private const string StartMessage = "All hosts tried for query failed (tried "; @@ -64,12 +62,6 @@ internal NoHostAvailableException(string message, Exception innerException) : ba Errors = new Dictionary(0); } - protected NoHostAvailableException(SerializationInfo info, StreamingContext context) : - base(info, context) - { - - } - private static string CreateMessage(Dictionary errors) { if (errors.Count == 0) diff --git a/src/Cassandra/FrameReader.cs b/src/Cassandra/FrameReader.cs index 8d8c8b029..63023a0d8 100644 --- a/src/Cassandra/FrameReader.cs +++ b/src/Cassandra/FrameReader.cs @@ -48,7 +48,7 @@ public FrameReader(Stream stream, ISerializer serializer) public byte ReadByte() { - Utils.ReadExactly(_stream, _buffer, 0, 1); + _stream.ReadExactly(_buffer, 0, 1); return _buffer[0]; } @@ -57,7 +57,7 @@ public byte ReadByte() /// public ushort ReadUInt16() { - Utils.ReadExactly(_stream, _buffer, 0, 2); + _stream.ReadExactly(_buffer, 0, 2); return BeConverter.ToUInt16(_buffer); } @@ -66,13 +66,13 @@ public ushort ReadUInt16() /// public short ReadInt16() { - Utils.ReadExactly(_stream, _buffer, 0, 2); + _stream.ReadExactly(_buffer, 0, 2); return BeConverter.ToInt16(_buffer); } public int ReadInt32() { - Utils.ReadExactly(_stream, _buffer, 0, 4); + _stream.ReadExactly(_buffer, 0, 4); return BeConverter.ToInt32(_buffer); } @@ -91,7 +91,7 @@ public string ReadLongString() private string ReadStringByLength(int length) { var bytes = new byte[length]; - Utils.ReadExactly(_stream, bytes, 0, length); + _stream.ReadExactly(bytes, 0, length); return Encoding.UTF8.GetString(bytes); } @@ -122,14 +122,14 @@ public IPEndPoint ReadInet() IPAddress ip; if (length == 4) { - Utils.ReadExactly(_stream, _buffer, 0, length); + _stream.ReadExactly(_buffer, 0, length); ip = new IPAddress(_buffer); return new IPEndPoint(ip, ReadInt32()); } if (length == 16) { var buffer = new byte[16]; - Utils.ReadExactly(_stream, buffer, 0, length); + _stream.ReadExactly(buffer, 0, length); ip = new IPAddress(buffer); return new IPEndPoint(ip, ReadInt32()); } @@ -184,7 +184,7 @@ public byte[] ReadShortBytes() public void Read(byte[] buffer, int offset, int count) { - Utils.ReadExactly(_stream, buffer, offset, count); + _stream.ReadExactly(buffer, offset, count); } /// @@ -193,7 +193,7 @@ public void Read(byte[] buffer, int offset, int count) /// internal object ReadFromBytes(byte[] buffer, int offset, int length, ColumnTypeCode typeCode, IColumnInfo typeInfo) { - Utils.ReadExactly(_stream, buffer, offset, length); + _stream.ReadExactly(buffer, offset, length); return _serializer.Deserialize(buffer, 0, length, typeCode, typeInfo); } @@ -203,7 +203,7 @@ internal object ReadFromBytes(byte[] buffer, int offset, int length, ColumnTypeC /// internal object ReadFromBytesEncrypted(string ks, string table, string column, byte[] buffer, int offset, int length, ColumnTypeCode typeCode, IColumnInfo typeInfo) { - Utils.ReadExactly(_stream, buffer, offset, length); + _stream.ReadExactly(buffer, offset, length); return _serializer.DeserializeAndDecrypt(ks, table, column, buffer, 0, length, typeCode, typeInfo); } } diff --git a/src/Cassandra/FrameWriter.cs b/src/Cassandra/FrameWriter.cs index 500a9ec8e..f3331d0b7 100644 --- a/src/Cassandra/FrameWriter.cs +++ b/src/Cassandra/FrameWriter.cs @@ -49,7 +49,7 @@ internal byte[] GetBuffer() { var buffer = new byte[_stream.Length]; _stream.Position = 0; - Utils.ReadExactly(_stream, buffer, 0, _stream.Length > int.MaxValue ? int.MaxValue : (int)_stream.Length); + _stream.ReadExactly(buffer, 0, _stream.Length > int.MaxValue ? int.MaxValue : (int)_stream.Length); return buffer; } diff --git a/src/Cassandra/Helpers/PlatformHelper.cs b/src/Cassandra/Helpers/PlatformHelper.cs index 01084c1da..3052bd4fa 100644 --- a/src/Cassandra/Helpers/PlatformHelper.cs +++ b/src/Cassandra/Helpers/PlatformHelper.cs @@ -16,8 +16,8 @@ using System; using System.IO; -using System.Reflection; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using System.Text.RegularExpressions; namespace Cassandra.Helpers @@ -31,15 +31,6 @@ public static bool IsKerberosSupported() return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); } - public static string GetTargetFramework() - { -#if NETSTANDARD2_0 - return ".NET Standard 2.0"; -#else - return null; -#endif - } - public static CpuInfo GetCpuInfo() { try @@ -75,6 +66,7 @@ public CpuInfo(string name, int length) } } + [SupportedOSPlatform("windows")] public static CpuInfo GetWmiCpuInfo() { var count = 0; @@ -112,44 +104,5 @@ public static CpuInfo GetLinuxProcCpuInfo() return new CpuInfo(null, Environment.ProcessorCount); } - public static bool RuntimeSupportsCloudTlsSettings() - { - var netCoreVersion = PlatformHelper.GetNetCoreVersion(); - if (netCoreVersion != null && Version.TryParse(netCoreVersion.Split('-')[0], out var version)) - { - PlatformHelper.Logger.Info("NET Core Runtime detected: " + netCoreVersion); - if (version.Major > 2) - { - return true; - } - - if (version.Major == 2 && version.Minor >= 1) - { - return true; - } - - return false; - } - - PlatformHelper.Logger.Warning( - "Could not detect NET Core Runtime or version parsing failed. " + - "Assuming that HttpClientHandler supports the required TLS settings for Cloud. " + - "Version: " + (netCoreVersion ?? "null")); - - // if we can't detect the version, assume it supports - return true; - } - - public static string GetNetCoreVersion() - { - var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; - var assemblyPath = assembly.CodeBase.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); - var netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App"); - if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2) - { - return assemblyPath[netCoreAppIndex + 1]; - } - return null; - } } -} \ No newline at end of file +} diff --git a/src/Cassandra/Helpers/ReflectionDelegateCache.cs b/src/Cassandra/Helpers/ReflectionDelegateCache.cs new file mode 100644 index 000000000..d195a65db --- /dev/null +++ b/src/Cassandra/Helpers/ReflectionDelegateCache.cs @@ -0,0 +1,49 @@ +// +// Copyright (C) DataStax Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Concurrent; +using System.Reflection; + +namespace Cassandra.Helpers +{ + /// + /// Caches delegates created from open generic methods via MakeGenericMethod + CreateDelegate. + /// Used to avoid repeated reflection overhead when invoking generic methods with + /// runtime-determined type arguments. + /// + /// The cache is bounded in practice by the finite set of .NET types used as column types + /// or converter type pairs, so unbounded growth is not a concern. + /// + internal static class ReflectionDelegateCache where TDelegate : Delegate + { + private static readonly ConcurrentDictionary Cache = new(); + + /// + /// Returns a cached delegate for the given key, or creates one by calling + /// MakeGenericMethod on with type arguments + /// derived from the key via . + /// + public static TDelegate GetOrAdd(TKey key, MethodInfo openGenericMethod, Func getTypeArgs) + { + return Cache.GetOrAdd(key, k => + { + var method = openGenericMethod.MakeGenericMethod(getTypeArgs(k)); + return (TDelegate)method.CreateDelegate(typeof(TDelegate)); + }); + } + } +} diff --git a/src/Cassandra/Mapping/MapperFactory.cs b/src/Cassandra/Mapping/MapperFactory.cs index 42ad8d3f8..7b346ba5f 100644 --- a/src/Cassandra/Mapping/MapperFactory.cs +++ b/src/Cassandra/Mapping/MapperFactory.cs @@ -452,7 +452,7 @@ public object AdaptValue(PocoData pocoData, PocoColumn column, object value) // (TColumn) poco.SomeFieldOrProp return Convert.ChangeType(value, column.ColumnType); } - return converter.DynamicInvoke(value); + return TypeConverter.SafeInvokeConverter(converter, value, column.MemberInfoType, column.ColumnType); } /// diff --git a/src/Cassandra/Mapping/TypeConversion/TypeConverter.cs b/src/Cassandra/Mapping/TypeConversion/TypeConverter.cs index ee2c0317e..3c74f995c 100644 --- a/src/Cassandra/Mapping/TypeConversion/TypeConverter.cs +++ b/src/Cassandra/Mapping/TypeConversion/TypeConverter.cs @@ -22,6 +22,7 @@ using System.Linq.Expressions; using System.Reflection; using Cassandra.Collections; +using Cassandra.Helpers; namespace Cassandra.Mapping.TypeConversion { @@ -89,6 +90,10 @@ public abstract class TypeConverter private static readonly MethodInfo ConvertIDictionaryToDbTypeMethod = typeof(TypeConverter) .GetTypeInfo().GetMethod(nameof(ConvertIDictionaryToDbType), PrivateInstance); + private static readonly MethodInfo InvokeConverterMethod = typeof(TypeConverter) + .GetMethod(nameof(InvokeConverter), BindingFlags.NonPublic | BindingFlags.Static) + ?? throw new InvalidOperationException("InvokeConverter method not found via reflection."); + private readonly ConcurrentDictionary, Delegate> _fromDbConverterCache; private readonly ConcurrentDictionary, Delegate> _toDbConverterCache; @@ -141,7 +146,7 @@ internal object ConvertToDbFromUdtFieldValue(Type valueType, Type dbType, object throw new InvalidTypeException($"No converter is available from Type {valueType} is not convertible to type {dbType}"); } - return converter.DynamicInvoke(value); + return SafeInvokeConverter(converter, value, valueType, dbType); } /// @@ -155,7 +160,7 @@ internal object ConvertToUdtFieldFromDbValue(Type dbType, Type valueType, object throw new InvalidTypeException($"No converter is available from Type {dbType} is not convertible to type {valueType}"); } - return converter.DynamicInvoke(value); + return SafeInvokeConverter(converter, value, dbType, valueType); } /// @@ -242,7 +247,7 @@ private Delegate FindFromDbConverter() return null; } - Func mapper = d => d == null ? default(TPoco) : (TPoco)deleg.DynamicInvoke(d); + Func mapper = d => d == null ? default(TPoco) : (TPoco)SafeInvokeConverter(deleg, d, typeof(TDatabase), underlyingType); return mapper; } } @@ -451,7 +456,7 @@ private Delegate FindToDbConverter() { if (d != null) { - return (TDatabase)deleg.DynamicInvoke(d); + return (TDatabase)SafeInvokeConverter(deleg, d, underlyingType, typeof(TDatabase)); } if (default(TDatabase) != null) @@ -722,6 +727,53 @@ private TResult[] ConvertToArrayFromDb(IEnumerable li /// The Type expected by C* for the database column. /// A Func that can converter between the two Types or null if one is not available. protected abstract Func GetUserDefinedToDbConverter(); + + /// + /// Invokes a converter delegate without using DynamicInvoke. + /// DynamicInvoke may fail on .NET 9+ for certain dynamically compiled delegates, + /// and is slower than a properly-typed invocation even for pre-built delegates. + /// + internal static object SafeInvokeConverter(Delegate converter, object value, Type sourceType, Type destType) + { + var invoker = ReflectionDelegateCache<(Type, Type), Func>.GetOrAdd( + (sourceType, destType), InvokeConverterMethod, key => new[] { key.Item1, key.Item2 }); + return invoker(converter, value); + } + + private static readonly System.Runtime.CompilerServices.ConditionalWeakTable ReboundDelegateCache = new(); + + private static object InvokeConverter(Delegate del, object value) + { + if (del is Func func) + { + return func((TSource)value); + } + + // Fallback for user-defined converters that return a non-Func delegate type. + // Re-bind the delegate's method as a proper Func so that we + // avoid both DynamicInvoke and Method.Invoke, which can fail on .NET 9+ for + // dynamically compiled delegates. The result is cached so re-binding happens + // only once per delegate instance. + var cached = (Func)ReboundDelegateCache.GetValue(del, d => + { + try + { + return d.Target != null + ? (Func)Delegate.CreateDelegate(typeof(Func), d.Target, d.Method) + : (Func)Delegate.CreateDelegate(typeof(Func), d.Method); + } + catch (ArgumentException ex) + { + throw new InvalidOperationException( + $"Failed to invoke type converter from {typeof(TSource)} to {typeof(TDest)}. " + + $"The delegate type '{d.GetType()}' (declaring type: {d.Method.DeclaringType}) " + + $"could not be re-bound as Func<{typeof(TSource).Name}, {typeof(TDest).Name}>. " + + "Ensure that user-defined type converters return a compatible Func delegate.", + ex); + } + }); + return cached((TSource)value); + } } internal static class ReflectionUtils diff --git a/src/Cassandra/SSLOptions.cs b/src/Cassandra/SSLOptions.cs index b2aef4ea8..c72c4aac1 100644 --- a/src/Cassandra/SSLOptions.cs +++ b/src/Cassandra/SSLOptions.cs @@ -29,7 +29,7 @@ public class SSLOptions { private readonly static Logger _logger = new Logger(typeof(SSLOptions)); private RemoteCertificateValidationCallback _remoteCertValidationCallback = ValidateServerCertificate; - private SslProtocols _sslProtocol = SslProtocols.Tls; + private SslProtocols _sslProtocol = SslProtocols.None; private bool _checkCertificateRevocation; private X509CertificateCollection _certificateCollection = new X509CertificateCollection(); private Func _hostNameResolver = GetHostName; @@ -84,7 +84,7 @@ public SSLOptions() /// /// Creates SSL options used for SSL connections with Casandra hosts. /// - /// type of SSL protocol, default set to Tls. + /// type of SSL protocol, default set to None (OS-negotiated, typically TLS 1.2+). /// specifies whether the certificate revocation list is checked during connection authentication. /// verifies Cassandra host SSL certificate used for authentication. /// diff --git a/src/Cassandra/Utils.cs b/src/Cassandra/Utils.cs index 00fdb3738..2ce9e5e30 100644 --- a/src/Cassandra/Utils.cs +++ b/src/Cassandra/Utils.cs @@ -29,31 +29,6 @@ namespace Cassandra { internal static class Utils { - /// - /// Reads exactly the specified number of bytes from the stream. - /// Throws an exception if the stream ends before reading all requested bytes. - /// - /// After deprecation of net6 to be replaced with Stream.ReadExactly - /// - internal static void ReadExactly(Stream stream, byte[] buffer, int offset, int count) - { - if (count == 0) - { - return; - } - - int totalBytesRead = 0; - while (totalBytesRead < count) - { - var bytesRead = stream.Read(buffer, offset + totalBytesRead, count - totalBytesRead); - if (bytesRead == 0) - { - throw new IOException($"Unexpected end of stream. Expected to read {count} bytes but only read {totalBytesRead} bytes."); - } - totalBytesRead += bytesRead; - } - } - public static string ConvertToCqlMap(IDictionary source) { var sb = new StringBuilder("{"); @@ -213,7 +188,7 @@ public static byte[] ReadAllBytes(Stream stream, int position) { var buffer = new byte[stream.Length - position]; stream.Position = position; - ReadExactly(stream, buffer, 0, buffer.Length - position); + stream.ReadExactly(buffer, 0, buffer.Length - position); return buffer; } @@ -228,7 +203,7 @@ public static byte[] ReadAllBytes(IEnumerable streamList, long totalLeng { stream.Position = 0; var itemLength = (int)stream.Length; - ReadExactly(stream, buffer, offset, itemLength); + stream.ReadExactly(buffer, offset, itemLength); offset += itemLength; } return buffer; diff --git a/src/Extensions/Cassandra.AppMetrics/Cassandra.AppMetrics.csproj b/src/Extensions/Cassandra.AppMetrics/Cassandra.AppMetrics.csproj index 41442cafd..96a1f1467 100644 --- a/src/Extensions/Cassandra.AppMetrics/Cassandra.AppMetrics.csproj +++ b/src/Extensions/Cassandra.AppMetrics/Cassandra.AppMetrics.csproj @@ -6,7 +6,7 @@ 3.99.0.0 3.22.0.2 DataStax and ScyllaDB - netstandard2.0 + net8.0;net9.0 DataStax and ScyllaDB true true @@ -21,7 +21,7 @@ LICENSE.md https://github.com/scylladb/csharp-driver https://github.com/scylladb/csharp-driver - 7.1 + latest diff --git a/src/Extensions/Cassandra.OpenTelemetry/Cassandra.OpenTelemetry.csproj b/src/Extensions/Cassandra.OpenTelemetry/Cassandra.OpenTelemetry.csproj index 3fc236c2c..b14de7f23 100644 --- a/src/Extensions/Cassandra.OpenTelemetry/Cassandra.OpenTelemetry.csproj +++ b/src/Extensions/Cassandra.OpenTelemetry/Cassandra.OpenTelemetry.csproj @@ -6,7 +6,7 @@ 3.99.0.0 3.22.0.2 DataStax and ScyllaDB - netstandard2.0 + net8.0;net9.0 true true Cassandra.OpenTelemetry