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);
+    }
+}

Reply via email to