This is an automated email from the ASF dual-hosted git repository.
ptupitsyn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new b8d1554757e IGNITE-22131 .NET: Implement ADO.NET classes (#6631)
b8d1554757e is described below
commit b8d1554757e6e597b1074e097865ec7694b25614
Author: Pavel Tupitsyn <[email protected]>
AuthorDate: Mon Sep 22 16:20:13 2025 +0300
IGNITE-22131 .NET: Implement ADO.NET classes (#6631)
Implement essential
[ADO.NET](https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/ado-net-overview)
classes:
* `IgniteDbColumn`
* `IgniteDbCommand`
* `IgniteDbConnection.cs`
* `IgniteDbConnectionStringBuilder.cs`
* `IgniteDbDataReader.cs`
* `IgniteDbException.cs`
* `IgniteDbParameter.cs`
* `IgniteDbParameterCollection.cs`
* `IgniteDbTransaction.cs`
---
.../Sql/IgniteDbCommandTests.cs | 245 +++++++++++++++++++
.../Sql/IgniteDbConnectionStringBuilderTests.cs | 163 +++++++++++++
.../Sql/IgniteDbConnectionTests.cs | 123 ++++++++++
.../Sql/IgniteDbParameterCollectionTests.cs | 220 +++++++++++++++++
.../Sql/IgniteDbParameterTests.cs | 64 +++++
.../Sql/IgniteDbTransactionTests.cs | 137 +++++++++++
.../dotnet/Apache.Ignite.Tests/ToStringTests.cs | 8 +-
.../Apache.Ignite/IgniteClientConfiguration.cs | 7 +-
.../dotnet/Apache.Ignite/Sql/IgniteDbCommand.cs | 264 +++++++++++++++++++++
.../dotnet/Apache.Ignite/Sql/IgniteDbConnection.cs | 172 ++++++++++++++
.../Sql/IgniteDbConnectionStringBuilder.cs | 189 +++++++++++++++
.../dotnet/Apache.Ignite/Sql/IgniteDbException.cs | 55 +++++
.../dotnet/Apache.Ignite/Sql/IgniteDbParameter.cs | 86 +++++++
.../Sql/IgniteDbParameterCollection.cs | 191 +++++++++++++++
.../Apache.Ignite/Sql/IgniteDbTransaction.cs | 93 ++++++++
15 files changed, 2013 insertions(+), 4 deletions(-)
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbCommandTests.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbCommandTests.cs
new file mode 100644
index 00000000000..0dac199dd0b
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbCommandTests.cs
@@ -0,0 +1,245 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+namespace Apache.Ignite.Tests.Sql;
+
+using System;
+using System.Data.Common;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+using Ignite.Sql;
+using NUnit.Framework;
+
+/// <summary>
+/// Tests for <see cref="IgniteDbCommand"/>.
+/// </summary>
+[SuppressMessage("ReSharper", "MethodHasAsyncOverload", Justification =
"Tests.")]
+public class IgniteDbCommandTests : IgniteTestsBase
+{
+ private const string TestTable = nameof(IgniteDbCommandTests);
+
+ [TearDown]
+ public async Task DropTestTable() =>
+ await Client.Sql.ExecuteScriptAsync("DROP TABLE IF EXISTS " +
TestTable);
+
+ [Test]
+ public async Task TestSelect()
+ {
+ await using var conn = new IgniteDbConnection(null);
+ conn.Open(Client);
+
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = "SELECT 1";
+
+ await using var reader = cmd.ExecuteReader();
+ Assert.IsTrue(await reader.ReadAsync());
+ Assert.AreEqual(1, reader.GetInt32(0));
+ Assert.IsFalse(await reader.ReadAsync());
+ }
+
+ [Test]
+ public async Task TestSelectWithParameter()
+ {
+ await using var conn = new IgniteDbConnection(null);
+ conn.Open(Client);
+
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = "SELECT ?";
+
+ var param = cmd.CreateParameter();
+ param.Value = 42;
+ cmd.Parameters.Add(param);
+
+ await using var reader = cmd.ExecuteReader();
+ Assert.IsTrue(await reader.ReadAsync());
+ Assert.AreEqual(42, reader.GetInt32(0));
+ Assert.IsFalse(await reader.ReadAsync());
+ }
+
+ [Test]
+ public async Task TestExecuteScalar()
+ {
+ await using var conn = new IgniteDbConnection(null);
+ conn.Open(Client);
+
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = "SELECT 1";
+
+ var result = cmd.ExecuteScalar();
+ Assert.AreEqual(1, result);
+ }
+
+ [Test]
+ public async Task TestPrepareNotSupported()
+ {
+ await using var conn = new IgniteDbConnection(null);
+ conn.Open(Client);
+
+ using var cmd = conn.CreateCommand();
+ cmd.CommandText = "SELECT 1";
+
+ Assert.Throws<NotSupportedException>(() => cmd.Prepare());
+ }
+
+ [Test]
+ public async Task TestDdl()
+ {
+ await using var conn = new IgniteDbConnection(null);
+ conn.Open(Client);
+
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = $"CREATE TABLE {TestTable} (id INT PRIMARY KEY, val
VARCHAR)";
+
+ var result = cmd.ExecuteNonQuery();
+ Assert.AreEqual(1, result);
+ }
+
+ [Test]
+ public async Task TestDdlWithTxThrows()
+ {
+ await using var conn = new IgniteDbConnection(null);
+ conn.Open(Client);
+
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = $"CREATE TABLE {TestTable} (id INT PRIMARY KEY, val
VARCHAR)";
+
+ await using var transaction = await conn.BeginTransactionAsync();
+ cmd.Transaction = transaction;
+
+ var ex = Assert.CatchAsync<DbException>(async () => await
cmd.ExecuteNonQueryAsync());
+ Assert.AreEqual("DDL doesn't support transactions.", ex?.Message);
+ }
+
+ [Test]
+ public async Task TestDml([Values(true, false)] bool tx)
+ {
+ await Client.Sql.ExecuteScriptAsync($"DELETE FROM {TableName}");
+
+ await using var conn = new IgniteDbConnection(null);
+ conn.Open(Client);
+
+ await using var cmd = conn.CreateCommand();
+
+ cmd.CommandText = $"INSERT INTO {TableName} (key, val) VALUES (?, ?)";
+ cmd.Parameters.Add(new IgniteDbParameter { Value = 1 });
+ cmd.Parameters.Add(new IgniteDbParameter { Value = "dml1" });
+
+ await using var transaction = tx ? await conn.BeginTransactionAsync()
: null;
+ cmd.Transaction = transaction;
+
+ var result = cmd.ExecuteNonQuery();
+ Assert.AreEqual(1, result); // One row inserted
+
+ if (tx)
+ {
+ // Not visible outside the transaction.
+ Assert.IsFalse(await TupleView.ContainsKeyAsync(null,
GetTuple(1)));
+ transaction?.Commit();
+ }
+
+ Assert.AreEqual("dml1", (await TupleView.GetAsync(null,
GetTuple(1))).Value["val"]);
+ }
+
+ [Test]
+ public async Task TestTimeout()
+ {
+ using var server = new FakeServer();
+ using var client = await server.ConnectClientAsync();
+
+ await using var conn = new IgniteDbConnection(null);
+ conn.Open(client);
+
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = "SELECT_FOO";
+ cmd.CommandTimeout = 123;
+
+ await using var reader = await cmd.ExecuteReaderAsync();
+
+ Assert.AreEqual(123_000, server.LastSqlTimeoutMs);
+ }
+
+ [Test]
+ public async Task TestExecuteScalarException()
+ {
+ await using var conn = new IgniteDbConnection(null);
+ conn.Open(Client);
+
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = "SELECT * FROM NON_EXISTENT_TABLE";
+
+ var ex = Assert.Catch<DbException>(() => cmd.ExecuteScalar());
+ StringAssert.StartsWith("Failed to validate query", ex.Message);
+ }
+
+ [Test]
+ public async Task TestExecuteNonQueryException()
+ {
+ await using var conn = new IgniteDbConnection(null);
+ conn.Open(Client);
+
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = "INSERT INTO NON_EXISTENT_TABLE (id) VALUES (1)";
+
+ var ex = Assert.Catch<DbException>(() => cmd.ExecuteNonQuery());
+ StringAssert.StartsWith("Failed to validate query", ex.Message);
+ }
+
+ [Test]
+ public async Task TestExecuteReaderException()
+ {
+ await using var conn = new IgniteDbConnection(null);
+ conn.Open(Client);
+
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = "SELECT * FROM NON_EXISTENT_TABLE";
+
+ var ex = Assert.Catch<DbException>(() => cmd.ExecuteReader());
+ StringAssert.StartsWith("Failed to validate query", ex.Message);
+ }
+
+ [Test]
+ public async Task TestCancel()
+ {
+ await using var conn = new IgniteDbConnection(null);
+ conn.Open(Client);
+
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = "SELECT 1 UNION ALL SELECT 1";
+
+ cmd.Cancel();
+ Assert.ThrowsAsync<OperationCanceledException>(async () => await
cmd.ExecuteReaderAsync());
+ }
+
+ [Test]
+ public async Task TestToString()
+ {
+ await using var conn = new IgniteDbConnection(null);
+ conn.Open(Client);
+
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = "SELECT 1";
+ cmd.CommandTimeout = 123;
+ cmd.Transaction = await conn.BeginTransactionAsync();
+ ((IgniteDbCommand)cmd).PageSize = 321;
+
+ var str = cmd.ToString();
+ StringAssert.StartsWith(
+ "IgniteDbCommand { CommandText = SELECT 1, CommandTimeout = 123,
PageSize = 321, " +
+ "Transaction = IgniteDbTransaction { IgniteTransaction =
Transaction { Id =",
+ str);
+ }
+}
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbConnectionStringBuilderTests.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbConnectionStringBuilderTests.cs
new file mode 100644
index 00000000000..a2ccb31e012
--- /dev/null
+++
b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbConnectionStringBuilderTests.cs
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+namespace Apache.Ignite.Tests.Sql;
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Ignite.Sql;
+using NUnit.Framework;
+
+public class IgniteDbConnectionStringBuilderTests
+{
+ [Test]
+ public void TestParseFullConnectionString()
+ {
+ var connStr =
+
"Endpoints=localhost:10800,localhost:10801;SocketTimeout=00:00:02.5000000;OperationTimeout=00:01:14.0700000;"
+
+
"HeartbeatInterval=00:00:01.3640000;ReconnectInterval=00:00:00.5432100;SslEnabled=True;Username=user1;Password=hunter2";
+
+ var builder = new IgniteDbConnectionStringBuilder(connStr);
+
+ CollectionAssert.AreEquivalent(new[] {"localhost:10800",
"localhost:10801"}, builder.Endpoints);
+ Assert.AreEqual(TimeSpan.FromSeconds(2.5), builder.SocketTimeout);
+ Assert.AreEqual(TimeSpan.FromMinutes(1.2345),
builder.OperationTimeout);
+ Assert.AreEqual(TimeSpan.FromSeconds(1.364),
builder.HeartbeatInterval);
+ Assert.AreEqual(TimeSpan.FromSeconds(0.54321),
builder.ReconnectInterval);
+ Assert.IsTrue(builder.SslEnabled);
+ Assert.AreEqual("user1", builder.Username);
+ Assert.AreEqual("hunter2", builder.Password);
+
+ Assert.AreEqual(connStr.ToLowerInvariant(),
builder.ToString().ToLowerInvariant());
+
+ IgniteClientConfiguration clientConfig =
builder.ToIgniteClientConfiguration();
+
+ CollectionAssert.AreEquivalent(new[] {"localhost:10800",
"localhost:10801"}, clientConfig.Endpoints);
+ Assert.AreEqual(TimeSpan.FromSeconds(2.5), clientConfig.SocketTimeout);
+ Assert.AreEqual(TimeSpan.FromMinutes(1.2345),
clientConfig.OperationTimeout);
+ Assert.AreEqual(TimeSpan.FromSeconds(1.364),
clientConfig.HeartbeatInterval);
+ Assert.AreEqual(TimeSpan.FromSeconds(0.54321),
clientConfig.ReconnectInterval);
+ Assert.IsNotNull(clientConfig.SslStreamFactory);
+ Assert.AreEqual("user1",
((BasicAuthenticator)clientConfig.Authenticator!).Username);
+ Assert.AreEqual("hunter2",
((BasicAuthenticator)clientConfig.Authenticator!).Password);
+ }
+
+ [Test]
+ public void TestParseMinimalConnectionString()
+ {
+ var connStr = "endpoints=foobar:1234";
+
+ var builder = new IgniteDbConnectionStringBuilder(connStr);
+
+ CollectionAssert.AreEquivalent(new[] {"foobar:1234"},
builder.Endpoints);
+ Assert.AreEqual(IgniteClientConfiguration.DefaultSocketTimeout,
builder.SocketTimeout);
+ Assert.AreEqual(IgniteClientConfiguration.DefaultOperationTimeout,
builder.OperationTimeout);
+ Assert.AreEqual(IgniteClientConfiguration.DefaultHeartbeatInterval,
builder.HeartbeatInterval);
+ Assert.AreEqual(IgniteClientConfiguration.DefaultReconnectInterval,
builder.ReconnectInterval);
+ Assert.IsFalse(builder.SslEnabled);
+ Assert.IsNull(builder.Username);
+ Assert.IsNull(builder.Password);
+
+ Assert.AreEqual(connStr, builder.ToString());
+
+ IgniteClientConfiguration clientConfig =
builder.ToIgniteClientConfiguration();
+
+ CollectionAssert.AreEquivalent(new[] {"foobar:1234"},
clientConfig.Endpoints);
+ Assert.AreEqual(IgniteClientConfiguration.DefaultSocketTimeout,
clientConfig.SocketTimeout);
+ Assert.AreEqual(IgniteClientConfiguration.DefaultOperationTimeout,
clientConfig.OperationTimeout);
+ Assert.AreEqual(IgniteClientConfiguration.DefaultHeartbeatInterval,
clientConfig.HeartbeatInterval);
+ Assert.AreEqual(IgniteClientConfiguration.DefaultReconnectInterval,
clientConfig.ReconnectInterval);
+ Assert.IsNull(clientConfig.SslStreamFactory);
+ Assert.IsNull(clientConfig.Authenticator);
+ }
+
+ [Test]
+ public void TestToStringBuildsFullConnectionString()
+ {
+ var builder = new IgniteDbConnectionStringBuilder
+ {
+ Endpoints = ["localhost:10800", "localhost:10801"],
+ SocketTimeout = TimeSpan.FromSeconds(2.5),
+ OperationTimeout = TimeSpan.FromMinutes(1.2345),
+ HeartbeatInterval = TimeSpan.FromSeconds(1.364),
+ ReconnectInterval = TimeSpan.FromSeconds(0.54321),
+ SslEnabled = true,
+ Username = "user1",
+ Password = "hunter2"
+ };
+
+ Assert.AreEqual(
+
"Endpoints=localhost:10800,localhost:10801;SocketTimeout=00:00:02.5000000;OperationTimeout=00:01:14.0700000;"
+
+
"HeartbeatInterval=00:00:01.3640000;ReconnectInterval=00:00:00.5432100;SslEnabled=True;Username=user1;Password=hunter2",
+ builder.ToString());
+ }
+
+ [Test]
+ public void TestToStringBuildsMinimalConnectionString()
+ {
+ var builder = new IgniteDbConnectionStringBuilder
+ {
+ Endpoints = ["foo:123"]
+ };
+
+ Assert.AreEqual("Endpoints=foo:123", builder.ToString());
+ }
+
+ [Test]
+ public void TestBuilderHasSamePropertiesAsClientConfig()
+ {
+ var builderProps =
typeof(IgniteDbConnectionStringBuilder).GetProperties();
+ var configProps = typeof(IgniteClientConfiguration).GetProperties();
+
+ foreach (var configProp in configProps)
+ {
+ if (configProp.Name is
nameof(IgniteClientConfiguration.LoggerFactory)
+ or nameof(IgniteClientConfiguration.RetryPolicy)
+ or nameof(IgniteClientConfiguration.Authenticator)
+ or nameof(IgniteClientConfiguration.SslStreamFactory))
+ {
+ // Not supported yet.
+ continue;
+ }
+
+ var builderProp = builderProps.SingleOrDefault(x => x.Name ==
configProp.Name);
+ Assert.NotNull(builderProp, $"Property '{configProp.Name}' not
found in IgniteDbConnectionStringBuilder");
+ Assert.AreEqual(configProp.PropertyType,
builderProp!.PropertyType, $"Property '{configProp.Name}' type mismatch");
+ }
+ }
+
+ [Test]
+ [SuppressMessage("ReSharper", "ObjectCreationAsStatement", Justification =
"Tests")]
+ public void TestUnknownConnectionStringPropertyThrows()
+ {
+ var connStr = "foo=bar;Endpoints=localhost:10800";
+
+ var ex = Assert.Throws<ArgumentException>(() => new
IgniteDbConnectionStringBuilder(connStr));
+ Assert.AreEqual("Unknown connection string key: 'foo'. (Parameter
'keyword')", ex.Message);
+ }
+
+ [Test]
+ [SuppressMessage("ReSharper", "CollectionNeverQueried.Local",
Justification = "Tests")]
+ public void TestUnknownSetterPropertyThrows()
+ {
+ var builder = new IgniteDbConnectionStringBuilder();
+
+ var ex = Assert.Throws<ArgumentException>(() => builder["baz"] =
"bar");
+ Assert.AreEqual("Unknown connection string key: 'baz'. (Parameter
'keyword')", ex.Message);
+ }
+}
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbConnectionTests.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbConnectionTests.cs
new file mode 100644
index 00000000000..ae942c0aaff
--- /dev/null
+++
b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbConnectionTests.cs
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+namespace Apache.Ignite.Tests.Sql;
+
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using Ignite.Sql;
+using NUnit.Framework;
+
+public class IgniteDbConnectionTests : IgniteTestsBase
+{
+ [Test]
+ public async Task TestOpenClose()
+ {
+ var connectionString = $"Endpoints={GetConfig().Endpoints.First()}";
+ await using var conn = new IgniteDbConnection(connectionString);
+ Assert.AreEqual(ConnectionState.Closed, conn.State);
+
+ await conn.OpenAsync();
+ Assert.AreEqual(ConnectionState.Open, conn.State);
+ Assert.AreEqual("3.x", conn.ServerVersion);
+ Assert.AreEqual(string.Empty, conn.DataSource);
+ Assert.AreEqual(string.Empty, conn.Database);
+ Assert.AreEqual(connectionString, conn.ConnectionString);
+ Assert.IsNotNull(conn.Client);
+
+ await conn.CloseAsync();
+ Assert.AreEqual(ConnectionState.Closed, conn.State);
+ }
+
+ [Test]
+ public async Task TestExistingClient([Values(true, false)] bool ownsClient)
+ {
+ using var client = await IgniteClient.StartAsync(GetConfig());
+
+ await using var conn = new IgniteDbConnection(null);
+ Assert.AreEqual(ConnectionState.Closed, conn.State);
+
+ conn.Open(client, ownsClient);
+ Assert.AreEqual(ConnectionState.Open, conn.State);
+
+ await conn.CloseAsync();
+ Assert.AreEqual(ConnectionState.Closed, conn.State);
+
+ if (ownsClient)
+ {
+ Assert.That(client.GetConnections(), Is.Empty, "Client should be
closed after connection is closed with ownsClient: true");
+ }
+ else
+ {
+ Assert.That(client.GetConnections(), Is.Not.Empty, "Client should
be open after connection is closed with ownsClient: false");
+ }
+ }
+
+ [Test]
+ public void TestConnectionStringProperty()
+ {
+ var connectionString = $"Endpoints={GetConfig().Endpoints.First()}";
+ using var conn = new IgniteDbConnection(null);
+ Assert.AreEqual(string.Empty, conn.ConnectionString);
+ conn.ConnectionString = connectionString;
+ Assert.AreEqual(ConnectionState.Closed, conn.State);
+
+ conn.Open();
+ Assert.AreEqual(connectionString, conn.ConnectionString);
+ Assert.IsNotNull(conn.Client);
+
+ conn.Close();
+ Assert.AreEqual(ConnectionState.Closed, conn.State);
+ }
+
+ [Test]
+ public async Task TestCommand([Values(true, false)] bool withTx)
+ {
+ await using var conn = new IgniteDbConnection(null);
+ Assert.AreEqual(ConnectionState.Closed, conn.State);
+
+ conn.Open(Client, ownsClient: false);
+ Assert.AreEqual(ConnectionState.Open, conn.State);
+
+ await using var cmd = conn.CreateCommand();
+ Assert.IsNotNull(cmd);
+ Assert.AreEqual(conn, cmd.Connection);
+ Assert.AreEqual(CommandType.Text, cmd.CommandType);
+ Assert.AreEqual(0, cmd.CommandTimeout);
+ Assert.IsNull(cmd.Transaction);
+
+ await using var tx = withTx ? await
conn.BeginTransactionAsync(IsolationLevel.Serializable) : null;
+ cmd.Transaction = tx;
+ Assert.AreEqual(tx, cmd.Transaction);
+
+ cmd.CommandText = "SELECT 1";
+ var scalar = await cmd.ExecuteScalarAsync();
+ Assert.AreEqual(1, scalar);
+ }
+
+ [Test]
+ public void TestToString()
+ {
+ using var conn = new IgniteDbConnection(null);
+ conn.ConnectionString = "foo_bar";
+
+ Assert.AreEqual(
+ "IgniteDbConnection { ConnectionString = foo_bar, State = Closed,
ServerVersion = 3.x, Client = }",
+ conn.ToString());
+ }
+}
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbParameterCollectionTests.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbParameterCollectionTests.cs
new file mode 100644
index 00000000000..9d18f08c756
--- /dev/null
+++
b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbParameterCollectionTests.cs
@@ -0,0 +1,220 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+namespace Apache.Ignite.Tests.Sql;
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Ignite.Sql;
+using NUnit.Framework;
+
+/// <summary>
+/// Tests for <see cref="IgniteDbParameterCollection"/>.
+/// </summary>
+public class IgniteDbParameterCollectionTests
+{
+ [Test]
+ public void TestAddAndRetrieve()
+ {
+ var col = new IgniteDbParameterCollection();
+ var param = new IgniteDbParameter { ParameterName = "p1" };
+
+ col.Add(param);
+
+ Assert.AreEqual(1, col.Count);
+ Assert.AreSame(param, col[0]);
+ Assert.AreSame(param, col["p1"]);
+ }
+
+ [Test]
+ public void TestRemove()
+ {
+ var col = new IgniteDbParameterCollection();
+ var param = new IgniteDbParameter();
+
+ col.Add((object)param);
+ col.Remove((object)param);
+
+ Assert.AreEqual(0, col.Count);
+ }
+
+ [Test]
+ public void TestContains()
+ {
+ var col = new IgniteDbParameterCollection();
+ var param = new IgniteDbParameter { ParameterName = "p1" };
+
+ col.Add(param);
+
+ Assert.IsTrue(col.Contains(param));
+ Assert.IsTrue(col.Contains((object)param));
+ Assert.IsTrue(col.Contains("p1"));
+ }
+
+ [Test]
+ public void TestClear()
+ {
+ var col = new IgniteDbParameterCollection
+ {
+ new IgniteDbParameter(),
+ new IgniteDbParameter()
+ };
+
+ col.Clear();
+
+ Assert.AreEqual(0, col.Count);
+ }
+
+ [Test]
+ public void TestIndexOf()
+ {
+ var col = new IgniteDbParameterCollection();
+ var param1 = new IgniteDbParameter { ParameterName = "p1" };
+ var param2 = new IgniteDbParameter { ParameterName = "p2" };
+
+ col.Add(param1);
+ col.Add(param2);
+
+ Assert.AreEqual(0, col.IndexOf(param1));
+ Assert.AreEqual(0, col.IndexOf((object)param1));
+ Assert.AreEqual(1, col.IndexOf("p2"));
+ }
+
+ [Test]
+ public void TestAddRange()
+ {
+ var col = new IgniteDbParameterCollection();
+ var param1 = new IgniteDbParameter { ParameterName = "p1" };
+ var param2 = new IgniteDbParameter { ParameterName = "p2" };
+
+ col.AddRange(new[] { param1, param2 });
+
+ Assert.AreEqual(2, col.Count);
+ Assert.AreSame(param1, col[0]);
+ Assert.AreSame(param2, col[1]);
+ }
+
+ [Test]
+ public void TestInsertAndRemoveAt()
+ {
+ var col = new IgniteDbParameterCollection();
+ var param1 = new IgniteDbParameter { ParameterName = "p1" };
+ var param2 = new IgniteDbParameter { ParameterName = "p2" };
+
+ col.Add(param1);
+ col.Insert(0, param2);
+
+ Assert.AreSame(param2, col[0]);
+ Assert.AreSame(param1, col[1]);
+
+ col.RemoveAt(0);
+ Assert.AreSame(param1, col[0]);
+
+ col.RemoveAt("p1");
+ Assert.AreEqual(0, col.Count);
+ }
+
+ [Test]
+ public void TestSetAndGetParameter()
+ {
+ var col = new IgniteDbParameterCollection();
+ var param1 = new IgniteDbParameter { ParameterName = "p1" };
+ var param2 = new IgniteDbParameter { ParameterName = "p2" };
+ col.Add(param1);
+
+ // Set by index
+ col[0] = param2;
+ Assert.AreSame(param2, col[0]);
+
+ // Set by name
+ col.Add(param1);
+ var dbParam = new IgniteDbParameter { ParameterName = "p3" };
+ int idx = col.IndexOf("p2");
+ col[idx] = dbParam;
+ Assert.AreSame(dbParam, col[0]);
+
+ // Get by index and name
+ Assert.AreSame(dbParam, col[0]);
+ Assert.AreSame(dbParam, col[col.IndexOf("p3")]);
+ }
+
+ [Test]
+ public void TestCopyTo()
+ {
+ var col = new IgniteDbParameterCollection();
+ var param1 = new IgniteDbParameter { ParameterName = "p1" };
+ var param2 = new IgniteDbParameter { ParameterName = "p2" };
+
+ col.Add(param1);
+ col.Add(param2);
+
+ var arr = new IgniteDbParameter[2];
+ col.CopyTo(arr, 0);
+
+ Assert.AreSame(param1, arr[0]);
+ Assert.AreSame(param2, arr[1]);
+
+ var arr2 = new IgniteDbParameter[2];
+ ((ICollection)col).CopyTo(arr2, 0);
+
+ Assert.AreSame(param1, arr2[0]);
+ Assert.AreSame(param2, arr2[1]);
+ }
+
+ [Test]
+ public void TestEnumerator()
+ {
+ var collection = new IgniteDbParameterCollection();
+
+ var param1 = new IgniteDbParameter { ParameterName = "p1" };
+ var param2 = new IgniteDbParameter { ParameterName = "p2" };
+
+ collection.Add(param1);
+ collection.Add(param2);
+
+ var list = new List<IgniteDbParameter>();
+ foreach (var p in collection)
+ {
+ list.Add((IgniteDbParameter)p);
+ }
+
+ Assert.AreEqual(2, list.Count);
+ Assert.Contains(param1, list);
+ Assert.Contains(param2, list);
+ }
+
+ [Test]
+ public void TestExceptions()
+ {
+ var collection = new IgniteDbParameterCollection();
+ Assert.Throws<ArgumentOutOfRangeException>(() => { _ = collection[0];
});
+ Assert.Throws<InvalidOperationException>(() =>
collection.RemoveAt("notfound"));
+ }
+
+ [Test]
+ public void TestToString()
+ {
+ var col = new IgniteDbParameterCollection
+ {
+ new IgniteDbParameter { Value = 1 },
+ new IgniteDbParameter { Value = "x" }
+ };
+
+ Assert.AreEqual("IgniteDbParameterCollection { Count = 2 }",
col.ToString());
+ }
+}
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbParameterTests.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbParameterTests.cs
new file mode 100644
index 00000000000..2a7e767899d
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbParameterTests.cs
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+namespace Apache.Ignite.Tests.Sql;
+
+using System;
+using System.Data;
+using Ignite.Sql;
+using NUnit.Framework;
+
+public class IgniteDbParameterTests
+{
+ [Test]
+ public void TestDefaults()
+ {
+ var param = new IgniteDbParameter();
+ Assert.AreEqual(DbType.String, param.DbType);
+ Assert.AreEqual(ParameterDirection.Input, param.Direction);
+ Assert.IsFalse(param.IsNullable);
+ Assert.AreEqual(string.Empty, param.ParameterName);
+ Assert.AreEqual(string.Empty, param.SourceColumn);
+ Assert.IsNull(param.Value);
+ Assert.IsFalse(param.SourceColumnNullMapping);
+ Assert.AreEqual(0, param.Size);
+ }
+
+ [Test]
+ public void TestDirectionOnlyInputAllowed()
+ {
+ var param = new IgniteDbParameter();
+ Assert.AreEqual(ParameterDirection.Input, param.Direction);
+ Assert.Throws<ArgumentException>(() => param.Direction =
ParameterDirection.Output);
+ }
+
+ [Test]
+ public void TestResetDbTypeSetsString()
+ {
+ var param = new IgniteDbParameter { DbType = DbType.Boolean };
+ param.ResetDbType();
+ Assert.AreEqual(DbType.String, param.DbType);
+ }
+
+ [Test]
+ public void TestToString()
+ {
+ var param = new IgniteDbParameter { Value = 12.3 };
+
+ Assert.AreEqual("IgniteDbParameter { Value = 12.3 }",
param.ToString());
+ }
+}
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbTransactionTests.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbTransactionTests.cs
new file mode 100644
index 00000000000..59b9799eb37
--- /dev/null
+++
b/modules/platforms/dotnet/Apache.Ignite.Tests/Sql/IgniteDbTransactionTests.cs
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+namespace Apache.Ignite.Tests.Sql;
+
+using System.Threading.Tasks;
+using Ignite.Sql;
+using Ignite.Transactions;
+using Internal.Common;
+using NUnit.Framework;
+
+/// <summary>
+/// Tests for <see cref="IgniteDbTransaction"/>.
+/// </summary>
+public class IgniteDbTransactionTests
+{
+ [Test]
+ public void TestCommit()
+ {
+ var tx = new TestIgniteTx();
+ var dbTx = new IgniteDbTransaction(tx,
System.Data.IsolationLevel.ReadCommitted, null!);
+
+ Assert.AreEqual(System.Data.IsolationLevel.ReadCommitted,
dbTx.IsolationLevel);
+ Assert.AreSame(tx, dbTx.IgniteTransaction);
+
+ dbTx.Commit();
+
+ Assert.IsTrue(tx.IsCommitted);
+ Assert.IsFalse(tx.IsRolledback);
+ Assert.IsFalse(tx.IsDisposed);
+ }
+
+ [Test]
+ public void TestRollback()
+ {
+ var tx = new TestIgniteTx();
+ var dbTx = new IgniteDbTransaction(tx,
System.Data.IsolationLevel.ReadCommitted, null!);
+
+ dbTx.Rollback();
+
+ Assert.IsFalse(tx.IsCommitted);
+ Assert.IsTrue(tx.IsRolledback);
+ Assert.IsFalse(tx.IsDisposed);
+ }
+
+ [Test]
+ public async Task TestCommitAsync()
+ {
+ var tx = new TestIgniteTx();
+ var dbTx = new IgniteDbTransaction(tx,
System.Data.IsolationLevel.ReadCommitted, null!);
+
+ await dbTx.CommitAsync();
+
+ Assert.IsTrue(tx.IsCommitted);
+ Assert.IsFalse(tx.IsRolledback);
+ Assert.IsFalse(tx.IsDisposed);
+ }
+
+ [Test]
+ public async Task TestRollbackAsync()
+ {
+ var tx = new TestIgniteTx();
+ var dbTx = new IgniteDbTransaction(tx,
System.Data.IsolationLevel.ReadCommitted, null!);
+
+ await dbTx.RollbackAsync(string.Empty);
+
+ Assert.IsFalse(tx.IsCommitted);
+ Assert.IsTrue(tx.IsRolledback);
+ Assert.IsFalse(tx.IsDisposed);
+ }
+
+ [Test]
+ public void TestDispose()
+ {
+ var tx = new TestIgniteTx();
+ var dbTx = new IgniteDbTransaction(tx,
System.Data.IsolationLevel.ReadCommitted, null!);
+
+ dbTx.Dispose();
+
+ Assert.IsTrue(tx.IsDisposed);
+ }
+
+ [Test]
+ public void TestToString()
+ {
+ var dbTx = new IgniteDbTransaction(new TestIgniteTx(),
System.Data.IsolationLevel.ReadCommitted, null!);
+
+ Assert.AreEqual("IgniteDbTransaction { IgniteTransaction =
TestIgniteTx { }, Connection = }", dbTx.ToString());
+ }
+
+ private class TestIgniteTx : ITransaction
+ {
+ public bool IsDisposed { get; set; }
+
+ public bool IsCommitted { get; set; }
+
+ public bool IsRolledback { get; set; }
+
+ public bool IsReadOnly => false;
+
+ public ValueTask DisposeAsync()
+ {
+ IsDisposed = true;
+ return ValueTask.CompletedTask;
+ }
+
+ public void Dispose() => IsDisposed = true;
+
+ public Task CommitAsync()
+ {
+ IsCommitted = true;
+ return Task.CompletedTask;
+ }
+
+ public Task RollbackAsync()
+ {
+ IsRolledback = true;
+ return Task.CompletedTask;
+ }
+
+ public override string ToString() =>
IgniteToStringBuilder.Build(GetType());
+ }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/ToStringTests.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/ToStringTests.cs
index 5f17aee1a99..bad8788cdf8 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/ToStringTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/ToStringTests.cs
@@ -21,6 +21,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using Ignite.Sql;
using Ignite.Table;
using NUnit.Framework;
@@ -31,10 +32,11 @@ public class ToStringTests
{
private static readonly List<Type> PublicFacingTypes =
GetPublicFacingTypes().ToList();
- private static readonly HashSet<Type> ExcludedTypes = new()
- {
+ private static readonly HashSet<Type> ExcludedTypes =
+ [
typeof(RetryReadPolicy), // Inherits from RetryReadPolicy
- };
+ typeof(IgniteDbConnectionStringBuilder)
+ ];
[Test]
public void TestAllPublicFacingTypesHaveConsistentToString()
diff --git
a/modules/platforms/dotnet/Apache.Ignite/IgniteClientConfiguration.cs
b/modules/platforms/dotnet/Apache.Ignite/IgniteClientConfiguration.cs
index a4182daf033..3cb76b44c21 100644
--- a/modules/platforms/dotnet/Apache.Ignite/IgniteClientConfiguration.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/IgniteClientConfiguration.cs
@@ -41,6 +41,11 @@ namespace Apache.Ignite
/// </summary>
public static readonly TimeSpan DefaultSocketTimeout =
TimeSpan.FromSeconds(30);
+ /// <summary>
+ /// Default socket timeout.
+ /// </summary>
+ public static readonly TimeSpan DefaultOperationTimeout =
Timeout.InfiniteTimeSpan;
+
/// <summary>
/// Default heartbeat interval.
/// </summary>
@@ -117,7 +122,7 @@ namespace Apache.Ignite
/// which case the operation timeout is applied to each individual
network call.
/// </summary>
[DefaultValue(typeof(TimeSpan), "-00:00:00.001")]
- public TimeSpan OperationTimeout { get; set; } =
Timeout.InfiniteTimeSpan;
+ public TimeSpan OperationTimeout { get; set; } =
DefaultOperationTimeout;
/// <summary>
/// Gets endpoints to connect to.
diff --git a/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbCommand.cs
b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbCommand.cs
new file mode 100644
index 00000000000..5a8f2ef079b
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbCommand.cs
@@ -0,0 +1,264 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+namespace Apache.Ignite.Sql;
+
+using System;
+using System.Data;
+using System.Data.Common;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using System.Threading.Tasks;
+using Internal.Common;
+using NodaTime;
+using Table;
+using Transactions;
+
+/// <summary>
+/// Ignite database command.
+/// </summary>
+public sealed class IgniteDbCommand : DbCommand
+{
+ private readonly CancellationTokenSource _cancellationTokenSource = new();
+
+ private IgniteDbParameterCollection? _parameters;
+
+ private string _commandText = string.Empty;
+
+ /// <inheritdoc />
+ [AllowNull]
+ public override string CommandText
+ {
+ get => _commandText;
+ set => _commandText = value ?? string.Empty;
+ }
+
+ /// <inheritdoc />
+ public override int CommandTimeout { get; set; }
+
+ /// <summary>
+ /// Gets or sets the page size (number of rows fetched at a time by the
underlying Ignite data reader).
+ /// </summary>
+ public int PageSize { get; set; } = SqlStatement.DefaultPageSize;
+
+ /// <inheritdoc />
+ public override CommandType CommandType { get; set; } = CommandType.Text;
+
+ /// <inheritdoc />
+ public override UpdateRowSource UpdatedRowSource { get; set; }
+
+ /// <inheritdoc />
+ public override bool DesignTimeVisible { get; set; }
+
+ /// <summary>
+ /// Gets or sets the transaction within which the command executes.
+ /// </summary>
+ public IgniteDbTransaction? IgniteDbTransaction
+ {
+ get => (IgniteDbTransaction?)Transaction;
+ set => Transaction = value;
+ }
+
+ /// <inheritdoc />
+ protected override DbConnection? DbConnection { get; set; }
+
+ /// <inheritdoc />
+ protected override DbParameterCollection DbParameterCollection =>
+ _parameters ??= new IgniteDbParameterCollection();
+
+ /// <inheritdoc />
+ protected override DbTransaction? DbTransaction { get; set; }
+
+ /// <inheritdoc />
+ public override void Cancel() => _cancellationTokenSource.Cancel();
+
+ /// <inheritdoc />
+ public override int ExecuteNonQuery() =>
+ ExecuteNonQueryAsync(CancellationToken.None).GetAwaiter().GetResult();
+
+ /// <inheritdoc />
+ [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on
the awaited task", Justification = "False positive")]
+ public override async Task<int> ExecuteNonQueryAsync(CancellationToken
cancellationToken)
+ {
+ var args = GetArgs();
+ var statement = GetStatement();
+
+ using CancellationTokenSource linkedCts =
CancellationTokenSource.CreateLinkedTokenSource(
+ cancellationToken, _cancellationTokenSource.Token);
+
+ try
+ {
+ await using IResultSet<object> resultSet = await
GetSql().ExecuteAsync<object>(
+ transaction: GetIgniteTx(),
+ statement,
+ linkedCts.Token,
+ args).ConfigureAwait(false);
+
+ Debug.Assert(!resultSet.HasRowSet, "!resultSet.HasRowSet");
+
+ if (resultSet.AffectedRows < 0)
+ {
+ return resultSet.WasApplied ? 1 : 0;
+ }
+
+ return (int)resultSet.AffectedRows;
+ }
+ catch (SqlException sqlEx)
+ {
+ throw new IgniteDbException(sqlEx.Message, sqlEx);
+ }
+ }
+
+ /// <inheritdoc />
+ [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on
the awaited task", Justification = "False positive")]
+ public override async Task<object?> ExecuteScalarAsync(CancellationToken
cancellationToken)
+ {
+ var args = GetArgs();
+ var statement = GetStatement();
+
+ using CancellationTokenSource linkedCts =
CancellationTokenSource.CreateLinkedTokenSource(
+ cancellationToken, _cancellationTokenSource.Token);
+
+ try
+ {
+ await using IResultSet<IIgniteTuple> resultSet = await
GetSql().ExecuteAsync(
+ transaction: GetIgniteTx(),
+ statement,
+ linkedCts.Token,
+ args).ConfigureAwait(false);
+
+ await foreach (var row in resultSet)
+ {
+ // Return the first result.
+ return row[0];
+ }
+ }
+ catch (SqlException sqlEx)
+ {
+ throw new IgniteDbException(sqlEx.Message, sqlEx);
+ }
+
+ throw new IgniteDbException("Query returned no results: " + statement);
+ }
+
+ /// <inheritdoc />
+ public override object? ExecuteScalar() =>
ExecuteScalarAsync().GetAwaiter().GetResult();
+
+ /// <inheritdoc />
+ public override void Prepare() => throw new NotSupportedException("Prepare
is not supported.");
+
+ /// <inheritdoc/>
+ public override string ToString() =>
+ new IgniteToStringBuilder(GetType())
+ .Append(CommandText)
+ .Append(CommandTimeout)
+ .Append(PageSize)
+ .Append(Transaction)
+ .Append(Connection)
+ .Build();
+
+ /// <inheritdoc />
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (disposing)
+ {
+ _cancellationTokenSource.Dispose();
+ }
+ }
+
+ /// <inheritdoc />
+ protected override async Task<DbDataReader>
ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken
cancellationToken)
+ {
+ var args = GetArgs();
+ var statement = GetStatement();
+
+ // Can't create linked CTS here because the returned reader outlasts
this method.
+ if (cancellationToken == CancellationToken.None)
+ {
+ cancellationToken = _cancellationTokenSource.Token;
+ }
+
+ try
+ {
+ return await GetSql().ExecuteReaderAsync(
+ transaction: GetIgniteTx(),
+ statement,
+ cancellationToken,
+ args).ConfigureAwait(false);
+ }
+ catch (SqlException sqlEx)
+ {
+ throw new IgniteDbException(sqlEx.Message, sqlEx);
+ }
+ }
+
+ /// <inheritdoc />
+ protected override DbParameter CreateDbParameter() => new
IgniteDbParameter();
+
+ /// <inheritdoc />
+ protected override DbDataReader ExecuteDbDataReader(CommandBehavior
behavior) =>
+ ExecuteDbDataReaderAsync(behavior,
CancellationToken.None).GetAwaiter().GetResult();
+
+ private ISql GetSql()
+ {
+ if (DbConnection is not IgniteDbConnection igniteConn)
+ {
+ throw new InvalidOperationException("DbConnection is not an
IgniteConnection or is null.");
+ }
+
+ var client = igniteConn.Client
+ ?? throw new InvalidOperationException("Ignite client is
not initialized (connection is not open).");
+
+ return client.Sql;
+ }
+
+ private SqlStatement GetStatement() => new(CommandText)
+ {
+ PageSize = PageSize,
+ Timeout = TimeSpan.FromSeconds(CommandTimeout) // 0 means no timeout,
both in ADO.NET and Ignite.
+ };
+
+ private ITransaction? GetIgniteTx() =>
IgniteDbTransaction?.IgniteTransaction;
+
+ private object?[] GetArgs()
+ {
+ if (_parameters == null || _parameters.Count == 0)
+ {
+ return [];
+ }
+
+ var arr = new object?[_parameters.Count];
+
+ for (var i = 0; i < _parameters.Count; i++)
+ {
+ arr[i] = _parameters[i].Value switch
+ {
+ DBNull => null,
+ byte u8 => (sbyte)u8,
+ ushort u16 => (short)u16,
+ uint u32 => (int)u32,
+ DateTime dt => LocalDateTime.FromDateTime(dt),
+ var other => other
+ };
+ }
+
+ return arr;
+ }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbConnection.cs
b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbConnection.cs
new file mode 100644
index 00000000000..699aa8d1018
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbConnection.cs
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+namespace Apache.Ignite.Sql;
+
+using System;
+using System.Data;
+using System.Data.Common;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using System.Threading.Tasks;
+using Internal.Common;
+
+/// <summary>
+/// Ignite database connection.
+/// </summary>
+public sealed class IgniteDbConnection : DbConnection
+{
+ private bool _ownsClient;
+
+ private IIgniteClient? _igniteClient;
+
+ private string _connectionString = string.Empty;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IgniteDbConnection"/>
class.
+ /// </summary>
+ /// <param name="connectionString">Connection string.</param>
+ public IgniteDbConnection(string? connectionString)
+ {
+ ConnectionString = connectionString;
+ }
+
+ /// <inheritdoc />
+ [AllowNull]
+ public override string ConnectionString
+ {
+ get => _connectionString;
+ set
+ {
+ if (State != ConnectionState.Closed)
+ {
+ throw new InvalidOperationException("Cannot set
ConnectionString while the connection is open.");
+ }
+
+ _connectionString = value ?? string.Empty;
+ }
+ }
+
+ /// <inheritdoc />
+ public override string Database => string.Empty;
+
+ /// <inheritdoc />
+ public override ConnectionState State => _igniteClient == null
+ ? ConnectionState.Closed
+ : ConnectionState.Open;
+
+ /// <inheritdoc />
+ public override string DataSource => string.Empty;
+
+ /// <inheritdoc />
+ public override string ServerVersion => "3.x"; // TODO IGNITE-25936
+
+ /// <summary>
+ /// Gets the underlying Ignite client instance, or null if the connection
is not open.
+ /// </summary>
+ public IIgniteClient? Client => _igniteClient;
+
+ /// <inheritdoc />
+ public override void ChangeDatabase(string databaseName) =>
+ throw new NotSupportedException("Changing database is not supported in
Ignite.");
+
+ /// <inheritdoc />
+ public override void Close()
+ {
+ if (_ownsClient)
+ {
+ _igniteClient?.Dispose();
+ }
+
+ _igniteClient = null;
+ }
+
+ /// <inheritdoc />
+ public override void Open() =>
OpenAsync(CancellationToken.None).GetAwaiter().GetResult();
+
+ /// <inheritdoc />
+ public override async Task OpenAsync(CancellationToken cancellationToken)
+ {
+ if (State != ConnectionState.Closed)
+ {
+ throw new InvalidOperationException("Connection is already open.");
+ }
+
+ var connStrBuilder = new
IgniteDbConnectionStringBuilder(ConnectionString);
+ IgniteClientConfiguration cfg =
connStrBuilder.ToIgniteClientConfiguration();
+
+ _igniteClient = await
IgniteClient.StartAsync(cfg).ConfigureAwait(false);
+ _ownsClient = true;
+ }
+
+ /// <summary>
+ /// Opens the connection using an existing Ignite client.
+ /// </summary>
+ /// <param name="igniteClient">Ignite client.</param>
+ /// <param name="ownsClient">Whether to dispose the client when the
connection is disposed.</param>
+ public void Open(IIgniteClient igniteClient, bool ownsClient = false)
+ {
+ if (State != ConnectionState.Closed)
+ {
+ throw new InvalidOperationException("Connection is already open.");
+ }
+
+ IgniteArgumentCheck.NotNull(igniteClient);
+ _igniteClient = igniteClient;
+ _ownsClient = ownsClient;
+ }
+
+ /// <inheritdoc/>
+ public override string ToString() =>
+ new IgniteToStringBuilder(GetType())
+ .Append(ConnectionString)
+ .Append(State)
+ .Append(ServerVersion)
+ .Append(Client)
+ .Build();
+
+ /// <inheritdoc />
+ protected override DbTransaction BeginDbTransaction(IsolationLevel
isolationLevel) =>
+ BeginDbTransactionAsync(isolationLevel,
CancellationToken.None).AsTask().GetAwaiter().GetResult();
+
+ /// <inheritdoc />
+ protected override async ValueTask<DbTransaction> BeginDbTransactionAsync(
+ IsolationLevel isolationLevel,
+ CancellationToken cancellationToken)
+ {
+ var tx = await
_igniteClient!.Transactions.BeginAsync().ConfigureAwait(false);
+
+ return new IgniteDbTransaction(tx, isolationLevel, this);
+ }
+
+ /// <inheritdoc />
+ protected override DbCommand CreateDbCommand() => new IgniteDbCommand
+ {
+ Connection = this
+ };
+
+ /// <inheritdoc />
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ Close();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git
a/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbConnectionStringBuilder.cs
b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbConnectionStringBuilder.cs
new file mode 100644
index 00000000000..cb49013fbbd
--- /dev/null
+++
b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbConnectionStringBuilder.cs
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+namespace Apache.Ignite.Sql;
+
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+
+/// <summary>
+/// Ignite connection string builder.
+/// </summary>
+[SuppressMessage("Design", "CA1010:Generic interface should also be
implemented", Justification = "Reviewed.")]
+public sealed class IgniteDbConnectionStringBuilder : DbConnectionStringBuilder
+{
+ /// <summary>
+ /// Gets the character used to separate multiple endpoints in the
connection string.
+ /// </summary>
+ public const char EndpointSeparator = ',';
+
+ private static readonly IReadOnlySet<string> KnownKeys = new
HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ {
+ nameof(Endpoints),
+ nameof(SocketTimeout),
+ nameof(OperationTimeout),
+ nameof(HeartbeatInterval),
+ nameof(ReconnectInterval),
+ nameof(SslEnabled),
+ nameof(Username),
+ nameof(Password)
+ };
+
+ /// <summary>
+ /// Initializes a new instance of the <see
cref="IgniteDbConnectionStringBuilder"/> class.
+ /// </summary>
+ public IgniteDbConnectionStringBuilder()
+ {
+ // No-op.
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see
cref="IgniteDbConnectionStringBuilder"/> class.
+ /// </summary>
+ /// <param name="connectionString">Connection string.</param>
+ public IgniteDbConnectionStringBuilder(string connectionString)
+ {
+ ConnectionString = connectionString;
+ }
+
+ /// <summary>
+ /// Gets or sets the Ignite endpoints.
+ /// Multiple endpoints can be specified separated by comma, e.g.
"localhost:10800,localhost:10801".
+ /// If the port is not specified, the default port 10800 is used.
+ /// </summary>
+ [SuppressMessage("Usage", "CA2227:Collection properties should be read
only", Justification = "Reviewed.")]
+ public IList<string> Endpoints
+ {
+ get => this[nameof(Endpoints)] is string endpoints
+ ? endpoints.Split(EndpointSeparator,
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ : [];
+ set => this[nameof(Endpoints)] = string.Join(EndpointSeparator, value);
+ }
+
+ /// <summary>
+ /// Gets or sets the socket timeout. See <see
cref="IgniteClientConfiguration.SocketTimeout"/> for more details.
+ /// </summary>
+ public TimeSpan SocketTimeout
+ {
+ get => GetString(nameof(SocketTimeout)) is { } s
+ ? TimeSpan.Parse(s, CultureInfo.InvariantCulture)
+ : IgniteClientConfiguration.DefaultSocketTimeout;
+ set => this[nameof(SocketTimeout)] = value.ToString();
+ }
+
+ /// <summary>
+ /// Gets or sets the socket timeout. See <see
cref="IgniteClientConfiguration.OperationTimeout"/> for more details.
+ /// </summary>
+ public TimeSpan OperationTimeout
+ {
+ get => GetString(nameof(OperationTimeout)) is { } s
+ ? TimeSpan.Parse(s, CultureInfo.InvariantCulture)
+ : IgniteClientConfiguration.DefaultOperationTimeout;
+ set => this[nameof(OperationTimeout)] = value.ToString();
+ }
+
+ /// <summary>
+ /// Gets or sets the heartbeat interval. See <see
cref="IgniteClientConfiguration.HeartbeatInterval"/> for more details.
+ /// </summary>
+ public TimeSpan HeartbeatInterval
+ {
+ get => GetString(nameof(HeartbeatInterval)) is { } s
+ ? TimeSpan.Parse(s, CultureInfo.InvariantCulture)
+ : IgniteClientConfiguration.DefaultHeartbeatInterval;
+ set => this[nameof(HeartbeatInterval)] = value.ToString();
+ }
+
+ /// <summary>
+ /// Gets or sets the reconnect interval. See <see
cref="IgniteClientConfiguration.ReconnectInterval"/> for more details.
+ /// </summary>
+ public TimeSpan ReconnectInterval
+ {
+ get => GetString(nameof(ReconnectInterval)) is { } s
+ ? TimeSpan.Parse(s, CultureInfo.InvariantCulture)
+ : IgniteClientConfiguration.DefaultReconnectInterval;
+ set => this[nameof(ReconnectInterval)] = value.ToString();
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether SSL is enabled.
+ /// </summary>
+ public bool SslEnabled
+ {
+ get => GetString(nameof(SslEnabled)) is { } s && bool.Parse(s);
+ set => this[nameof(SslEnabled)] = value.ToString();
+ }
+
+ /// <summary>
+ /// Gets or sets the username for authentication.
+ /// </summary>
+ public string? Username
+ {
+ get => GetString(nameof(Username));
+ set => this[nameof(Username)] = value;
+ }
+
+ /// <summary>
+ /// Gets or sets the password for authentication.
+ /// </summary>
+ public string? Password
+ {
+ get => GetString(nameof(Password));
+ set => this[nameof(Password)] = value;
+ }
+
+ /// <inheritdoc />
+ [AllowNull]
+ public override object this[string keyword]
+ {
+ get => base[keyword];
+ set
+ {
+ if (!KnownKeys.Contains(keyword))
+ {
+ throw new ArgumentException($"Unknown connection string key:
'{keyword}'.", nameof(keyword));
+ }
+
+ base[keyword] = value;
+ }
+ }
+
+ /// <summary>
+ /// Converts this instance to <see cref="IgniteClientConfiguration"/>.
+ /// </summary>
+ /// <returns>Ignite client configuration.</returns>
+ public IgniteClientConfiguration ToIgniteClientConfiguration()
+ {
+ return new IgniteClientConfiguration([.. Endpoints])
+ {
+ SocketTimeout = SocketTimeout,
+ OperationTimeout = OperationTimeout,
+ HeartbeatInterval = HeartbeatInterval,
+ ReconnectInterval = ReconnectInterval,
+ SslStreamFactory = SslEnabled ? new SslStreamFactory() : null,
+ Authenticator = Username is null && Password is null ? null : new
BasicAuthenticator
+ {
+ Username = Username ?? string.Empty,
+ Password = Password ?? string.Empty
+ }
+ };
+ }
+
+ private string? GetString(string key) => TryGetValue(key, out var s) ?
(string?)s : null;
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbException.cs
b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbException.cs
new file mode 100644
index 00000000000..3cfe7804a8d
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbException.cs
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+namespace Apache.Ignite.Sql;
+
+using System.Data.Common;
+
+/// <summary>
+/// Ignite database exception.
+/// </summary>
+public sealed class IgniteDbException : DbException
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IgniteDbException"/>
class.
+ /// </summary>
+ /// <param name="message">Message.</param>
+ /// <param name="innerException">Inner exception.</param>
+ public IgniteDbException(string message, System.Exception innerException)
+ : base(message, innerException)
+ {
+ // No-op.
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IgniteDbException"/>
class.
+ /// </summary>
+ /// <param name="message">Message.</param>
+ public IgniteDbException(string message)
+ : base(message)
+ {
+ // No-op.
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IgniteDbException"/>
class.
+ /// </summary>
+ public IgniteDbException()
+ {
+ // No-op.
+ }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbParameter.cs
b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbParameter.cs
new file mode 100644
index 00000000000..a5f38d792c8
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbParameter.cs
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+namespace Apache.Ignite.Sql;
+
+using System;
+using System.Data;
+using System.Data.Common;
+using System.Diagnostics.CodeAnalysis;
+using Internal.Common;
+
+/// <summary>
+/// Ignite database parameter.
+/// </summary>
+public sealed class IgniteDbParameter : DbParameter
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IgniteDbParameter"/>
class.
+ /// </summary>
+ public IgniteDbParameter()
+ {
+ // No-op.
+ }
+
+ /// <inheritdoc />
+ public override DbType DbType { get; set; } = DbType.String;
+
+ /// <summary>
+ /// Gets or sets the direction of the parameter. Only <see
cref="ParameterDirection.Input" /> is supported.
+ /// </summary>
+ /// <value>The direction of the parameter.</value>
+ public override ParameterDirection Direction
+ {
+ get => ParameterDirection.Input;
+ set
+ {
+ if (value != ParameterDirection.Input)
+ {
+ throw new ArgumentException($"Only ParameterDirection.Input is
supported: {value}", nameof(value));
+ }
+ }
+ }
+
+ /// <inheritdoc />
+ public override bool IsNullable { get; set; }
+
+ /// <inheritdoc />
+ [AllowNull]
+ public override string ParameterName { get; set; } = string.Empty;
+
+ /// <inheritdoc />
+ [AllowNull]
+ public override string SourceColumn { get; set; } = string.Empty;
+
+ /// <inheritdoc />
+ public override object? Value { get; set; }
+
+ /// <inheritdoc />
+ public override bool SourceColumnNullMapping { get; set; }
+
+ /// <inheritdoc />
+ public override int Size { get; set; }
+
+ /// <inheritdoc />
+ public override void ResetDbType() => DbType = DbType.String;
+
+ /// <inheritdoc/>
+ public override string ToString() =>
+ new IgniteToStringBuilder(GetType())
+ .Append(Value) // Only value is important for an Ignite parameter.
+ .Build();
+}
diff --git
a/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbParameterCollection.cs
b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbParameterCollection.cs
new file mode 100644
index 00000000000..e8eec334701
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbParameterCollection.cs
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+namespace Apache.Ignite.Sql;
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Internal.Common;
+
+/// <summary>
+/// Ignite database parameter collection.
+/// </summary>
+public sealed class IgniteDbParameterCollection : DbParameterCollection,
IReadOnlyList<IgniteDbParameter>, IList<IgniteDbParameter>
+{
+ [SuppressMessage("Design", "CA1002:Do not expose generic lists",
Justification = "Not exposed.")]
+ private readonly List<IgniteDbParameter> _parameters = new();
+
+ /// <summary>
+ /// Initializes a new instance of the <see
cref="IgniteDbParameterCollection"/> class.
+ /// </summary>
+ internal IgniteDbParameterCollection()
+ {
+ // No-op.
+ }
+
+ /// <summary>
+ /// Gets the number of parameters in the collection.
+ /// </summary>
+ public override int Count
+ => _parameters.Count;
+
+ /// <inheritdoc/>
+ public override object SyncRoot
+ => ((ICollection)_parameters).SyncRoot;
+
+ /// <summary>
+ /// Gets or sets the element at the specified index.
+ /// </summary>
+ /// <param name="index">The zero-based index of the element to get or
set.</param>
+ /// <returns>The element at the specified index.</returns>
+ public new IgniteDbParameter this[int index]
+ {
+ get => _parameters[index];
+ set => _parameters[index] = value;
+ }
+
+ /// <inheritdoc/>
+ public override int Add(object value)
+ {
+ _parameters.Add((IgniteDbParameter)value);
+
+ return Count - 1;
+ }
+
+ /// <inheritdoc/>
+ public override void AddRange(Array values)
+ => _parameters.AddRange(values.Cast<IgniteDbParameter>());
+
+ /// <inheritdoc />
+ public void Add(IgniteDbParameter item) => _parameters.Add(item);
+
+ /// <summary>
+ /// Removes all parameters from the collection.
+ /// </summary>
+ public override void Clear()
+ => _parameters.Clear();
+
+ /// <inheritdoc />
+ public bool Contains(IgniteDbParameter item) => _parameters.Contains(item);
+
+ /// <inheritdoc />
+ public void CopyTo(IgniteDbParameter[] array, int arrayIndex) =>
_parameters.CopyTo(array, arrayIndex);
+
+ /// <inheritdoc/>
+ public override void CopyTo(Array array, int index)
+ => _parameters.CopyTo((IgniteDbParameter[])array, index);
+
+ /// <inheritdoc/>
+ public override bool Contains(object value)
+ => _parameters.Contains((IgniteDbParameter)value);
+
+ /// <inheritdoc/>
+ public override bool Contains(string value)
+ => IndexOf(value) != -1;
+
+ /// <inheritdoc/>
+ IEnumerator<IgniteDbParameter>
IEnumerable<IgniteDbParameter>.GetEnumerator()
+ => _parameters.GetEnumerator();
+
+ /// <inheritdoc/>
+ public override IEnumerator GetEnumerator()
+ => _parameters.GetEnumerator();
+
+ /// <inheritdoc/>
+ public override int IndexOf(object value)
+ => _parameters.IndexOf((IgniteDbParameter)value);
+
+ /// <inheritdoc/>
+ public override int IndexOf(string parameterName)
+ {
+ for (var index = 0; index < _parameters.Count; index++)
+ {
+ if (_parameters[index].ParameterName == parameterName)
+ {
+ return index;
+ }
+ }
+
+ return -1;
+ }
+
+ /// <inheritdoc/>
+ public override void Insert(int index, object value)
+ => Insert(index, (IgniteDbParameter)value);
+
+ /// <inheritdoc/>
+ public int IndexOf(IgniteDbParameter item)
+ => _parameters.IndexOf(item);
+
+ /// <inheritdoc/>
+ public void Insert(int index, IgniteDbParameter value)
+ => _parameters.Insert(index, value);
+
+ /// <inheritdoc/>
+ public override void Remove(object value)
+ => Remove((IgniteDbParameter)value);
+
+ /// <inheritdoc/>
+ public bool Remove(IgniteDbParameter value)
+ => _parameters.Remove(value);
+
+ /// <summary>Removes the item at the specified index.</summary>
+ /// <param name="index">The zero-based index of the item to remove.</param>
+ public override void RemoveAt(int index)
+ => _parameters.RemoveAt(index);
+
+ /// <inheritdoc/>
+ public override void RemoveAt(string parameterName)
+ => RemoveAt(IndexOfChecked(parameterName));
+
+ /// <inheritdoc/>
+ public override string ToString() =>
+ new IgniteToStringBuilder(GetType())
+ .Append(Count)
+ .Build();
+
+ /// <inheritdoc/>
+ protected override void SetParameter(int index, DbParameter value)
+ => this[index] = (IgniteDbParameter)value;
+
+ /// <inheritdoc/>
+ protected override void SetParameter(string parameterName, DbParameter
value)
+ => SetParameter(IndexOfChecked(parameterName), value);
+
+ /// <inheritdoc/>
+ protected override DbParameter GetParameter(int index)
+ => this[index];
+
+ /// <inheritdoc/>
+ protected override DbParameter GetParameter(string parameterName)
+ => GetParameter(IndexOfChecked(parameterName));
+
+ private int IndexOfChecked(string parameterName)
+ {
+ var index = IndexOf(parameterName);
+ if (index == -1)
+ {
+ throw new InvalidOperationException($"Parameter not found:
{parameterName}");
+ }
+
+ return index;
+ }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbTransaction.cs
b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbTransaction.cs
new file mode 100644
index 00000000000..ecb8798945f
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite/Sql/IgniteDbTransaction.cs
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+namespace Apache.Ignite.Sql;
+
+using System.Data;
+using System.Data.Common;
+using System.Threading;
+using System.Threading.Tasks;
+using Internal.Common;
+using Transactions;
+
+/// <summary>
+/// Ignite database transaction.
+/// </summary>
+public sealed class IgniteDbTransaction : DbTransaction
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IgniteDbTransaction"/>
class.
+ /// </summary>
+ /// <param name="tx">Underlying Ignite transaction.</param>
+ /// <param name="isolationLevel">Isolation level.</param>
+ /// <param name="connection">Connection.</param>
+ public IgniteDbTransaction(ITransaction tx, IsolationLevel isolationLevel,
DbConnection connection)
+ {
+ IgniteTransaction = tx;
+ IsolationLevel = isolationLevel;
+ DbConnection = connection;
+ }
+
+ /// <inheritdoc />
+ public override IsolationLevel IsolationLevel { get; }
+
+ /// <inheritdoc />
+ public override bool SupportsSavepoints => false;
+
+ /// <summary>
+ /// Gets the underlying Ignite transaction.
+ /// </summary>
+ public ITransaction IgniteTransaction { get; }
+
+ /// <inheritdoc />
+ protected override DbConnection DbConnection { get; }
+
+ /// <inheritdoc />
+ public override void Commit() =>
CommitAsync(CancellationToken.None).GetAwaiter().GetResult();
+
+ /// <inheritdoc />
+ public override void Rollback() => RollbackAsync(null!,
CancellationToken.None).GetAwaiter().GetResult();
+
+ /// <inheritdoc />
+ public override async ValueTask DisposeAsync()
+ {
+ await IgniteTransaction.DisposeAsync().ConfigureAwait(false);
+ await base.DisposeAsync().ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ public override async Task CommitAsync(CancellationToken cancellationToken
= default) =>
+ await IgniteTransaction.CommitAsync().ConfigureAwait(false);
+
+ /// <inheritdoc />
+ public override async Task RollbackAsync(string savepointName,
CancellationToken cancellationToken = default) =>
+ await IgniteTransaction.RollbackAsync().ConfigureAwait(false);
+
+ /// <inheritdoc/>
+ public override string ToString() =>
+ new IgniteToStringBuilder(GetType())
+ .Append(IgniteTransaction)
+ .Append(Connection)
+ .Build();
+
+ /// <inheritdoc />
+ protected override void Dispose(bool disposing)
+ {
+ IgniteTransaction.Dispose();
+ base.Dispose(disposing);
+ }
+}