You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by pt...@apache.org on 2022/10/17 18:06:28 UTC

[ignite-3] branch main updated: IGNITE-17910 .NET: Add KeyValueView (#1220)

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 d99593434f IGNITE-17910 .NET: Add KeyValueView (#1220)
d99593434f is described below

commit d99593434f8273a82d0fbc0253b9dea9626fa4cc
Author: Pavel Tupitsyn <pt...@apache.org>
AuthorDate: Mon Oct 17 21:06:22 2022 +0300

    IGNITE-17910 .NET: Add KeyValueView (#1220)
    
    * Add `ITable.GetKeyValueView<TK, TV>()`
    * Add support for key/val pair object mapping to `ObjectSerializerHandler`
---
 .../dotnet/Apache.Ignite.Tests/IgniteTestsBase.cs  |   4 +-
 .../Table/KeyValueViewBinaryTests.cs               |   2 +
 .../Table/KeyValueViewPocoPrimitiveTests.cs        | 293 ++++++++++++++++++
 .../Table/KeyValueViewPocoTests.cs                 | 294 ++++++++++++++++++
 .../Table/KeyValueViewPrimitivePocoTests.cs        | 282 ++++++++++++++++++
 .../Table/KeyValueViewPrimitiveTests.cs            | 328 +++++++++++++++++++++
 .../Table/Serialization/ObjectSerializerHandler.cs | 270 +++++++++++++++--
 .../dotnet/Apache.Ignite/Internal/Table/Table.cs   |   6 +
 .../platforms/dotnet/Apache.Ignite/Table/ITable.cs |  13 +
 9 files changed, 1464 insertions(+), 28 deletions(-)

diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/IgniteTestsBase.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/IgniteTestsBase.cs
index 5e4f997359..37f8cf3f7a 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/IgniteTestsBase.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/IgniteTestsBase.cs
@@ -58,8 +58,6 @@ namespace Apache.Ignite.Tests
 
         protected IRecordView<IIgniteTuple> TupleView { get; private set; } = null!;
 
-        protected IKeyValueView<IIgniteTuple, IIgniteTuple> KvView => Table.KeyValueBinaryView;
-
         protected IRecordView<Poco> PocoView { get; private set; } = null!;
 
         [OneTimeSetUp]
@@ -99,6 +97,8 @@ namespace Apache.Ignite.Tests
 
         protected static Poco GetPoco(long id, string? val = null) => new() {Key = id, Val = val};
 
+        protected static Poco GetPoco(string? val) => new() {Val = val};
+
         protected static IgniteClientConfiguration GetConfig() => new()
         {
             Endpoints = { "127.0.0.1:" + ServerNode.Port },
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewBinaryTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewBinaryTests.cs
index de36205632..77b561ccc5 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewBinaryTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewBinaryTests.cs
@@ -29,6 +29,8 @@ using NUnit.Framework;
 /// </summary>
 public class KeyValueViewBinaryTests : IgniteTestsBase
 {
+    private IKeyValueView<IIgniteTuple, IIgniteTuple> KvView => Table.KeyValueBinaryView;
+
     [TearDown]
     public async Task CleanTable()
     {
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPocoPrimitiveTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPocoPrimitiveTests.cs
new file mode 100644
index 0000000000..03d4c19e75
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPocoPrimitiveTests.cs
@@ -0,0 +1,293 @@
+/*
+ * 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.Table;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Ignite.Table;
+using NUnit.Framework;
+
+/// <summary>
+/// Tests for key-value view where key is a user object (poco), and value is a primitive (string).
+/// </summary>
+public class KeyValueViewPocoPrimitiveTests : IgniteTestsBase
+{
+    private IKeyValueView<Poco, string> KvView => Table.GetKeyValueView<Poco, string>();
+
+    [TearDown]
+    public async Task CleanTable()
+    {
+        await TupleView.DeleteAllAsync(null, Enumerable.Range(-1, 12).Select(x => GetTuple(x)));
+    }
+
+    [Test]
+    public async Task TestPutGet()
+    {
+        await KvView.PutAsync(null, GetPoco(1L), "val");
+
+        (string res, bool hasRes) = await KvView.GetAsync(null, GetPoco(1L));
+
+        Assert.IsTrue(hasRes);
+        Assert.AreEqual("val", res);
+    }
+
+    [Test]
+    public async Task TestGetNonExistentKeyReturnsEmptyOption()
+    {
+        (string res, bool hasRes) = await KvView.GetAsync(null, GetPoco(-111L));
+
+        Assert.IsFalse(hasRes);
+        Assert.IsNull(res);
+    }
+
+    [Test]
+    public async Task TestGetAll()
+    {
+        await KvView.PutAsync(null, GetPoco(7L), "val1");
+        await KvView.PutAsync(null, GetPoco(8L), "val2");
+
+        IDictionary<Poco, string> res = await KvView.GetAllAsync(null, Enumerable.Range(-1, 100).Select(x => GetPoco(x)).ToList());
+        IDictionary<Poco, string> resEmpty = await KvView.GetAllAsync(null, Array.Empty<Poco>());
+
+        Assert.AreEqual(2, res.Count);
+        Assert.AreEqual("val1", res.Single(x => x.Key.Key == 7).Value);
+        Assert.AreEqual("val2", res.Single(x => x.Key.Key == 8).Value);
+
+        Assert.AreEqual(0, resEmpty.Count);
+    }
+
+    [Test]
+    public void TestGetAllWithNullKeyThrowsArgumentException()
+    {
+        var ex = Assert.ThrowsAsync<ArgumentNullException>(async () =>
+            await KvView.GetAllAsync(null, new[] { GetPoco(1L), null! }));
+
+        Assert.AreEqual("Value cannot be null. (Parameter 'key')", ex!.Message);
+    }
+
+    [Test]
+    public void TestPutNullThrowsArgumentException()
+    {
+        var keyEx = Assert.ThrowsAsync<ArgumentNullException>(async () => await KvView.PutAsync(null, null!, null!));
+        Assert.AreEqual("Value cannot be null. (Parameter 'key')", keyEx!.Message);
+
+        var valEx = Assert.ThrowsAsync<ArgumentNullException>(async () => await KvView.PutAsync(null, GetPoco(1L), null!));
+        Assert.AreEqual("Value cannot be null. (Parameter 'val')", valEx!.Message);
+    }
+
+    [Test]
+    public async Task TestContains()
+    {
+        await KvView.PutAsync(null, GetPoco(7L), "val1");
+
+        bool res1 = await KvView.ContainsAsync(null, GetPoco(7L));
+        bool res2 = await KvView.ContainsAsync(null, GetPoco(8L));
+
+        Assert.IsTrue(res1);
+        Assert.IsFalse(res2);
+    }
+
+    [Test]
+    public async Task TestPutAll()
+    {
+        await KvView.PutAllAsync(null, new Dictionary<Poco, string>());
+        await KvView.PutAllAsync(
+            null,
+            Enumerable.Range(-1, 7).Select(x => new KeyValuePair<Poco, string>(GetPoco(x), "v" + x)));
+
+        IDictionary<Poco, string> res = await KvView.GetAllAsync(null, Enumerable.Range(-10, 20).Select(x => GetPoco(x)));
+
+        Assert.AreEqual(7, res.Count);
+
+        for (int i = -1; i < 6; i++)
+        {
+            string val = res.Single(x => x.Key.Key == i).Value;
+            Assert.AreEqual("v" + i, val);
+        }
+    }
+
+    [Test]
+    public async Task TestGetAndPut()
+    {
+        Option<string> res1 = await KvView.GetAndPutAsync(null, GetPoco(1), "1");
+        Option<string> res2 = await KvView.GetAndPutAsync(null, GetPoco(1), "2");
+        Option<string> res3 = await KvView.GetAsync(null, GetPoco(1));
+
+        Assert.IsFalse(res1.HasValue);
+        Assert.IsTrue(res2.HasValue);
+        Assert.IsTrue(res3.HasValue);
+
+        Assert.AreEqual("1", res2.Value);
+        Assert.AreEqual("2", res3.Value);
+    }
+
+    [Test]
+    public async Task TestPutIfAbsent()
+    {
+        await KvView.PutAsync(null, GetPoco(1), "1");
+
+        bool res1 = await KvView.PutIfAbsentAsync(null, GetPoco(1), "11");
+        Option<string> res2 = await KvView.GetAsync(null, GetPoco(1));
+
+        bool res3 = await KvView.PutIfAbsentAsync(null, GetPoco(2), "2");
+        Option<string> res4 = await KvView.GetAsync(null, GetPoco(2));
+
+        Assert.IsFalse(res1);
+        Assert.AreEqual("1", res2.Value);
+
+        Assert.IsTrue(res3);
+        Assert.AreEqual("2", res4.Value);
+    }
+
+    [Test]
+    public async Task TestRemove()
+    {
+        await KvView.PutAsync(null, GetPoco(1), "1");
+
+        bool res1 = await KvView.RemoveAsync(null, GetPoco(1));
+        bool res2 = await KvView.RemoveAsync(null, GetPoco(2));
+        bool res3 = await KvView.ContainsAsync(null, GetPoco(1));
+
+        Assert.IsTrue(res1);
+        Assert.IsFalse(res2);
+        Assert.IsFalse(res3);
+    }
+
+    [Test]
+    public async Task TestRemoveExact()
+    {
+        await KvView.PutAsync(null, GetPoco(1), "1");
+
+        bool res1 = await KvView.RemoveAsync(null, GetPoco(1), "111");
+        bool res2 = await KvView.RemoveAsync(null, GetPoco(1), "1");
+        bool res3 = await KvView.ContainsAsync(null, GetPoco(1));
+
+        Assert.IsFalse(res1);
+        Assert.IsTrue(res2);
+        Assert.IsFalse(res3);
+    }
+
+    [Test]
+    public async Task TestRemoveAll()
+    {
+        await KvView.PutAsync(null, GetPoco(1), "1");
+
+        IList<Poco> res1 = await KvView.RemoveAllAsync(null, Enumerable.Range(-1, 8).Select(x => GetPoco(x, "foo")));
+        bool res2 = await KvView.ContainsAsync(null, GetPoco(1));
+
+        Assert.AreEqual(new[] { -1, 0, 2, 3, 4, 5, 6 }, res1.Select(x => x.Key).OrderBy(x => x));
+        Assert.IsFalse(res2);
+    }
+
+    [Test]
+    public async Task TestRemoveAllExact()
+    {
+        await KvView.PutAsync(null, GetPoco(1), "1");
+
+        IList<Poco> res1 = await KvView.RemoveAllAsync(
+            null,
+            Enumerable.Range(-1, 8).Select(x => new KeyValuePair<Poco, string>(GetPoco(x), x.ToString())));
+
+        bool res2 = await KvView.ContainsAsync(null, GetPoco(1));
+
+        Assert.AreEqual(new[] { -1, 0, 2, 3, 4, 5, 6 }, res1.Select(x => x.Key).OrderBy(x => x));
+        Assert.IsFalse(res2);
+    }
+
+    [Test]
+    public async Task TestGetAndRemove()
+    {
+        await KvView.PutAsync(null, GetPoco(1), "1");
+
+        (string val1, bool hasVal1) = await KvView.GetAndRemoveAsync(null, GetPoco(1));
+        (string val2, bool hasVal2) = await KvView.GetAndRemoveAsync(null, GetPoco(1));
+
+        Assert.IsTrue(hasVal1);
+        Assert.AreEqual("1", val1);
+
+        Assert.IsFalse(hasVal2);
+        Assert.IsNull(val2);
+    }
+
+    [Test]
+    public async Task TestReplace()
+    {
+        await KvView.PutAsync(null, GetPoco(1), "1");
+
+        bool res1 = await KvView.ReplaceAsync(null, GetPoco(0), "00");
+        Option<string> res2 = await KvView.GetAsync(null, GetPoco(0));
+
+        bool res3 = await KvView.ReplaceAsync(null, GetPoco(1), "11");
+        Option<string> res4 = await KvView.GetAsync(null, GetPoco(1));
+
+        Assert.IsFalse(res1);
+        Assert.IsFalse(res2.HasValue);
+
+        Assert.IsTrue(res3);
+        Assert.IsTrue(res4.HasValue);
+        Assert.AreEqual("11", res4.Value);
+    }
+
+    [Test]
+    public async Task TestReplaceExact()
+    {
+        await KvView.PutAsync(null, GetPoco(1), "1");
+
+        bool res1 = await KvView.ReplaceAsync(transaction: null, key: GetPoco(0), oldVal: "0", newVal: "00");
+        Option<string> res2 = await KvView.GetAsync(null, GetPoco(0));
+
+        bool res3 = await KvView.ReplaceAsync(transaction: null, key: GetPoco(1), oldVal: "1", newVal: "11");
+        Option<string> res4 = await KvView.GetAsync(null, GetPoco(1));
+
+        bool res5 = await KvView.ReplaceAsync(transaction: null, key: GetPoco(2), oldVal: "1", newVal: "22");
+        Option<string> res6 = await KvView.GetAsync(null, GetPoco(1));
+
+        Assert.IsFalse(res1);
+        Assert.IsFalse(res2.HasValue);
+
+        Assert.IsTrue(res3);
+        Assert.IsTrue(res4.HasValue);
+        Assert.AreEqual("11", res4.Value);
+
+        Assert.IsFalse(res5);
+        Assert.AreEqual("11", res6.Value);
+    }
+
+    [Test]
+    public async Task TestGetAndReplace()
+    {
+        await KvView.PutAsync(null, GetPoco(1), "1");
+
+        Option<string> res1 = await KvView.GetAndReplaceAsync(null, GetPoco(0), "00");
+        Option<string> res2 = await KvView.GetAsync(null, GetPoco(0));
+
+        Option<string> res3 = await KvView.GetAndReplaceAsync(null, GetPoco(1), "11");
+        Option<string> res4 = await KvView.GetAsync(null, GetPoco(1));
+
+        Assert.IsFalse(res1.HasValue);
+        Assert.IsFalse(res2.HasValue);
+
+        Assert.IsTrue(res3.HasValue);
+        Assert.AreEqual("1", res3.Value);
+
+        Assert.IsTrue(res4.HasValue);
+        Assert.AreEqual("11", res4.Value);
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPocoTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPocoTests.cs
new file mode 100644
index 0000000000..4c12af07d0
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPocoTests.cs
@@ -0,0 +1,294 @@
+/*
+ * 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.Table;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Ignite.Table;
+using NUnit.Framework;
+
+/// <summary>
+/// Tests for key-value POCO view.
+/// </summary>
+public class KeyValueViewPocoTests : IgniteTestsBase
+{
+    private IKeyValueView<Poco, Poco> KvView => Table.GetKeyValueView<Poco, Poco>();
+
+    [TearDown]
+    public async Task CleanTable()
+    {
+        await TupleView.DeleteAllAsync(null, Enumerable.Range(-1, 12).Select(x => GetTuple(x)));
+    }
+
+    [Test]
+    public async Task TestPutGet()
+    {
+        await KvView.PutAsync(null, GetPoco(1L), GetPoco("val"));
+
+        (Poco res, bool hasRes) = await KvView.GetAsync(null, GetPoco(1L));
+
+        Assert.IsTrue(hasRes);
+        Assert.AreEqual("val", res.Val);
+        Assert.AreEqual(0, res.Key);
+    }
+
+    [Test]
+    public async Task TestGetNonExistentKeyReturnsEmptyOption()
+    {
+        (Poco res, bool hasRes) = await KvView.GetAsync(null, GetPoco(-111L));
+
+        Assert.IsFalse(hasRes);
+        Assert.IsNull(res);
+    }
+
+    [Test]
+    public async Task TestGetAll()
+    {
+        await KvView.PutAsync(null, GetPoco(7L), GetPoco("val1"));
+        await KvView.PutAsync(null, GetPoco(8L), GetPoco("val2"));
+
+        IDictionary<Poco, Poco> res = await KvView.GetAllAsync(null, Enumerable.Range(-1, 100).Select(x => GetPoco(x)).ToList());
+        IDictionary<Poco, Poco> resEmpty = await KvView.GetAllAsync(null, Array.Empty<Poco>());
+
+        Assert.AreEqual(2, res.Count);
+        Assert.AreEqual("val1", res.Single(x => x.Key.Key == 7).Value.Val);
+        Assert.AreEqual("val2", res.Single(x => x.Key.Key == 8).Value.Val);
+
+        Assert.AreEqual(0, resEmpty.Count);
+    }
+
+    [Test]
+    public void TestGetAllWithNullKeyThrowsArgumentException()
+    {
+        var ex = Assert.ThrowsAsync<ArgumentNullException>(async () =>
+            await KvView.GetAllAsync(null, new[] { GetPoco(1L), null! }));
+
+        Assert.AreEqual("Value cannot be null. (Parameter 'key')", ex!.Message);
+    }
+
+    [Test]
+    public void TestPutNullThrowsArgumentException()
+    {
+        var keyEx = Assert.ThrowsAsync<ArgumentNullException>(async () => await KvView.PutAsync(null, null!, null!));
+        Assert.AreEqual("Value cannot be null. (Parameter 'key')", keyEx!.Message);
+
+        var valEx = Assert.ThrowsAsync<ArgumentNullException>(async () => await KvView.PutAsync(null, GetPoco(1L), null!));
+        Assert.AreEqual("Value cannot be null. (Parameter 'val')", valEx!.Message);
+    }
+
+    [Test]
+    public async Task TestContains()
+    {
+        await KvView.PutAsync(null, GetPoco(7L), GetPoco("val1"));
+
+        bool res1 = await KvView.ContainsAsync(null, GetPoco(7L));
+        bool res2 = await KvView.ContainsAsync(null, GetPoco(8L));
+
+        Assert.IsTrue(res1);
+        Assert.IsFalse(res2);
+    }
+
+    [Test]
+    public async Task TestPutAll()
+    {
+        await KvView.PutAllAsync(null, new Dictionary<Poco, Poco>());
+        await KvView.PutAllAsync(
+            null,
+            Enumerable.Range(-1, 7).Select(x => new KeyValuePair<Poco, Poco>(GetPoco(x), GetPoco("v" + x))));
+
+        IDictionary<Poco, Poco> res = await KvView.GetAllAsync(null, Enumerable.Range(-10, 20).Select(x => GetPoco(x)));
+
+        Assert.AreEqual(7, res.Count);
+
+        for (int i = -1; i < 6; i++)
+        {
+            Poco val = res.Single(x => x.Key.Key == i).Value;
+            Assert.AreEqual("v" + i, val.Val);
+        }
+    }
+
+    [Test]
+    public async Task TestGetAndPut()
+    {
+        Option<Poco> res1 = await KvView.GetAndPutAsync(null, GetPoco(1), GetPoco("1"));
+        Option<Poco> res2 = await KvView.GetAndPutAsync(null, GetPoco(1), GetPoco("2"));
+        Option<Poco> res3 = await KvView.GetAsync(null, GetPoco(1));
+
+        Assert.IsFalse(res1.HasValue);
+        Assert.IsTrue(res2.HasValue);
+        Assert.IsTrue(res3.HasValue);
+
+        Assert.AreEqual("1", res2.Value.Val);
+        Assert.AreEqual("2", res3.Value.Val);
+    }
+
+    [Test]
+    public async Task TestPutIfAbsent()
+    {
+        await KvView.PutAsync(null, GetPoco(1), GetPoco("1"));
+
+        bool res1 = await KvView.PutIfAbsentAsync(null, GetPoco(1), GetPoco("11"));
+        Option<Poco> res2 = await KvView.GetAsync(null, GetPoco(1));
+
+        bool res3 = await KvView.PutIfAbsentAsync(null, GetPoco(2), GetPoco("2"));
+        Option<Poco> res4 = await KvView.GetAsync(null, GetPoco(2));
+
+        Assert.IsFalse(res1);
+        Assert.AreEqual("1", res2.Value.Val);
+
+        Assert.IsTrue(res3);
+        Assert.AreEqual("2", res4.Value.Val);
+    }
+
+    [Test]
+    public async Task TestRemove()
+    {
+        await KvView.PutAsync(null, GetPoco(1), GetPoco("1"));
+
+        bool res1 = await KvView.RemoveAsync(null, GetPoco(1));
+        bool res2 = await KvView.RemoveAsync(null, GetPoco(2));
+        bool res3 = await KvView.ContainsAsync(null, GetPoco(1));
+
+        Assert.IsTrue(res1);
+        Assert.IsFalse(res2);
+        Assert.IsFalse(res3);
+    }
+
+    [Test]
+    public async Task TestRemoveExact()
+    {
+        await KvView.PutAsync(null, GetPoco(1), GetPoco("1"));
+
+        bool res1 = await KvView.RemoveAsync(null, GetPoco(1), GetPoco("111"));
+        bool res2 = await KvView.RemoveAsync(null, GetPoco(1), GetPoco("1"));
+        bool res3 = await KvView.ContainsAsync(null, GetPoco(1));
+
+        Assert.IsFalse(res1);
+        Assert.IsTrue(res2);
+        Assert.IsFalse(res3);
+    }
+
+    [Test]
+    public async Task TestRemoveAll()
+    {
+        await KvView.PutAsync(null, GetPoco(1), GetPoco("1"));
+
+        IList<Poco> res1 = await KvView.RemoveAllAsync(null, Enumerable.Range(-1, 8).Select(x => GetPoco(x, "foo")));
+        bool res2 = await KvView.ContainsAsync(null, GetPoco(1));
+
+        Assert.AreEqual(new[] { -1, 0, 2, 3, 4, 5, 6 }, res1.Select(x => x.Key).OrderBy(x => x));
+        Assert.IsFalse(res2);
+    }
+
+    [Test]
+    public async Task TestRemoveAllExact()
+    {
+        await KvView.PutAsync(null, GetPoco(1), GetPoco("1"));
+
+        IList<Poco> res1 = await KvView.RemoveAllAsync(
+            null,
+            Enumerable.Range(-1, 8).Select(x => new KeyValuePair<Poco, Poco>(GetPoco(x), GetPoco(x.ToString()))));
+
+        bool res2 = await KvView.ContainsAsync(null, GetPoco(1));
+
+        Assert.AreEqual(new[] { -1, 0, 2, 3, 4, 5, 6 }, res1.Select(x => x.Key).OrderBy(x => x));
+        Assert.IsFalse(res2);
+    }
+
+    [Test]
+    public async Task TestGetAndRemove()
+    {
+        await KvView.PutAsync(null, GetPoco(1), GetPoco("1"));
+
+        (Poco val1, bool hasVal1) = await KvView.GetAndRemoveAsync(null, GetPoco(1));
+        (Poco val2, bool hasVal2) = await KvView.GetAndRemoveAsync(null, GetPoco(1));
+
+        Assert.IsTrue(hasVal1);
+        Assert.AreEqual("1", val1.Val);
+
+        Assert.IsFalse(hasVal2);
+        Assert.IsNull(val2);
+    }
+
+    [Test]
+    public async Task TestReplace()
+    {
+        await KvView.PutAsync(null, GetPoco(1), GetPoco("1"));
+
+        bool res1 = await KvView.ReplaceAsync(null, GetPoco(0), GetPoco("00"));
+        Option<Poco> res2 = await KvView.GetAsync(null, GetPoco(0));
+
+        bool res3 = await KvView.ReplaceAsync(null, GetPoco(1), GetPoco("11"));
+        Option<Poco> res4 = await KvView.GetAsync(null, GetPoco(1));
+
+        Assert.IsFalse(res1);
+        Assert.IsFalse(res2.HasValue);
+
+        Assert.IsTrue(res3);
+        Assert.IsTrue(res4.HasValue);
+        Assert.AreEqual("11", res4.Value.Val);
+    }
+
+    [Test]
+    public async Task TestReplaceExact()
+    {
+        await KvView.PutAsync(null, GetPoco(1), GetPoco("1"));
+
+        bool res1 = await KvView.ReplaceAsync(transaction: null, key: GetPoco(0), oldVal: GetPoco("0"), newVal: GetPoco("00"));
+        Option<Poco> res2 = await KvView.GetAsync(null, GetPoco(0));
+
+        bool res3 = await KvView.ReplaceAsync(transaction: null, key: GetPoco(1), oldVal: GetPoco("1"), newVal: GetPoco("11"));
+        Option<Poco> res4 = await KvView.GetAsync(null, GetPoco(1));
+
+        bool res5 = await KvView.ReplaceAsync(transaction: null, key: GetPoco(2), oldVal: GetPoco("1"), newVal: GetPoco("22"));
+        Option<Poco> res6 = await KvView.GetAsync(null, GetPoco(1));
+
+        Assert.IsFalse(res1);
+        Assert.IsFalse(res2.HasValue);
+
+        Assert.IsTrue(res3);
+        Assert.IsTrue(res4.HasValue);
+        Assert.AreEqual("11", res4.Value.Val);
+
+        Assert.IsFalse(res5);
+        Assert.AreEqual("11", res6.Value.Val);
+    }
+
+    [Test]
+    public async Task TestGetAndReplace()
+    {
+        await KvView.PutAsync(null, GetPoco(1), GetPoco("1"));
+
+        Option<Poco> res1 = await KvView.GetAndReplaceAsync(null, GetPoco(0), GetPoco("00"));
+        Option<Poco> res2 = await KvView.GetAsync(null, GetPoco(0));
+
+        Option<Poco> res3 = await KvView.GetAndReplaceAsync(null, GetPoco(1), GetPoco("11"));
+        Option<Poco> res4 = await KvView.GetAsync(null, GetPoco(1));
+
+        Assert.IsFalse(res1.HasValue);
+        Assert.IsFalse(res2.HasValue);
+
+        Assert.IsTrue(res3.HasValue);
+        Assert.AreEqual("1", res3.Value.Val);
+
+        Assert.IsTrue(res4.HasValue);
+        Assert.AreEqual("11", res4.Value.Val);
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPrimitivePocoTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPrimitivePocoTests.cs
new file mode 100644
index 0000000000..b3f52b07fa
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPrimitivePocoTests.cs
@@ -0,0 +1,282 @@
+/*
+ * 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.Table;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Ignite.Table;
+using NUnit.Framework;
+
+/// <summary>
+/// Tests for key-value view where key is a primitive (long), and value is a user object (poco).
+/// </summary>
+public class KeyValueViewPrimitivePocoTests : IgniteTestsBase
+{
+    private IKeyValueView<long, Poco> KvView => Table.GetKeyValueView<long, Poco>();
+
+    [TearDown]
+    public async Task CleanTable()
+    {
+        await TupleView.DeleteAllAsync(null, Enumerable.Range(-1, 12).Select(x => GetTuple(x)));
+    }
+
+    [Test]
+    public async Task TestPutGet()
+    {
+        await KvView.PutAsync(null, 1L, GetPoco("val"));
+
+        (Poco res, bool hasRes) = await KvView.GetAsync(null, 1L);
+
+        Assert.IsTrue(hasRes);
+        Assert.AreEqual("val", res.Val);
+        Assert.AreEqual(0, res.Key);
+    }
+
+    [Test]
+    public async Task TestGetNonExistentKeyReturnsEmptyOption()
+    {
+        (Poco res, bool hasRes) = await KvView.GetAsync(null, -111L);
+
+        Assert.IsFalse(hasRes);
+        Assert.IsNull(res);
+    }
+
+    [Test]
+    public async Task TestGetAll()
+    {
+        await KvView.PutAsync(null, 7L, GetPoco("val1"));
+        await KvView.PutAsync(null, 8L, GetPoco("val2"));
+
+        IDictionary<long, Poco> res = await KvView.GetAllAsync(null, Enumerable.Range(-1, 100).Select(x => (long)x).ToList());
+        IDictionary<long, Poco> resEmpty = await KvView.GetAllAsync(null, Array.Empty<long>());
+
+        Assert.AreEqual(2, res.Count);
+        Assert.AreEqual("val1", res[7].Val);
+        Assert.AreEqual("val2", res[8].Val);
+
+        Assert.AreEqual(0, resEmpty.Count);
+    }
+
+    [Test]
+    public void TestPutNullThrowsArgumentException()
+    {
+        var valEx = Assert.ThrowsAsync<ArgumentNullException>(async () => await KvView.PutAsync(null, 1L, null!));
+        Assert.AreEqual("Value cannot be null. (Parameter 'val')", valEx!.Message);
+    }
+
+    [Test]
+    public async Task TestContains()
+    {
+        await KvView.PutAsync(null, 7L, GetPoco("val1"));
+
+        bool res1 = await KvView.ContainsAsync(null, 7L);
+        bool res2 = await KvView.ContainsAsync(null, 8L);
+
+        Assert.IsTrue(res1);
+        Assert.IsFalse(res2);
+    }
+
+    [Test]
+    public async Task TestPutAll()
+    {
+        await KvView.PutAllAsync(null, new Dictionary<long, Poco>());
+        await KvView.PutAllAsync(
+            null,
+            Enumerable.Range(-1, 7).Select(x => new KeyValuePair<long, Poco>(x, GetPoco("v" + x))));
+
+        IDictionary<long, Poco> res = await KvView.GetAllAsync(null, Enumerable.Range(-10, 20).Select(x => (long)x));
+
+        Assert.AreEqual(7, res.Count);
+
+        for (int i = -1; i < 6; i++)
+        {
+            Poco val = res[i];
+            Assert.AreEqual("v" + i, val.Val);
+        }
+    }
+
+    [Test]
+    public async Task TestGetAndPut()
+    {
+        Option<Poco> res1 = await KvView.GetAndPutAsync(null, 1, GetPoco("1"));
+        Option<Poco> res2 = await KvView.GetAndPutAsync(null, 1, GetPoco("2"));
+        Option<Poco> res3 = await KvView.GetAsync(null, 1);
+
+        Assert.IsFalse(res1.HasValue);
+        Assert.IsTrue(res2.HasValue);
+        Assert.IsTrue(res3.HasValue);
+
+        Assert.AreEqual("1", res2.Value.Val);
+        Assert.AreEqual("2", res3.Value.Val);
+    }
+
+    [Test]
+    public async Task TestPutIfAbsent()
+    {
+        await KvView.PutAsync(null, 1, GetPoco("1"));
+
+        bool res1 = await KvView.PutIfAbsentAsync(null, 1, GetPoco("11"));
+        Option<Poco> res2 = await KvView.GetAsync(null, 1);
+
+        bool res3 = await KvView.PutIfAbsentAsync(null, 2, GetPoco("2"));
+        Option<Poco> res4 = await KvView.GetAsync(null, 2);
+
+        Assert.IsFalse(res1);
+        Assert.AreEqual("1", res2.Value.Val);
+
+        Assert.IsTrue(res3);
+        Assert.AreEqual("2", res4.Value.Val);
+    }
+
+    [Test]
+    public async Task TestRemove()
+    {
+        await KvView.PutAsync(null, 1, GetPoco("1"));
+
+        bool res1 = await KvView.RemoveAsync(null, 1);
+        bool res2 = await KvView.RemoveAsync(null, 2);
+        bool res3 = await KvView.ContainsAsync(null, 1);
+
+        Assert.IsTrue(res1);
+        Assert.IsFalse(res2);
+        Assert.IsFalse(res3);
+    }
+
+    [Test]
+    public async Task TestRemoveExact()
+    {
+        await KvView.PutAsync(null, 1, GetPoco("1"));
+
+        bool res1 = await KvView.RemoveAsync(null, 1, GetPoco("111"));
+        bool res2 = await KvView.RemoveAsync(null, 1, GetPoco("1"));
+        bool res3 = await KvView.ContainsAsync(null, 1);
+
+        Assert.IsFalse(res1);
+        Assert.IsTrue(res2);
+        Assert.IsFalse(res3);
+    }
+
+    [Test]
+    public async Task TestRemoveAll()
+    {
+        await KvView.PutAsync(null, 1, GetPoco("1"));
+
+        IList<long> res1 = await KvView.RemoveAllAsync(null, Enumerable.Range(-1, 8).Select(x => (long)x));
+        bool res2 = await KvView.ContainsAsync(null, 1);
+
+        Assert.AreEqual(new[] { -1, 0, 2, 3, 4, 5, 6 }, res1.OrderBy(x => x));
+        Assert.IsFalse(res2);
+    }
+
+    [Test]
+    public async Task TestRemoveAllExact()
+    {
+        await KvView.PutAsync(null, 1, GetPoco("1"));
+
+        IList<long> res1 = await KvView.RemoveAllAsync(
+            null,
+            Enumerable.Range(-1, 8).Select(x => new KeyValuePair<long, Poco>(x, GetPoco(x.ToString()))));
+
+        bool res2 = await KvView.ContainsAsync(null, 1);
+
+        Assert.AreEqual(new[] { -1, 0, 2, 3, 4, 5, 6 }, res1.OrderBy(x => x));
+        Assert.IsFalse(res2);
+    }
+
+    [Test]
+    public async Task TestGetAndRemove()
+    {
+        await KvView.PutAsync(null, 1, GetPoco("1"));
+
+        (Poco val1, bool hasVal1) = await KvView.GetAndRemoveAsync(null, 1);
+        (Poco val2, bool hasVal2) = await KvView.GetAndRemoveAsync(null, 1);
+
+        Assert.IsTrue(hasVal1);
+        Assert.AreEqual("1", val1.Val);
+
+        Assert.IsFalse(hasVal2);
+        Assert.IsNull(val2);
+    }
+
+    [Test]
+    public async Task TestReplace()
+    {
+        await KvView.PutAsync(null, 1, GetPoco("1"));
+
+        bool res1 = await KvView.ReplaceAsync(null, 0, GetPoco("00"));
+        Option<Poco> res2 = await KvView.GetAsync(null, 0);
+
+        bool res3 = await KvView.ReplaceAsync(null, 1, GetPoco("11"));
+        Option<Poco> res4 = await KvView.GetAsync(null, 1);
+
+        Assert.IsFalse(res1);
+        Assert.IsFalse(res2.HasValue);
+
+        Assert.IsTrue(res3);
+        Assert.IsTrue(res4.HasValue);
+        Assert.AreEqual("11", res4.Value.Val);
+    }
+
+    [Test]
+    public async Task TestReplaceExact()
+    {
+        await KvView.PutAsync(null, 1, GetPoco("1"));
+
+        bool res1 = await KvView.ReplaceAsync(transaction: null, key: 0, oldVal: GetPoco("0"), newVal: GetPoco("00"));
+        Option<Poco> res2 = await KvView.GetAsync(null, 0);
+
+        bool res3 = await KvView.ReplaceAsync(transaction: null, key: 1, oldVal: GetPoco("1"), newVal: GetPoco("11"));
+        Option<Poco> res4 = await KvView.GetAsync(null, 1);
+
+        bool res5 = await KvView.ReplaceAsync(transaction: null, key: 2, oldVal: GetPoco("1"), newVal: GetPoco("22"));
+        Option<Poco> res6 = await KvView.GetAsync(null, 1);
+
+        Assert.IsFalse(res1);
+        Assert.IsFalse(res2.HasValue);
+
+        Assert.IsTrue(res3);
+        Assert.IsTrue(res4.HasValue);
+        Assert.AreEqual("11", res4.Value.Val);
+
+        Assert.IsFalse(res5);
+        Assert.AreEqual("11", res6.Value.Val);
+    }
+
+    [Test]
+    public async Task TestGetAndReplace()
+    {
+        await KvView.PutAsync(null, 1, GetPoco("1"));
+
+        Option<Poco> res1 = await KvView.GetAndReplaceAsync(null, 0, GetPoco("00"));
+        Option<Poco> res2 = await KvView.GetAsync(null, 0);
+
+        Option<Poco> res3 = await KvView.GetAndReplaceAsync(null, 1, GetPoco("11"));
+        Option<Poco> res4 = await KvView.GetAsync(null, 1);
+
+        Assert.IsFalse(res1.HasValue);
+        Assert.IsFalse(res2.HasValue);
+
+        Assert.IsTrue(res3.HasValue);
+        Assert.AreEqual("1", res3.Value.Val);
+
+        Assert.IsTrue(res4.HasValue);
+        Assert.AreEqual("11", res4.Value.Val);
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPrimitiveTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPrimitiveTests.cs
new file mode 100644
index 0000000000..20035837a2
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPrimitiveTests.cs
@@ -0,0 +1,328 @@
+/*
+ * 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.Table;
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Threading.Tasks;
+using Ignite.Table;
+using NodaTime;
+using NUnit.Framework;
+
+/// <summary>
+/// Tests for key-value POCO view.
+/// </summary>
+public class KeyValueViewPrimitiveTests : IgniteTestsBase
+{
+    private IKeyValueView<long, string> KvView => Table.GetKeyValueView<long, string>();
+
+    [TearDown]
+    public async Task CleanTable()
+    {
+        await TupleView.DeleteAllAsync(null, Enumerable.Range(-1, 12).Select(x => GetTuple(x)));
+    }
+
+    [Test]
+    public async Task TestPutGet()
+    {
+        await KvView.PutAsync(null, 1L, "val");
+
+        (string res, _) = await KvView.GetAsync(null, 1L);
+
+        Assert.AreEqual("val", res);
+    }
+
+    [Test]
+    public async Task TestGetNonExistentKeyReturnsEmptyOption()
+    {
+        (string res, bool hasRes) = await KvView.GetAsync(null, -111L);
+
+        Assert.IsFalse(hasRes);
+        Assert.IsNull(res);
+    }
+
+    [Test]
+    public async Task TestGetAll()
+    {
+        await KvView.PutAsync(null, 7L, "val1");
+        await KvView.PutAsync(null, 8L, "val2");
+
+        IDictionary<long, string> res = await KvView.GetAllAsync(null, Enumerable.Range(-1, 100).Select(x => (long)x).ToList());
+        IDictionary<long, string> resEmpty = await KvView.GetAllAsync(null, Array.Empty<long>());
+
+        Assert.AreEqual(2, res.Count);
+        Assert.AreEqual("val1", res[7L]);
+        Assert.AreEqual("val2", res[8L]);
+
+        Assert.AreEqual(0, resEmpty.Count);
+    }
+
+    [Test]
+    public void TestPutNullThrowsArgumentException()
+    {
+        var valEx = Assert.ThrowsAsync<ArgumentNullException>(async () => await KvView.PutAsync(null, 1L, null!));
+        Assert.AreEqual("Value cannot be null. (Parameter 'val')", valEx!.Message);
+    }
+
+    [Test]
+    public async Task TestContains()
+    {
+        await KvView.PutAsync(null, 7L, "val1");
+
+        bool res1 = await KvView.ContainsAsync(null, 7L);
+        bool res2 = await KvView.ContainsAsync(null, 8L);
+
+        Assert.IsTrue(res1);
+        Assert.IsFalse(res2);
+    }
+
+    [Test]
+    public async Task TestPutAll()
+    {
+        await KvView.PutAllAsync(null, new Dictionary<long, string>());
+        await KvView.PutAllAsync(
+            null,
+            Enumerable.Range(-1, 7).Select(x => new KeyValuePair<long, string>(x, "v" + x)));
+
+        IDictionary<long, string> res = await KvView.GetAllAsync(null, Enumerable.Range(-10, 20).Select(x => (long)x));
+
+        Assert.AreEqual(7, res.Count);
+
+        for (int i = -1; i < 6; i++)
+        {
+            string val = res[i];
+            Assert.AreEqual("v" + i, val);
+        }
+    }
+
+    [Test]
+    public async Task TestGetAndPut()
+    {
+        Option<string> res1 = await KvView.GetAndPutAsync(null, 1, "1");
+        Option<string> res2 = await KvView.GetAndPutAsync(null, 1, "2");
+        Option<string> res3 = await KvView.GetAsync(null, 1);
+
+        Assert.IsFalse(res1.HasValue);
+        Assert.IsTrue(res2.HasValue);
+        Assert.IsTrue(res3.HasValue);
+
+        Assert.AreEqual("1", res2.Value);
+        Assert.AreEqual("2", res3.Value);
+    }
+
+    [Test]
+    public async Task TestPutIfAbsent()
+    {
+        await KvView.PutAsync(null, 1, "1");
+
+        bool res1 = await KvView.PutIfAbsentAsync(null, 1, "11");
+        Option<string> res2 = await KvView.GetAsync(null, 1);
+
+        bool res3 = await KvView.PutIfAbsentAsync(null, 2, "2");
+        Option<string> res4 = await KvView.GetAsync(null, 2);
+
+        Assert.IsFalse(res1);
+        Assert.AreEqual("1", res2.Value);
+
+        Assert.IsTrue(res3);
+        Assert.AreEqual("2", res4.Value);
+    }
+
+    [Test]
+    public async Task TestRemove()
+    {
+        await KvView.PutAsync(null, 1, "1");
+
+        bool res1 = await KvView.RemoveAsync(null, 1);
+        bool res2 = await KvView.RemoveAsync(null, 2);
+        bool res3 = await KvView.ContainsAsync(null, 1);
+
+        Assert.IsTrue(res1);
+        Assert.IsFalse(res2);
+        Assert.IsFalse(res3);
+    }
+
+    [Test]
+    public async Task TestRemoveExact()
+    {
+        await KvView.PutAsync(null, 1, "1");
+
+        bool res1 = await KvView.RemoveAsync(null, 1, "111");
+        bool res2 = await KvView.RemoveAsync(null, 1, "1");
+        bool res3 = await KvView.ContainsAsync(null, 1);
+
+        Assert.IsFalse(res1);
+        Assert.IsTrue(res2);
+        Assert.IsFalse(res3);
+    }
+
+    [Test]
+    public async Task TestRemoveAll()
+    {
+        await KvView.PutAsync(null, 1, "1");
+
+        IList<long> res1 = await KvView.RemoveAllAsync(null, Enumerable.Range(-1, 8).Select(x => (long)x));
+        bool res2 = await KvView.ContainsAsync(null, 1);
+
+        Assert.AreEqual(new[] { -1, 0, 2, 3, 4, 5, 6 }, res1.OrderBy(x => x));
+        Assert.IsFalse(res2);
+    }
+
+    [Test]
+    public async Task TestRemoveAllExact()
+    {
+        await KvView.PutAsync(null, 1, "1");
+
+        IList<long> res1 = await KvView.RemoveAllAsync(
+            null,
+            Enumerable.Range(-1, 8).Select(x => new KeyValuePair<long, string>(x, x.ToString())));
+
+        bool res2 = await KvView.ContainsAsync(null, 1);
+
+        Assert.AreEqual(new[] { -1, 0, 2, 3, 4, 5, 6 }, res1.OrderBy(x => x));
+        Assert.IsFalse(res2);
+    }
+
+    [Test]
+    public async Task TestGetAndRemove()
+    {
+        await KvView.PutAsync(null, 1, "1");
+
+        (string val1, bool hasVal1) = await KvView.GetAndRemoveAsync(null, 1);
+        (string val2, bool hasVal2) = await KvView.GetAndRemoveAsync(null, 1);
+
+        Assert.IsTrue(hasVal1);
+        Assert.AreEqual("1", val1);
+
+        Assert.IsFalse(hasVal2);
+        Assert.IsNull(val2);
+    }
+
+    [Test]
+    public async Task TestReplace()
+    {
+        await KvView.PutAsync(null, 1, "1");
+
+        bool res1 = await KvView.ReplaceAsync(null, 0, "00");
+        Option<string> res2 = await KvView.GetAsync(null, 0);
+
+        bool res3 = await KvView.ReplaceAsync(null, 1, "11");
+        Option<string> res4 = await KvView.GetAsync(null, 1);
+
+        Assert.IsFalse(res1);
+        Assert.IsFalse(res2.HasValue);
+
+        Assert.IsTrue(res3);
+        Assert.IsTrue(res4.HasValue);
+        Assert.AreEqual("11", res4.Value);
+    }
+
+    [Test]
+    public async Task TestReplaceExact()
+    {
+        await KvView.PutAsync(null, 1, "1");
+
+        bool res1 = await KvView.ReplaceAsync(transaction: null, key: 0, oldVal: "0", newVal: "00");
+        Option<string> res2 = await KvView.GetAsync(null, 0);
+
+        bool res3 = await KvView.ReplaceAsync(transaction: null, key: 1, oldVal: "1", newVal: "11");
+        Option<string> res4 = await KvView.GetAsync(null, 1);
+
+        bool res5 = await KvView.ReplaceAsync(transaction: null, key: 2, oldVal: "1", newVal: "22");
+        Option<string> res6 = await KvView.GetAsync(null, 1);
+
+        Assert.IsFalse(res1);
+        Assert.IsFalse(res2.HasValue);
+
+        Assert.IsTrue(res3);
+        Assert.IsTrue(res4.HasValue);
+        Assert.AreEqual("11", res4.Value);
+
+        Assert.IsFalse(res5);
+        Assert.AreEqual("11", res6.Value);
+    }
+
+    [Test]
+    public async Task TestGetAndReplace()
+    {
+        await KvView.PutAsync(null, 1, "1");
+
+        Option<string> res1 = await KvView.GetAndReplaceAsync(null, 0, "00");
+        Option<string> res2 = await KvView.GetAsync(null, 0);
+
+        Option<string> res3 = await KvView.GetAndReplaceAsync(null, 1, "11");
+        Option<string> res4 = await KvView.GetAsync(null, 1);
+
+        Assert.IsFalse(res1.HasValue);
+        Assert.IsFalse(res2.HasValue);
+
+        Assert.IsTrue(res3.HasValue);
+        Assert.AreEqual("1", res3.Value);
+
+        Assert.IsTrue(res4.HasValue);
+        Assert.AreEqual("11", res4.Value);
+    }
+
+    [Test]
+    public async Task TestAllTypes()
+    {
+        await TestKey((sbyte)1, "TBL_INT8");
+        await TestKey((short)1, "TBL_INT16");
+        await TestKey(1, "TBL_INT32");
+        await TestKey(1L, "TBL_INT64");
+        await TestKey(1.1f, "TBL_FLOAT");
+        await TestKey(1.1d, "TBL_DOUBLE");
+        await TestKey(1.234m, "TBL_DECIMAL");
+        await TestKey("foo", "TBL_STRING");
+        await TestKey(new LocalDateTime(2022, 10, 13, 8, 4, 42), "TBL_DATETIME");
+        await TestKey(new LocalTime(3, 4, 5), "TBL_TIME");
+        await TestKey(Instant.FromUnixTimeMilliseconds(123456789101112), "TBL_TIMESTAMP");
+        await TestKey(new BigInteger(123456789101112), "TBL_NUMBER");
+        await TestKey(new byte[] { 1, 2, 3 }, "TBL_BYTES");
+        await TestKey(new BitArray(new[] { byte.MaxValue }), "TBL_BITMASK");
+    }
+
+    private static async Task TestKey<T>(T val, IKeyValueView<T, T> kvView)
+        where T : notnull
+    {
+        // Tests EmitKvWriter.
+        await kvView.PutAsync(null, val, val);
+
+        // Tests EmitKvValuePartReader.
+        var (getRes, _) = await kvView.GetAsync(null, val);
+
+        // Tests EmitKvReader.
+        var getAllRes = await kvView.GetAllAsync(null, new[] { val });
+
+        Assert.AreEqual(val, getRes);
+        Assert.AreEqual(val, getAllRes.Single().Value);
+    }
+
+    private async Task TestKey<T>(T val, string tableName)
+        where T : notnull
+    {
+        var table = await Client.Tables.GetTableAsync(tableName);
+
+        Assert.IsNotNull(table, tableName);
+
+        await TestKey(val, table!.GetKeyValueView<T, T>());
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
index becfff017e..a42bef6102 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
@@ -20,6 +20,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
     using System;
     using System.Collections.Concurrent;
     using System.Collections.Generic;
+    using System.Diagnostics;
     using System.Linq;
     using System.Reflection;
     using System.Reflection.Emit;
@@ -104,12 +105,17 @@ namespace Apache.Ignite.Internal.Table.Serialization
             var type = typeof(T);
 
             var method = new DynamicMethod(
-                name: "Write" + type.Name,
+                name: "Write" + type,
                 returnType: typeof(void),
                 parameterTypes: new[] { typeof(BinaryTupleBuilder).MakeByRefType(), typeof(Span<byte>), type },
                 m: typeof(IIgnite).Module,
                 skipVisibility: true);
 
+            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KvPair<,>))
+            {
+                return EmitKvWriter(schema, keyOnly, method);
+            }
+
             var il = method.GetILGenerator();
 
             var columns = schema.Columns;
@@ -182,17 +188,99 @@ namespace Apache.Ignite.Internal.Table.Serialization
             return (WriteDelegate<T>)method.CreateDelegate(typeof(WriteDelegate<T>));
         }
 
+        private static WriteDelegate<T> EmitKvWriter(Schema schema, bool keyOnly, DynamicMethod method)
+        {
+            var type = typeof(T);
+            var (keyType, valType, keyField, valField) = GetKeyValTypes();
+
+            var keyWriteMethod = BinaryTupleMethods.GetWriteMethodOrNull(keyType);
+            var valWriteMethod = BinaryTupleMethods.GetWriteMethodOrNull(valType);
+
+            var il = method.GetILGenerator();
+
+            var columns = schema.Columns;
+            var count = keyOnly ? schema.KeyColumnCount : columns.Count;
+
+            int mappedCount = 0;
+
+            for (var index = 0; index < count; index++)
+            {
+                var col = columns[index];
+
+                FieldInfo? fieldInfo;
+
+                if (keyWriteMethod != null && index == 0)
+                {
+                    fieldInfo = keyField;
+                }
+                else if (valWriteMethod != null && index == schema.KeyColumnCount)
+                {
+                    fieldInfo = valField;
+                }
+                else if ((col.IsKey && keyWriteMethod != null) || (!col.IsKey && valWriteMethod != null))
+                {
+                    fieldInfo = null;
+                }
+                else
+                {
+                    fieldInfo = (col.IsKey ? keyType : valType).GetFieldIgnoreCase(col.Name);
+                }
+
+                if (fieldInfo == null)
+                {
+                    il.Emit(OpCodes.Ldarg_0); // writer
+                    il.Emit(OpCodes.Ldarg_1); // noValueSet
+                    il.Emit(OpCodes.Call, BinaryTupleMethods.WriteNoValue);
+                }
+                else
+                {
+                    ValidateFieldType(fieldInfo, col);
+                    il.Emit(OpCodes.Ldarg_0); // writer
+                    il.Emit(OpCodes.Ldarg_2); // record
+
+                    var field = index < schema.KeyColumnCount ? keyField : valField;
+                    il.Emit(OpCodes.Ldfld, field);
+
+                    if (field != fieldInfo)
+                    {
+                        il.Emit(OpCodes.Ldfld, fieldInfo);
+                    }
+
+                    if (col.Type == ClientDataType.Decimal)
+                    {
+                        EmitLdcI4(il, col.Scale);
+                    }
+
+                    var writeMethod = BinaryTupleMethods.GetWriteMethod(fieldInfo.FieldType);
+                    il.Emit(OpCodes.Call, writeMethod);
+
+                    mappedCount++;
+                }
+            }
+
+            ValidateMappedCount(mappedCount, type, columns);
+
+            il.Emit(OpCodes.Ret);
+
+            return (WriteDelegate<T>)method.CreateDelegate(typeof(WriteDelegate<T>));
+        }
+
         private static ReadDelegate<T> EmitReader(Schema schema, bool keyOnly)
         {
             var type = typeof(T);
 
             var method = new DynamicMethod(
-                name: "Read" + type.Name,
+                name: "Read" + type,
                 returnType: type,
                 parameterTypes: new[] { typeof(BinaryTupleReader).MakeByRefType() },
                 m: typeof(IIgnite).Module,
                 skipVisibility: true);
 
+            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KvPair<,>))
+            {
+                return EmitKvReader(schema, keyOnly, method);
+            }
+
             var il = method.GetILGenerator();
 
             if (BinaryTupleMethods.GetReadMethodOrNull(type) is { } readMethod)
@@ -212,13 +300,38 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 return (ReadDelegate<T>)method.CreateDelegate(typeof(ReadDelegate<T>));
             }
 
-            var local = il.DeclareLocal(type);
+            var local = DeclareAndInitLocal(il, type);
 
-            il.Emit(OpCodes.Ldtoken, type);
-            il.Emit(OpCodes.Call, ReflectionUtils.GetTypeFromHandleMethod);
-            il.Emit(OpCodes.Call, ReflectionUtils.GetUninitializedObjectMethod);
+            var columns = schema.Columns;
+            var count = keyOnly ? schema.KeyColumnCount : columns.Count;
 
-            il.Emit(OpCodes.Stloc_0); // T res
+            for (var i = 0; i < count; i++)
+            {
+                var col = columns[i];
+                var fieldInfo = type.GetFieldIgnoreCase(col.Name);
+
+                EmitFieldRead(fieldInfo, il, col, i, local);
+            }
+
+            il.Emit(OpCodes.Ldloc_0); // res
+            il.Emit(OpCodes.Ret);
+
+            return (ReadDelegate<T>)method.CreateDelegate(typeof(ReadDelegate<T>));
+        }
+
+        private static ReadDelegate<T> EmitKvReader(Schema schema, bool keyOnly, DynamicMethod method)
+        {
+            var type = typeof(T);
+
+            var il = method.GetILGenerator();
+            var (keyType, valType, keyField, valField) = GetKeyValTypes();
+
+            var keyMethod = BinaryTupleMethods.GetReadMethodOrNull(keyType);
+            var valMethod = BinaryTupleMethods.GetReadMethodOrNull(valType);
+
+            var kvLocal = DeclareAndInitLocal(il, type);
+            var keyLocal = keyMethod == null ? DeclareAndInitLocal(il, keyType) : null;
+            var valLocal = valMethod == null ? DeclareAndInitLocal(il, valType) : null;
 
             var columns = schema.Columns;
             var count = keyOnly ? schema.KeyColumnCount : columns.Count;
@@ -226,11 +339,46 @@ namespace Apache.Ignite.Internal.Table.Serialization
             for (var i = 0; i < count; i++)
             {
                 var col = columns[i];
-                var fieldInfo = type.GetFieldIgnoreCase(col.Name);
+                FieldInfo? fieldInfo;
+                LocalBuilder? local;
+
+                if (i == 0 && keyMethod != null)
+                {
+                    fieldInfo = keyField;
+                    local = kvLocal;
+                }
+                else if (i == schema.KeyColumnCount && valMethod != null)
+                {
+                    fieldInfo = valField;
+                    local = kvLocal;
+                }
+                else
+                {
+                    local = col.IsKey ? keyLocal : valLocal;
+                    fieldInfo = local == null
+                        ? null
+                        : (col.IsKey ? keyType : valType).GetFieldIgnoreCase(col.Name);
+                }
 
                 EmitFieldRead(fieldInfo, il, col, i, local);
             }
 
+            // Copy Key to KvPair.
+            if (keyLocal != null)
+            {
+                il.Emit(OpCodes.Ldloca_S, kvLocal);
+                il.Emit(OpCodes.Ldloc, keyLocal);
+                il.Emit(OpCodes.Stfld, keyField);
+            }
+
+            // Copy Val to KvPair.
+            if (valLocal != null)
+            {
+                il.Emit(OpCodes.Ldloca_S, kvLocal);
+                il.Emit(OpCodes.Ldloc, valLocal);
+                il.Emit(OpCodes.Stfld, valField);
+            }
+
             il.Emit(OpCodes.Ldloc_0); // res
             il.Emit(OpCodes.Ret);
 
@@ -248,28 +396,20 @@ namespace Apache.Ignite.Internal.Table.Serialization
             }
 
             var method = new DynamicMethod(
-                name: "ReadValuePart" + type.Name,
+                name: "ReadValuePart" + type,
                 returnType: type,
                 parameterTypes: new[] { typeof(BinaryTupleReader).MakeByRefType(), type },
                 m: typeof(IIgnite).Module,
                 skipVisibility: true);
 
-            var il = method.GetILGenerator();
-            var local = il.DeclareLocal(type);
-
-            if (type.IsValueType)
+            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KvPair<,>))
             {
-                il.Emit(OpCodes.Ldloca_S, local);
-                il.Emit(OpCodes.Initobj, type);
-            }
-            else
-            {
-                il.Emit(OpCodes.Ldtoken, type);
-                il.Emit(OpCodes.Call, ReflectionUtils.GetTypeFromHandleMethod);
-                il.Emit(OpCodes.Call, ReflectionUtils.GetUninitializedObjectMethod);
-                il.Emit(OpCodes.Stloc_0); // T res
+                return EmitKvValuePartReader(schema, method);
             }
 
+            var il = method.GetILGenerator();
+            var local = DeclareAndInitLocal(il, type); // T res
+
             var columns = schema.Columns;
 
             for (var i = 0; i < columns.Count; i++)
@@ -277,7 +417,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 var col = columns[i];
                 var fieldInfo = type.GetFieldIgnoreCase(col.Name);
 
-                if (i < schema.KeyColumnCount)
+                if (col.IsKey)
                 {
                     if (fieldInfo != null)
                     {
@@ -299,9 +439,55 @@ namespace Apache.Ignite.Internal.Table.Serialization
             return (ReadValuePartDelegate<T>)method.CreateDelegate(typeof(ReadValuePartDelegate<T>));
         }
 
-        private static void EmitFieldRead(FieldInfo? fieldInfo, ILGenerator il, Column col, int elemIdx, LocalBuilder local)
+        private static ReadValuePartDelegate<T> EmitKvValuePartReader(Schema schema, DynamicMethod method)
+        {
+            var type = typeof(T);
+            var (_, valType, _, valField) = GetKeyValTypes();
+
+            var il = method.GetILGenerator();
+            var kvLocal = DeclareAndInitLocal(il, type);
+
+            var valReadMethod = BinaryTupleMethods.GetReadMethodOrNull(valType);
+
+            if (valReadMethod != null)
+            {
+                // Single-value mapping.
+                if (schema.Columns.Count == schema.KeyColumnCount)
+                {
+                    // No value columns.
+                    return (ref BinaryTupleReader _, T _) => default!;
+                }
+
+                EmitFieldRead(valField, il, schema.Columns[schema.KeyColumnCount], 0, kvLocal);
+            }
+            else
+            {
+                var valLocal = DeclareAndInitLocal(il, valType);
+                var columns = schema.Columns;
+
+                for (var i = schema.KeyColumnCount; i < columns.Count; i++)
+                {
+                    var col = columns[i];
+                    var fieldInfo = valType.GetFieldIgnoreCase(col.Name);
+
+                    EmitFieldRead(fieldInfo, il, col, i - schema.KeyColumnCount, valLocal);
+                }
+
+                // Copy Val to KvPair.
+                il.Emit(OpCodes.Ldloca_S, kvLocal);
+                il.Emit(OpCodes.Ldloc, valLocal);
+                il.Emit(OpCodes.Stfld, valField);
+            }
+
+            il.Emit(OpCodes.Ldloc_0); // res
+            il.Emit(OpCodes.Ret);
+
+            return (ReadValuePartDelegate<T>)method.CreateDelegate(typeof(ReadValuePartDelegate<T>));
+        }
+
+        private static void EmitFieldRead(FieldInfo? fieldInfo, ILGenerator il, Column col, int elemIdx, LocalBuilder? local)
         {
-            if (fieldInfo == null)
+            if (fieldInfo == null || local == null)
             {
                 return;
             }
@@ -310,7 +496,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
 
             var readMethod = BinaryTupleMethods.GetReadMethod(fieldInfo.FieldType);
 
-            il.Emit(fieldInfo.DeclaringType!.IsValueType ? OpCodes.Ldloca_S : OpCodes.Ldloc, local); // res
+            il.Emit(local.LocalType.IsValueType ? OpCodes.Ldloca_S : OpCodes.Ldloc, local); // res
             il.Emit(OpCodes.Ldarg_0); // reader
             EmitLdcI4(il, elemIdx); // index
 
@@ -408,5 +594,37 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 ErrorGroups.Client.Configuration,
                 $"Can't map '{type}' to columns '{columnStr}'. Matching fields not found.");
         }
+
+        private static LocalBuilder DeclareAndInitLocal(ILGenerator il, Type type)
+        {
+            var local = il.DeclareLocal(type);
+
+            if (type.IsValueType)
+            {
+                il.Emit(OpCodes.Ldloca_S, local);
+                il.Emit(OpCodes.Initobj, type);
+            }
+            else
+            {
+                il.Emit(OpCodes.Ldtoken, type);
+                il.Emit(OpCodes.Call, ReflectionUtils.GetTypeFromHandleMethod);
+                il.Emit(OpCodes.Call, ReflectionUtils.GetUninitializedObjectMethod);
+                il.Emit(OpCodes.Stloc, local);
+            }
+
+            return local;
+        }
+
+        private static (Type KeyType, Type ValType, FieldInfo KeyField, FieldInfo ValField) GetKeyValTypes()
+        {
+            var type = typeof(T);
+            Debug.Assert(
+                type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KvPair<,>),
+                "type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KvPair<,>)");
+
+            var keyValTypes = type.GetGenericArguments();
+
+            return (keyValTypes[0], keyValTypes[1], type.GetFieldIgnoreCase("Key")!, type.GetFieldIgnoreCase("Val")!);
+        }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
index 0098f7b986..15d95f614e 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
@@ -96,6 +96,12 @@ namespace Apache.Ignite.Internal.Table
         public IRecordView<T> GetRecordView<T>()
             where T : notnull => GetRecordViewInternal<T>();
 
+        /// <inheritdoc/>
+        public IKeyValueView<TK, TV> GetKeyValueView<TK, TV>()
+            where TK : notnull
+            where TV : notnull =>
+            new KeyValueView<TK, TV>(GetRecordViewInternal<KvPair<TK, TV>>());
+
         /// <summary>
         /// Gets the record view for the specified type.
         /// </summary>
diff --git a/modules/platforms/dotnet/Apache.Ignite/Table/ITable.cs b/modules/platforms/dotnet/Apache.Ignite/Table/ITable.cs
index 300f15243b..1ee9d3ce67 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Table/ITable.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Table/ITable.cs
@@ -47,5 +47,18 @@ namespace Apache.Ignite.Table
         /// <returns>Record view.</returns>
         public IRecordView<T> GetRecordView<T>() // TODO: Custom mapping (IGNITE-16356)
             where T : notnull;
+
+        /// <summary>
+        /// Gets the record view mapped to specified key and value types.
+        /// <para />
+        /// Table columns will be mapped to properties or fields by name, ignoring case. Any fields are supported,
+        /// including private and readonly.
+        /// </summary>
+        /// <typeparam name="TK">Key type.</typeparam>
+        /// <typeparam name="TV">Value type.</typeparam>
+        /// <returns>Key-value view.</returns>
+        public IKeyValueView<TK, TV> GetKeyValueView<TK, TV>()
+            where TK : notnull
+            where TV : notnull;
     }
 }