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/09/09 11:35:21 UTC

[ignite-3] branch main updated: IGNITE-17297 Adopt BinaryTuple format in the client protocol (#1058)

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 91f9cee9ab IGNITE-17297 Adopt BinaryTuple format in the client protocol (#1058)
91f9cee9ab is described below

commit 91f9cee9ab7130bad9b24458d563f4b06ec835a7
Author: Pavel Tupitsyn <pt...@apache.org>
AuthorDate: Fri Sep 9 14:35:15 2022 +0300

    IGNITE-17297 Adopt BinaryTuple format in the client protocol (#1058)
    
    Encode tuples and rows using BinaryTuple format in the thin client protocol. Update server, Java client, .NET client.
---
 modules/binary-tuple/pom.xml                       |  25 ----
 .../internal/binarytuple/BinaryTupleBuilder.java   |   9 ++
 modules/client-common/pom.xml                      |   4 +
 .../client/proto/ClientBinaryTupleUtils.java       | 120 +++++++++++++++++
 .../internal/client/proto/ClientMessagePacker.java |  32 ++++-
 .../client/proto/ClientMessageUnpacker.java        |  23 ++++
 .../proto/ClientMessagePackerUnpackerTest.java     |   2 +-
 .../handler/requests/table/ClientTableCommon.java  | 146 ++++++++-------------
 modules/client/pom.xml                             |   5 +
 .../internal/client/table/ClientKeyValueView.java  |  49 +++++--
 .../client/table/ClientRecordSerializer.java       |  69 +++++++---
 .../ignite/internal/client/table/ClientSchema.java |   7 -
 .../client/table/ClientTupleSerializer.java        | 144 +++++++++++++++++---
 .../marshaller/ClientMarshallerReader.java         |  71 ++++++----
 .../marshaller/ClientMarshallerWriter.java         |  47 ++++---
 .../ignite/client/AbstractClientTableTest.java     |   2 +-
 .../ignite/client/ClientKeyValueViewTest.java      |  14 +-
 .../apache/ignite/client/ClientRecordViewTest.java |  14 +-
 .../org/apache/ignite/client/ClientTableTest.java  |   3 +-
 .../org/apache/ignite/client/RetryPolicyTest.java  |  12 +-
 .../ignite/client/fakes/FakeIgniteTables.java      |   4 +-
 .../SerializerHandlerBenchmarksBase.cs             |   4 +-
 .../SerializerHandlerReadBenchmarks.cs             |  40 ++++--
 .../SerializerHandlerWriteBenchmarks.cs            |  13 +-
 .../Apache.Ignite.Tests/Compute/ComputeTests.cs    |   4 +-
 .../Proto/BinaryTuple/BinaryTupleTests.cs          |  86 +++++++-----
 .../Table/CustomTestIgniteTuple.cs                 |   2 +-
 .../Apache.Ignite.Tests/Table/IgniteTupleTests.cs  |   2 +-
 .../Table/{CustomTestIgniteTuple.cs => Poco2.cs}   |  37 +++---
 .../Table/RecordViewPocoTests.cs                   |  45 +++++++
 .../Serialization/ObjectSerializerHandlerTests.cs  |  51 +++----
 .../Proto/BinaryTuple/BinaryTupleBuilder.cs        | 104 +++++++++++++--
 .../Proto/BinaryTuple/BinaryTupleCommon.cs         |  15 ++-
 .../Proto/BinaryTuple/BinaryTupleReader.cs         |  43 +++++-
 .../Internal/Proto/MessagePackReaderExtensions.cs  |  14 ++
 .../Internal/Proto/MessagePackWriterExtensions.cs  |  18 +++
 .../Serialization/BinaryTupleBuilderExtensions.cs} |  36 ++---
 .../Table/Serialization/BinaryTupleMethods.cs      |  99 ++++++++++++++
 .../Table/Serialization/ByteSpanExtensions.cs}     |  35 ++---
 .../Table/Serialization/MessagePackMethods.cs      | 131 ------------------
 .../Table/Serialization/ObjectSerializerHandler.cs | 118 +++++++++++++----
 .../Table/Serialization/TupleSerializerHandler.cs  |  50 +++----
 42 files changed, 1144 insertions(+), 605 deletions(-)

diff --git a/modules/binary-tuple/pom.xml b/modules/binary-tuple/pom.xml
index 98effa4a21..6a3388b71f 100644
--- a/modules/binary-tuple/pom.xml
+++ b/modules/binary-tuple/pom.xml
@@ -52,29 +52,4 @@
         </dependency>
     </dependencies>
 
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-compiler-plugin</artifactId>
-                <dependencies>
-                    <dependency>
-                        <groupId>org.apache.ignite</groupId>
-                        <artifactId>ignite-configuration-annotation-processor</artifactId>
-                        <version>${project.version}</version>
-                    </dependency>
-                </dependencies>
-                <configuration>
-                    <annotationProcessorPaths>
-                        <path>
-                            <groupId>org.apache.ignite</groupId>
-                            <artifactId>ignite-configuration-annotation-processor</artifactId>
-                            <version>${project.version}</version>
-                        </path>
-                    </annotationProcessorPaths>
-                </configuration>
-            </plugin>
-        </plugins>
-    </build>
-
 </project>
diff --git a/modules/binary-tuple/src/main/java/org/apache/ignite/internal/binarytuple/BinaryTupleBuilder.java b/modules/binary-tuple/src/main/java/org/apache/ignite/internal/binarytuple/BinaryTupleBuilder.java
index 49737f650e..59b246aaa8 100644
--- a/modules/binary-tuple/src/main/java/org/apache/ignite/internal/binarytuple/BinaryTupleBuilder.java
+++ b/modules/binary-tuple/src/main/java/org/apache/ignite/internal/binarytuple/BinaryTupleBuilder.java
@@ -142,6 +142,15 @@ public class BinaryTupleBuilder {
         return proceed();
     }
 
+    /**
+     * Append a default (empty) value for the current element.
+     *
+     * @return {@code this} for chaining.
+     */
+    public BinaryTupleBuilder appendDefault() {
+        return proceed();
+    }
+
     /**
      * Append a value for the current element.
      *
diff --git a/modules/client-common/pom.xml b/modules/client-common/pom.xml
index 582912c670..ea1a667d2f 100644
--- a/modules/client-common/pom.xml
+++ b/modules/client-common/pom.xml
@@ -119,6 +119,10 @@
             <artifactId>jmh-generator-annprocess</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-binary-tuple</artifactId>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientBinaryTupleUtils.java b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientBinaryTupleUtils.java
new file mode 100644
index 0000000000..919bc74e7d
--- /dev/null
+++ b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientBinaryTupleUtils.java
@@ -0,0 +1,120 @@
+/*
+ * 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.
+ */
+
+package org.apache.ignite.internal.client.proto;
+
+import static org.apache.ignite.lang.ErrorGroups.Client.PROTOCOL_ERR;
+
+import org.apache.ignite.internal.binarytuple.BinaryTupleReader;
+import org.apache.ignite.lang.IgniteException;
+import org.apache.ignite.table.Tuple;
+
+/**
+ * Client binary tuple utils.
+ */
+public class ClientBinaryTupleUtils {
+    /**
+     * Reads a binary tuple column and sets the value in the specified tuple.
+     *
+     * @param reader         Binary tuple reader.
+     * @param readerIndex    Column index in the binary tuple.
+     * @param tuple          Target tuple.
+     * @param columnName     Column name.
+     * @param clientDataType Client data type (see {@link ClientDataType}).
+     */
+    public static void readAndSetColumnValue(
+            BinaryTupleReader reader,
+            int readerIndex,
+            Tuple tuple,
+            String columnName,
+            int clientDataType) {
+        if (reader.hasNullValue(readerIndex)) {
+            tuple.set(columnName, null);
+            return;
+        }
+
+        switch (clientDataType) {
+            case ClientDataType.INT8:
+                tuple.set(columnName, reader.byteValue(readerIndex));
+                break;
+
+            case ClientDataType.INT16:
+                tuple.set(columnName, reader.shortValue(readerIndex));
+                break;
+
+            case ClientDataType.INT32:
+                tuple.set(columnName, reader.intValue(readerIndex));
+                break;
+
+            case ClientDataType.INT64:
+                tuple.set(columnName, reader.longValue(readerIndex));
+                break;
+
+            case ClientDataType.FLOAT:
+                tuple.set(columnName, reader.floatValue(readerIndex));
+                break;
+
+            case ClientDataType.DOUBLE:
+                tuple.set(columnName, reader.doubleValue(readerIndex));
+                break;
+
+            case ClientDataType.DECIMAL:
+                // TODO IGNITE-17632: Get scale from schema.
+                tuple.set(columnName, reader.decimalValue(readerIndex, 100));
+                break;
+
+            case ClientDataType.UUID:
+                tuple.set(columnName, reader.uuidValue(readerIndex));
+                break;
+
+            case ClientDataType.STRING:
+                tuple.set(columnName, reader.stringValue(readerIndex));
+                break;
+
+            case ClientDataType.BYTES:
+                tuple.set(columnName, reader.bytesValue(readerIndex));
+                break;
+
+            case ClientDataType.BITMASK:
+                tuple.set(columnName, reader.bitmaskValue(readerIndex));
+                break;
+
+            case ClientDataType.NUMBER:
+                tuple.set(columnName, reader.numberValue(readerIndex));
+                break;
+
+            case ClientDataType.DATE:
+                tuple.set(columnName, reader.dateValue(readerIndex));
+                break;
+
+            case ClientDataType.TIME:
+                tuple.set(columnName, reader.timeValue(readerIndex));
+                break;
+
+            case ClientDataType.DATETIME:
+                tuple.set(columnName, reader.dateTimeValue(readerIndex));
+                break;
+
+            case ClientDataType.TIMESTAMP:
+                tuple.set(columnName, reader.timestampValue(readerIndex));
+                break;
+
+            default:
+                throw new IgniteException(PROTOCOL_ERR, "Unsupported type: " + clientDataType);
+        }
+    }
+}
diff --git a/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientMessagePacker.java b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientMessagePacker.java
index 2d0ff7063f..694cb494db 100644
--- a/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientMessagePacker.java
+++ b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientMessagePacker.java
@@ -25,6 +25,7 @@ import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufUtil;
 import java.math.BigDecimal;
 import java.math.BigInteger;
+import java.nio.ByteBuffer;
 import java.sql.Date;
 import java.sql.Time;
 import java.sql.Timestamp;
@@ -34,6 +35,7 @@ import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.util.BitSet;
 import java.util.UUID;
+import org.apache.ignite.internal.binarytuple.BinaryTupleBuilder;
 
 /**
  * ByteBuf-based MsgPack implementation. Replaces {@link org.msgpack.core.MessagePacker} to avoid
@@ -468,6 +470,20 @@ public class ClientMessagePacker implements AutoCloseable {
         buf.writeBytes(src);
     }
 
+    /**
+     * Writes a byte buffer to the output.
+     *
+     * <p>This method is used with {@link #packRawStringHeader(int)} or {@link #packBinaryHeader(int)}
+     * methods.
+     *
+     * @param src the data to add.
+     */
+    public void writePayload(ByteBuffer src) {
+        assert !closed : "Packer is closed";
+
+        buf.writeBytes(src);
+    }
+
     /**
      * Writes a byte array to the output.
      *
@@ -861,7 +877,7 @@ public class ClientMessagePacker implements AutoCloseable {
             packInt(ClientDataType.BITMASK);
             packBitSet((BitSet) obj);
         } else {
-            throw new UnsupportedOperationException("Custom objects are not supported");
+            throw new UnsupportedOperationException("Custom objects are not supported: " + cls);
         }
     }
 
@@ -887,6 +903,20 @@ public class ClientMessagePacker implements AutoCloseable {
         }
     }
 
+    /**
+     * Packs binary tuple with no-value set.
+     *
+     * @param builder Builder.
+     * @param noValueSet No-value bit set.
+     */
+    public void packBinaryTuple(BinaryTupleBuilder builder, BitSet noValueSet) {
+        packBitSet(noValueSet);
+
+        var buf = builder.build();
+        packBinaryHeader(buf.limit() - buf.position());
+        writePayload(buf);
+    }
+
     /**
      * {@inheritDoc}
      */
diff --git a/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientMessageUnpacker.java b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientMessageUnpacker.java
index 3201a9a75b..6d90259bc1 100644
--- a/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientMessageUnpacker.java
+++ b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientMessageUnpacker.java
@@ -39,6 +39,8 @@ import static org.msgpack.core.MessagePack.Code;
 import io.netty.buffer.ByteBuf;
 import java.math.BigDecimal;
 import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.nio.charset.StandardCharsets;
 import java.time.Instant;
 import java.time.LocalDate;
@@ -625,6 +627,27 @@ public class ClientMessageUnpacker implements AutoCloseable {
         return res;
     }
 
+    /**
+     * Reads a binary value.
+     * NOTE: Exposes internal pooled buffer to avoid copying. The buffer is not valid after current instance is closed.
+     *
+     * @return Payload bytes.
+     */
+    public ByteBuffer readBinaryUnsafe() {
+        assert refCnt > 0 : "Unpacker is closed";
+
+        var length = unpackBinaryHeader();
+        var idx = buf.readerIndex();
+
+        // Note: this may or may not avoid the actual copy.
+        ByteBuffer byteBuffer = buf.internalNioBuffer(idx, length).slice();
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+
+        buf.readerIndex(idx + length);
+
+        return byteBuffer;
+    }
+
     /**
      * Skips values.
      *
diff --git a/modules/client-common/src/test/java/org/apache/ignite/internal/client/proto/ClientMessagePackerUnpackerTest.java b/modules/client-common/src/test/java/org/apache/ignite/internal/client/proto/ClientMessagePackerUnpackerTest.java
index 6346a8de1c..b8cbcf0401 100644
--- a/modules/client-common/src/test/java/org/apache/ignite/internal/client/proto/ClientMessagePackerUnpackerTest.java
+++ b/modules/client-common/src/test/java/org/apache/ignite/internal/client/proto/ClientMessagePackerUnpackerTest.java
@@ -285,7 +285,7 @@ public class ClientMessagePackerUnpackerTest {
     @Test
     public void testObjectArray() {
         try (var packer = new ClientMessagePacker(PooledByteBufAllocator.DEFAULT.directBuffer())) {
-            Object[] args = new Object[]{(byte) 4, (short) 8, 15, 16L, 23.0f, 42.0d, "TEST_STRING", null, UUID.randomUUID(), false};
+            Object[] args = new Object[]{(byte) 4, (short) 8, 15, 16L, 23.0f, 42.0d, "TEST_STRING", null, UUID.randomUUID()};
             packer.packObjectArray(args);
 
             var buf = packer.getBuffer();
diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/table/ClientTableCommon.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/table/ClientTableCommon.java
index 59f990f865..1fa1a556ad 100644
--- a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/table/ClientTableCommon.java
+++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/table/ClientTableCommon.java
@@ -17,12 +17,12 @@
 
 package org.apache.ignite.client.handler.requests.table;
 
-import static org.apache.ignite.internal.client.proto.ClientMessageCommon.NO_VALUE;
 import static org.apache.ignite.lang.ErrorGroups.Client.PROTOCOL_ERR;
 import static org.apache.ignite.lang.ErrorGroups.Client.TABLE_ID_NOT_FOUND_ERR;
 
 import java.math.BigDecimal;
 import java.math.BigInteger;
+import java.nio.ByteBuffer;
 import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
@@ -32,6 +32,9 @@ import java.util.BitSet;
 import java.util.Collection;
 import java.util.UUID;
 import org.apache.ignite.client.handler.ClientResourceRegistry;
+import org.apache.ignite.internal.binarytuple.BinaryTupleBuilder;
+import org.apache.ignite.internal.binarytuple.BinaryTupleReader;
+import org.apache.ignite.internal.client.proto.ClientBinaryTupleUtils;
 import org.apache.ignite.internal.client.proto.ClientDataType;
 import org.apache.ignite.internal.client.proto.ClientMessagePacker;
 import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
@@ -51,7 +54,6 @@ import org.apache.ignite.table.manager.IgniteTables;
 import org.apache.ignite.tx.Transaction;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
-import org.msgpack.core.MessageTypeException;
 
 /**
  * Common table functionality.
@@ -87,24 +89,6 @@ public class ClientTableCommon {
         }
     }
 
-    /**
-     * Writes a tuple.
-     *
-     * @param packer Packer.
-     * @param tuple  Tuple.
-     */
-    public static void writeTupleOrNil(ClientMessagePacker packer, Tuple tuple) {
-        if (tuple == null) {
-            packer.packNil();
-
-            return;
-        }
-
-        var schema = ((SchemaAware) tuple).schema();
-
-        writeTuple(packer, tuple, schema);
-    }
-
     /**
      * Writes a tuple.
      *
@@ -123,40 +107,6 @@ public class ClientTableCommon {
         writeTuple(packer, tuple, schema, false, part);
     }
 
-    /**
-     * Writes a tuple.
-     *
-     * @param packer Packer.
-     * @param tuple  Tuple.
-     * @param schema Tuple schema.
-     * @throws IgniteException on failed serialization.
-     */
-    public static void writeTuple(
-            ClientMessagePacker packer,
-            Tuple tuple,
-            SchemaDescriptor schema
-    ) {
-        writeTuple(packer, tuple, schema, false, TuplePart.KEY_AND_VAL);
-    }
-
-    /**
-     * Writes a tuple.
-     *
-     * @param packer     Packer.
-     * @param tuple      Tuple.
-     * @param schema     Tuple schema.
-     * @param skipHeader Whether to skip the tuple header.
-     * @throws IgniteException on failed serialization.
-     */
-    public static void writeTuple(
-            ClientMessagePacker packer,
-            Tuple tuple,
-            SchemaDescriptor schema,
-            boolean skipHeader
-    ) {
-        writeTuple(packer, tuple, schema, skipHeader, TuplePart.KEY_AND_VAL);
-    }
-
     /**
      * Writes a tuple.
      *
@@ -180,17 +130,21 @@ public class ClientTableCommon {
             packer.packInt(schema.version());
         }
 
+        var builder = BinaryTupleBuilder.create(columnCount(schema, part), true);
+
         if (part != TuplePart.VAL) {
             for (var col : schema.keyColumns().columns()) {
-                writeColumnValue(packer, tuple, col);
+                writeColumnValue(builder, tuple, col);
             }
         }
 
         if (part != TuplePart.KEY) {
             for (var col : schema.valueColumns().columns()) {
-                writeColumnValue(packer, tuple, col);
+                writeColumnValue(builder, tuple, col);
             }
         }
+
+        packBinary(packer, builder.build());
     }
 
     /**
@@ -316,14 +270,22 @@ public class ClientTableCommon {
     ) {
         var cnt = keyOnly ? schema.keyColumns().length() : schema.length();
 
+        // NOTE: noValueSet is only present for client -> server communication.
+        // It helps disambiguate two cases: 1 - column value is not set, 2 - column value is set to null explicitly.
+        // If the column has a default value, it should be applied only in case 1.
+        // https://cwiki.apache.org/confluence/display/IGNITE/IEP-76+Thin+Client+Protocol+for+Ignite+3.0#IEP76ThinClientProtocolforIgnite3.0-NullvsNoValue
+        var noValueSet = unpacker.unpackBitSet();
+        var binaryTupleReader = new BinaryTupleReader(cnt, unpacker.readBinaryUnsafe());
         var tuple = Tuple.create(cnt);
 
         for (int i = 0; i < cnt; i++) {
-            if (unpacker.tryUnpackNoValue()) {
+            if (noValueSet.get(i)) {
                 continue;
             }
 
-            readAndSetColumnValue(unpacker, tuple, schema.column(i));
+            Column column = schema.column(i);
+            ClientBinaryTupleUtils.readAndSetColumnValue(
+                    binaryTupleReader, i, tuple, column.name(), getClientDataType(column.type().spec()));
         }
 
         return tuple;
@@ -410,16 +372,6 @@ public class ClientTableCommon {
         }
     }
 
-    private static void readAndSetColumnValue(ClientMessageUnpacker unpacker, Tuple tuple, Column col) {
-        try {
-            int type = getClientDataType(col.type().spec());
-            Object val = unpacker.unpackObject(type);
-            tuple.set(col.name(), val);
-        } catch (MessageTypeException e) {
-            throw new IgniteException(PROTOCOL_ERR, "Incorrect value type for column '" + col.name() + "': " + e.getMessage(), e);
-        }
-    }
-
     private static int getClientDataType(NativeTypeSpec spec) {
         switch (spec) {
             case INT8:
@@ -475,88 +427,94 @@ public class ClientTableCommon {
         }
     }
 
-    private static void writeColumnValue(ClientMessagePacker packer, Tuple tuple, Column col) {
-        var val = tuple.valueOrDefault(col.name(), NO_VALUE);
+    private static void writeColumnValue(BinaryTupleBuilder builder, Tuple tuple, Column col) {
+        var val = tuple.valueOrDefault(col.name(), null);
 
         if (val == null) {
-            packer.packNil();
-            return;
-        }
-
-        if (val == NO_VALUE) {
-            packer.packNoValue();
+            builder.appendNull();
             return;
         }
 
         switch (col.type().spec()) {
             case INT8:
-                packer.packByte((byte) val);
+                builder.appendByte((byte) val);
                 break;
 
             case INT16:
-                packer.packShort((short) val);
+                builder.appendShort((short) val);
                 break;
 
             case INT32:
-                packer.packInt((int) val);
+                builder.appendInt((int) val);
                 break;
 
             case INT64:
-                packer.packLong((long) val);
+                builder.appendLong((long) val);
                 break;
 
             case FLOAT:
-                packer.packFloat((float) val);
+                builder.appendFloat((float) val);
                 break;
 
             case DOUBLE:
-                packer.packDouble((double) val);
+                builder.appendDouble((double) val);
                 break;
 
             case DECIMAL:
-                packer.packDecimal((BigDecimal) val);
+                builder.appendDecimalNotNull((BigDecimal) val);
                 break;
 
             case NUMBER:
-                packer.packNumber((BigInteger) val);
+                builder.appendNumberNotNull((BigInteger) val);
                 break;
 
             case UUID:
-                packer.packUuid((UUID) val);
+                builder.appendUuidNotNull((UUID) val);
                 break;
 
             case STRING:
-                packer.packString((String) val);
+                builder.appendStringNotNull((String) val);
                 break;
 
             case BYTES:
-                byte[] bytes = (byte[]) val;
-                packer.packBinaryHeader(bytes.length);
-                packer.writePayload(bytes);
+                builder.appendBytesNotNull((byte[]) val);
                 break;
 
             case BITMASK:
-                packer.packBitSet((BitSet) val);
+                builder.appendBitmaskNotNull((BitSet) val);
                 break;
 
             case DATE:
-                packer.packDate((LocalDate) val);
+                builder.appendDateNotNull((LocalDate) val);
                 break;
 
             case TIME:
-                packer.packTime((LocalTime) val);
+                builder.appendTimeNotNull((LocalTime) val);
                 break;
 
             case DATETIME:
-                packer.packDateTime((LocalDateTime) val);
+                builder.appendDateTimeNotNull((LocalDateTime) val);
                 break;
 
             case TIMESTAMP:
-                packer.packTimestamp((Instant) val);
+                builder.appendTimestampNotNull((Instant) val);
                 break;
 
             default:
                 throw new IgniteException(PROTOCOL_ERR, "Data type not supported: " + col.type());
         }
     }
+
+    private static int columnCount(SchemaDescriptor schema, TuplePart part) {
+        switch (part) {
+            case KEY: return schema.keyColumns().length();
+            case VAL: return schema.valueColumns().length();
+            default: return schema.length();
+        }
+    }
+
+    private static void packBinary(ClientMessagePacker packer, ByteBuffer buf) {
+        packer.packBinaryHeader(buf.limit() - buf.position());
+        packer.writePayload(buf);
+    }
 }
diff --git a/modules/client/pom.xml b/modules/client/pom.xml
index 3a90c19538..2208d93d40 100644
--- a/modules/client/pom.xml
+++ b/modules/client/pom.xml
@@ -79,6 +79,11 @@
             <artifactId>netty-codec</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-binary-tuple</artifactId>
+        </dependency>
+
         <!-- Test dependencies -->
         <dependency>
             <groupId>org.hamcrest</groupId>
diff --git a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientKeyValueView.java b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientKeyValueView.java
index bad6df3be8..30014e3a2c 100644
--- a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientKeyValueView.java
+++ b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientKeyValueView.java
@@ -22,6 +22,7 @@ import static org.apache.ignite.internal.client.table.ClientTable.writeTx;
 import static org.apache.ignite.lang.ErrorGroups.Common.UNKNOWN_ERR;
 
 import java.io.Serializable;
+import java.util.BitSet;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -30,11 +31,14 @@ import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
+import org.apache.ignite.internal.binarytuple.BinaryTupleBuilder;
+import org.apache.ignite.internal.binarytuple.BinaryTupleReader;
 import org.apache.ignite.internal.client.PayloadOutputChannel;
 import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
 import org.apache.ignite.internal.client.proto.ClientOp;
 import org.apache.ignite.internal.client.proto.TuplePart;
 import org.apache.ignite.internal.marshaller.ClientMarshallerReader;
+import org.apache.ignite.internal.marshaller.ClientMarshallerWriter;
 import org.apache.ignite.internal.marshaller.Marshaller;
 import org.apache.ignite.internal.marshaller.MarshallerException;
 import org.apache.ignite.lang.IgniteException;
@@ -192,14 +196,11 @@ public class ClientKeyValueView<K, V> implements KeyValueView<K, V> {
         return tbl.doSchemaOutOpAsync(
                 ClientOp.TUPLE_UPSERT_ALL,
                 (s, w) -> {
-                    w.out().packUuid(tbl.tableId());
-                    writeTx(tx, w);
-                    w.out().packInt(s.version());
+                    writeSchemaAndTx(s, w, tx);
                     w.out().packInt(pairs.size());
 
                     for (Entry<K, V> e : pairs.entrySet()) {
-                        keySer.writeRecRaw(e.getKey(), s, w.out(), TuplePart.KEY);
-                        valSer.writeRecRaw(e.getValue(), s, w.out(), TuplePart.VAL);
+                        writeKeyValueRaw(s, w, e.getKey(), e.getValue());
                     }
                 },
                 r -> null);
@@ -370,11 +371,9 @@ public class ClientKeyValueView<K, V> implements KeyValueView<K, V> {
         return tbl.doSchemaOutOpAsync(
                 ClientOp.TUPLE_REPLACE_EXACT,
                 (s, w) -> {
-                    keySer.writeRec(tx, key, s, w, TuplePart.KEY);
-                    valSer.writeRecRaw(oldVal, s, w.out(), TuplePart.VAL);
-
-                    keySer.writeRecRaw(key, s, w.out(), TuplePart.KEY);
-                    valSer.writeRecRaw(newVal, s, w.out(), TuplePart.VAL);
+                    writeSchemaAndTx(s, w, tx);
+                    writeKeyValueRaw(s, w, key, oldVal);
+                    writeKeyValueRaw(s, w, key, newVal);
                 },
                 ClientMessageUnpacker::unpackBoolean);
     }
@@ -454,8 +453,29 @@ public class ClientKeyValueView<K, V> implements KeyValueView<K, V> {
     }
 
     private void writeKeyValue(ClientSchema s, PayloadOutputChannel w, @Nullable Transaction tx, @NotNull K key, V val) {
-        keySer.writeRec(tx, key, s, w, TuplePart.KEY);
-        valSer.writeRecRaw(val, s, w.out(), TuplePart.VAL);
+        writeSchemaAndTx(s, w, tx);
+        writeKeyValueRaw(s, w, key, val);
+    }
+
+    private void writeKeyValueRaw(ClientSchema s, PayloadOutputChannel w, @NotNull K key, V val) {
+        var builder = BinaryTupleBuilder.create(s.columns().length, true);
+        var noValueSet = new BitSet();
+        ClientMarshallerWriter writer = new ClientMarshallerWriter(builder, noValueSet);
+
+        try {
+            s.getMarshaller(keySer.mapper(), TuplePart.KEY).writeObject(key, writer);
+            s.getMarshaller(valSer.mapper(), TuplePart.VAL).writeObject(val, writer);
+        } catch (MarshallerException e) {
+            throw new IgniteException(UNKNOWN_ERR, e.getMessage(), e);
+        }
+
+        w.out().packBinaryTuple(builder, noValueSet);
+    }
+
+    private void writeSchemaAndTx(ClientSchema s, PayloadOutputChannel w, @Nullable Transaction tx) {
+        w.out().packUuid(tbl.tableId());
+        writeTx(tx, w);
+        w.out().packInt(s.version());
     }
 
     private HashMap<K, V> readGetAllResponse(ClientSchema schema, ClientMessageUnpacker in) {
@@ -466,11 +486,12 @@ public class ClientKeyValueView<K, V> implements KeyValueView<K, V> {
         Marshaller keyMarsh = schema.getMarshaller(keySer.mapper(), TuplePart.KEY);
         Marshaller valMarsh = schema.getMarshaller(valSer.mapper(), TuplePart.VAL);
 
-        var reader = new ClientMarshallerReader(in);
-
         try {
             for (int i = 0; i < cnt; i++) {
                 in.unpackBoolean(); // TODO: Optimize (IGNITE-16022).
+
+                var tupleReader = new BinaryTupleReader(schema.columns().length, in.readBinaryUnsafe());
+                var reader = new ClientMarshallerReader(tupleReader);
                 res.put((K) keyMarsh.readObject(reader, null), (V) valMarsh.readObject(reader, null));
             }
 
diff --git a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientRecordSerializer.java b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientRecordSerializer.java
index 1395fde606..cf772175b2 100644
--- a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientRecordSerializer.java
+++ b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientRecordSerializer.java
@@ -21,9 +21,12 @@ import static org.apache.ignite.internal.client.table.ClientTable.writeTx;
 import static org.apache.ignite.lang.ErrorGroups.Common.UNKNOWN_ERR;
 
 import java.util.ArrayList;
+import java.util.BitSet;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.UUID;
+import org.apache.ignite.internal.binarytuple.BinaryTupleBuilder;
+import org.apache.ignite.internal.binarytuple.BinaryTupleReader;
 import org.apache.ignite.internal.client.PayloadOutputChannel;
 import org.apache.ignite.internal.client.proto.ClientMessagePacker;
 import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
@@ -88,11 +91,27 @@ public class ClientRecordSerializer<R> {
      * @param <R> Record type.
      */
     public static <R> void writeRecRaw(@Nullable R rec, Mapper<R> mapper, ClientSchema schema, ClientMessagePacker out, TuplePart part) {
-        Marshaller marshaller = schema.getMarshaller(mapper, part);
-        ClientMarshallerWriter writer = new ClientMarshallerWriter(out);
+        writeRecRaw(rec, out, schema.getMarshaller(mapper, part), columnCount(schema, part));
+    }
 
+    /**
+     * Writes a record without header.
+     *
+     * @param rec Record.
+     * @param out Writer.
+     * @param marshaller Marshaller.
+     * @param columnCount Column count.
+     * @param <R> Record type.
+     */
+    static <R> void writeRecRaw(@Nullable R rec, ClientMessagePacker out, Marshaller marshaller, int columnCount) {
         try {
+            var builder = BinaryTupleBuilder.create(columnCount, true);
+            var noValueSet = new BitSet();
+
+            var writer = new ClientMarshallerWriter(builder, noValueSet);
             marshaller.writeObject(rec, writer);
+
+            out.packBinaryTuple(builder, noValueSet);
         } catch (MarshallerException e) {
             throw new IgniteException(UNKNOWN_ERR, e.getMessage(), e);
         }
@@ -123,14 +142,10 @@ public class ClientRecordSerializer<R> {
         out.out().packInt(schema.version());
 
         Marshaller marshaller = schema.getMarshaller(mapper, part);
-        ClientMarshallerWriter writer = new ClientMarshallerWriter(out.out());
+        int columnCount = columnCount(schema, part);
 
-        try {
-            marshaller.writeObject(rec, writer);
-            marshaller.writeObject(rec2, writer);
-        } catch (MarshallerException e) {
-            throw new IgniteException(UNKNOWN_ERR, e.getMessage(), e);
-        }
+        writeRecRaw(rec, out.out(), marshaller, columnCount);
+        writeRecRaw(rec2, out.out(), marshaller, columnCount);
     }
 
     void writeRecs(
@@ -146,14 +161,10 @@ public class ClientRecordSerializer<R> {
         out.out().packInt(recs.size());
 
         Marshaller marshaller = schema.getMarshaller(mapper, part);
-        ClientMarshallerWriter writer = new ClientMarshallerWriter(out.out());
+        int columnCount = columnCount(schema, part);
 
-        try {
-            for (R rec : recs) {
-                marshaller.writeObject(rec, writer);
-            }
-        } catch (MarshallerException e) {
-            throw new IgniteException(UNKNOWN_ERR, e.getMessage(), e);
+        for (R rec : recs) {
+            writeRecRaw(rec, out.out(), marshaller, columnCount);
         }
     }
 
@@ -167,13 +178,14 @@ public class ClientRecordSerializer<R> {
         var res = new ArrayList<R>(cnt);
 
         Marshaller marshaller = schema.getMarshaller(mapper, part);
-        var reader = new ClientMarshallerReader(in);
 
         try {
             for (int i = 0; i < cnt; i++) {
                 if (nullable && !in.unpackBoolean()) {
                     res.add(null);
                 } else {
+                    var tupleReader = new BinaryTupleReader(columnCount(schema, part), in.readBinaryUnsafe());
+                    var reader = new ClientMarshallerReader(tupleReader);
                     res.add((R) marshaller.readObject(reader, null));
                 }
             }
@@ -186,7 +198,9 @@ public class ClientRecordSerializer<R> {
 
     R readRec(ClientSchema schema, ClientMessageUnpacker in, TuplePart part) {
         Marshaller marshaller = schema.getMarshaller(mapper, part);
-        ClientMarshallerReader reader = new ClientMarshallerReader(in);
+
+        var tupleReader = new BinaryTupleReader(columnCount(schema, part), in.readBinaryUnsafe());
+        ClientMarshallerReader reader = new ClientMarshallerReader(tupleReader);
 
         try {
             return (R) marshaller.readObject(reader, null);
@@ -203,7 +217,8 @@ public class ClientRecordSerializer<R> {
         Marshaller keyMarshaller = schema.getMarshaller(mapper, TuplePart.KEY);
         Marshaller valMarshaller = schema.getMarshaller(mapper, TuplePart.VAL);
 
-        ClientMarshallerReader reader = new ClientMarshallerReader(in);
+        var tupleReader = new BinaryTupleReader(schema.columns().length - schema.keyColumnCount(), in.readBinaryUnsafe());
+        ClientMarshallerReader reader = new ClientMarshallerReader(tupleReader);
 
         try {
             var res = (R) valMarshaller.readObject(reader, null);
@@ -215,4 +230,20 @@ public class ClientRecordSerializer<R> {
             throw new IgniteException(UNKNOWN_ERR, e.getMessage(), e);
         }
     }
+
+    private static int columnCount(ClientSchema schema, TuplePart part) {
+        switch (part) {
+            case KEY:
+                return schema.keyColumnCount();
+
+            case VAL:
+                return schema.columns().length - schema.keyColumnCount();
+
+            case KEY_AND_VAL:
+                return schema.columns().length;
+
+            default:
+                throw new IllegalArgumentException();
+        }
+    }
 }
diff --git a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientSchema.java b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientSchema.java
index 88fbbc6989..25614b0bff 100644
--- a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientSchema.java
+++ b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientSchema.java
@@ -17,9 +17,7 @@
 
 package org.apache.ignite.internal.client.table;
 
-import static org.apache.ignite.internal.client.proto.ClientDataType.BIGINTEGER;
 import static org.apache.ignite.internal.client.proto.ClientDataType.BITMASK;
-import static org.apache.ignite.internal.client.proto.ClientDataType.BOOLEAN;
 import static org.apache.ignite.internal.client.proto.ClientDataType.BYTES;
 import static org.apache.ignite.internal.client.proto.ClientDataType.DATE;
 import static org.apache.ignite.internal.client.proto.ClientDataType.DATETIME;
@@ -174,9 +172,6 @@ public class ClientSchema {
 
     private static BinaryMode mode(int dataType) {
         switch (dataType) {
-            case BOOLEAN:
-                throw new IgniteException("TODO: " + dataType);
-
             case INT8:
                 return BinaryMode.BYTE;
 
@@ -207,8 +202,6 @@ public class ClientSchema {
             case DECIMAL:
                 return BinaryMode.DECIMAL;
 
-            // Falls through.
-            case BIGINTEGER:
             case NUMBER:
                 return BinaryMode.NUMBER;
 
diff --git a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientTupleSerializer.java b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientTupleSerializer.java
index 97d65ddec8..c358ea5e90 100644
--- a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientTupleSerializer.java
+++ b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientTupleSerializer.java
@@ -19,15 +19,28 @@ package org.apache.ignite.internal.client.table;
 
 import static org.apache.ignite.internal.client.proto.ClientMessageCommon.NO_VALUE;
 import static org.apache.ignite.internal.client.table.ClientTable.writeTx;
-
+import static org.apache.ignite.lang.ErrorGroups.Client.PROTOCOL_ERR;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
 import java.util.ArrayList;
+import java.util.BitSet;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.UUID;
+import org.apache.ignite.internal.binarytuple.BinaryTupleBuilder;
+import org.apache.ignite.internal.binarytuple.BinaryTupleReader;
 import org.apache.ignite.internal.client.PayloadOutputChannel;
+import org.apache.ignite.internal.client.proto.ClientBinaryTupleUtils;
+import org.apache.ignite.internal.client.proto.ClientDataType;
 import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
 import org.apache.ignite.lang.IgniteBiTuple;
+import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.table.Tuple;
 import org.apache.ignite.tx.Transaction;
 import org.jetbrains.annotations.NotNull;
@@ -121,13 +134,17 @@ public class ClientTupleSerializer {
         var columns = schema.columns();
         var count = keyOnly ? schema.keyColumnCount() : columns.length;
 
+        var builder = BinaryTupleBuilder.create(count, true);
+        var noValueSet = new BitSet(count);
+
         for (var i = 0; i < count; i++) {
             var col = columns[i];
-
             Object v = tuple.valueOrDefault(col.name(), NO_VALUE);
 
-            out.out().packObject(v);
+            appendValue(builder, noValueSet, col, v);
         }
+
+        out.out().packBinaryTuple(builder, noValueSet);
     }
 
     /**
@@ -154,6 +171,8 @@ public class ClientTupleSerializer {
         }
 
         var columns = schema.columns();
+        var noValueSet = new BitSet(columns.length);
+        var builder = BinaryTupleBuilder.create(columns.length, true);
 
         for (var i = 0; i < columns.length; i++) {
             var col = columns[i];
@@ -164,8 +183,10 @@ public class ClientTupleSerializer {
                             ? val.valueOrDefault(col.name(), NO_VALUE)
                             : NO_VALUE;
 
-            out.out().packObject(v);
+            appendValue(builder, noValueSet, col, v);
         }
+
+        out.out().packBinaryTuple(builder, noValueSet);
     }
 
     /**
@@ -216,8 +237,11 @@ public class ClientTupleSerializer {
 
         var colCnt = keyOnly ? schema.keyColumnCount() : schema.columns().length;
 
+        var binTuple = new BinaryTupleReader(colCnt, in.readBinaryUnsafe());
+
         for (var i = 0; i < colCnt; i++) {
-            tuple.setInternal(i, in.unpackObject(schema.columns()[i].type()));
+            ClientColumn column = schema.columns()[i];
+            ClientBinaryTupleUtils.readAndSetColumnValue(binTuple, i, tuple, column.name(), column.type());
         }
 
         return tuple;
@@ -225,15 +249,16 @@ public class ClientTupleSerializer {
 
     static Tuple readValueTuple(ClientSchema schema, ClientMessageUnpacker in, Tuple keyTuple) {
         var tuple = new ClientTuple(schema);
+        var binTuple = new BinaryTupleReader(schema.columns().length - schema.keyColumnCount(), in.readBinaryUnsafe());
 
         for (var i = 0; i < schema.columns().length; i++) {
             ClientColumn col = schema.columns()[i];
 
-            Object value = i < schema.keyColumnCount()
-                    ? keyTuple.value(col.name())
-                    : in.unpackObject(schema.columns()[i].type());
-
-            tuple.setInternal(i, value);
+            if (i < schema.keyColumnCount()) {
+                tuple.setInternal(i, keyTuple.value(col.name()));
+            } else {
+                ClientBinaryTupleUtils.readAndSetColumnValue(binTuple, i - schema.keyColumnCount(), tuple, col.name(), col.type());
+            }
         }
 
         return tuple;
@@ -244,12 +269,12 @@ public class ClientTupleSerializer {
         var colCnt = schema.columns().length;
 
         var valTuple = new ClientTuple(schema, keyColCnt, schema.columns().length - 1);
+        var binTupleReader = new BinaryTupleReader(colCnt - keyColCnt, in.readBinaryUnsafe());
 
         for (var i = keyColCnt; i < colCnt; i++) {
             ClientColumn col = schema.columns()[i];
-            Object val = in.unpackObject(col.type());
-
-            valTuple.setInternal(i - keyColCnt, val);
+            ClientBinaryTupleUtils.readAndSetColumnValue(
+                    binTupleReader, i - keyColCnt, valTuple, col.name(), col.type());
         }
 
         return valTuple;
@@ -262,14 +287,15 @@ public class ClientTupleSerializer {
         var keyTuple = new ClientTuple(schema, 0, keyColCnt - 1);
         var valTuple = new ClientTuple(schema, keyColCnt, schema.columns().length - 1);
 
+        var binTuple = new BinaryTupleReader(colCnt, in.readBinaryUnsafe());
+
         for (var i = 0; i < colCnt; i++) {
             ClientColumn col = schema.columns()[i];
-            Object val = in.unpackObject(col.type());
 
             if (i < keyColCnt) {
-                keyTuple.setInternal(i, val);
+                ClientBinaryTupleUtils.readAndSetColumnValue(binTuple, i, keyTuple, col.name(), col.type());
             } else {
-                valTuple.setInternal(i - keyColCnt, val);
+                ClientBinaryTupleUtils.readAndSetColumnValue(binTuple, i, valTuple, col.name(), col.type());
             }
         }
 
@@ -329,4 +355,90 @@ public class ClientTupleSerializer {
 
         return res;
     }
+
+    private static void appendValue(BinaryTupleBuilder builder, BitSet noValueSet, ClientColumn col, Object v) {
+        if (v == null) {
+            builder.appendNull();
+            return;
+        }
+
+        if (v == NO_VALUE) {
+            noValueSet.set(col.schemaIndex());
+            builder.appendDefault();
+            return;
+        }
+
+        try {
+            switch (col.type()) {
+                case ClientDataType.INT8:
+                    builder.appendByte((byte) v);
+                    return;
+
+                case ClientDataType.INT16:
+                    builder.appendShort((short) v);
+                    return;
+
+                case ClientDataType.INT32:
+                    builder.appendInt((int) v);
+                    return;
+
+                case ClientDataType.INT64:
+                    builder.appendLong((long) v);
+                    return;
+
+                case ClientDataType.FLOAT:
+                    builder.appendFloat((float) v);
+                    return;
+
+                case ClientDataType.DOUBLE:
+                    builder.appendDouble((double) v);
+                    return;
+
+                case ClientDataType.DECIMAL:
+                    builder.appendDecimalNotNull((BigDecimal) v);
+                    return;
+
+                case ClientDataType.UUID:
+                    builder.appendUuidNotNull((UUID) v);
+                    return;
+
+                case ClientDataType.STRING:
+                    builder.appendStringNotNull((String) v);
+                    return;
+
+                case ClientDataType.BYTES:
+                    builder.appendBytesNotNull((byte[]) v);
+                    return;
+
+                case ClientDataType.BITMASK:
+                    builder.appendBitmaskNotNull((BitSet) v);
+                    return;
+
+                case ClientDataType.DATE:
+                    builder.appendDateNotNull((LocalDate) v);
+                    return;
+
+                case ClientDataType.TIME:
+                    builder.appendTimeNotNull((LocalTime) v);
+                    return;
+
+                case ClientDataType.DATETIME:
+                    builder.appendDateTimeNotNull((LocalDateTime) v);
+                    return;
+
+                case ClientDataType.TIMESTAMP:
+                    builder.appendTimestampNotNull((Instant) v);
+                    return;
+
+                case ClientDataType.NUMBER:
+                    builder.appendNumberNotNull((BigInteger) v);
+                    return;
+
+                default:
+                    throw new IllegalArgumentException("Unsupported type: " + col.type());
+            }
+        } catch (ClassCastException e) {
+            throw new IgniteException(PROTOCOL_ERR, "Incorrect value type for column '" + col.name() + "': " + e.getMessage(), e);
+        }
+    }
 }
diff --git a/modules/client/src/main/java/org/apache/ignite/internal/marshaller/ClientMarshallerReader.java b/modules/client/src/main/java/org/apache/ignite/internal/marshaller/ClientMarshallerReader.java
index 1e0e7569b2..52af968a47 100644
--- a/modules/client/src/main/java/org/apache/ignite/internal/marshaller/ClientMarshallerReader.java
+++ b/modules/client/src/main/java/org/apache/ignite/internal/marshaller/ClientMarshallerReader.java
@@ -25,6 +25,7 @@ import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.util.BitSet;
 import java.util.UUID;
+import org.apache.ignite.internal.binarytuple.BinaryTupleReader;
 import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
 
 /**
@@ -32,152 +33,172 @@ import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
  */
 public class ClientMarshallerReader implements MarshallerReader {
     /** Unpacker. */
-    private final ClientMessageUnpacker unpacker;
+    private final BinaryTupleReader unpacker;
+
+    /** Index. */
+    private int index;
 
     /**
      * Constructor.
      *
      * @param unpacker Unpacker.
      */
-    public ClientMarshallerReader(ClientMessageUnpacker unpacker) {
+    public ClientMarshallerReader(BinaryTupleReader unpacker) {
         this.unpacker = unpacker;
     }
 
     /** {@inheritDoc} */
     @Override
     public void skipValue() {
-        unpacker.skipValues(1);
+        index++;
     }
 
     /** {@inheritDoc} */
     @Override
     public byte readByte() {
-        return unpacker.unpackByte();
+        return unpacker.byteValue(index++);
     }
 
     /** {@inheritDoc} */
     @Override
     public Byte readByteBoxed() {
-        return unpacker.tryUnpackNil() ? null : unpacker.unpackByte();
+        var idx = index++;
+        return unpacker.hasNullValue(idx) ? null : unpacker.byteValue(idx);
     }
 
     /** {@inheritDoc} */
     @Override
     public short readShort() {
-        return unpacker.unpackShort();
+        return unpacker.shortValue(index++);
     }
 
     /** {@inheritDoc} */
     @Override
     public Short readShortBoxed() {
-        return unpacker.tryUnpackNil() ? null : unpacker.unpackShort();
+        var idx = index++;
+        return unpacker.hasNullValue(idx) ? null : unpacker.shortValue(idx);
     }
 
     /** {@inheritDoc} */
     @Override
     public int readInt() {
-        return unpacker.unpackInt();
+        return unpacker.intValue(index++);
     }
 
     /** {@inheritDoc} */
     @Override
     public Integer readIntBoxed() {
-        return unpacker.tryUnpackNil() ? null : unpacker.unpackInt();
+        var idx = index++;
+        return unpacker.hasNullValue(idx) ? null : unpacker.intValue(idx);
     }
 
     /** {@inheritDoc} */
     @Override
     public long readLong() {
-        return unpacker.unpackLong();
+        return unpacker.longValue(index++);
     }
 
     /** {@inheritDoc} */
     @Override
     public Long readLongBoxed() {
-        return unpacker.tryUnpackNil() ? null : unpacker.unpackLong();
+        var idx = index++;
+        return unpacker.hasNullValue(idx) ? null : unpacker.longValue(idx);
     }
 
     /** {@inheritDoc} */
     @Override
     public float readFloat() {
-        return unpacker.unpackFloat();
+        return unpacker.floatValue(index++);
     }
 
     /** {@inheritDoc} */
     @Override
     public Float readFloatBoxed() {
-        return unpacker.tryUnpackNil() ? null : unpacker.unpackFloat();
+        var idx = index++;
+        return unpacker.hasNullValue(idx) ? null : unpacker.floatValue(idx);
     }
 
     /** {@inheritDoc} */
     @Override
     public double readDouble() {
-        return unpacker.unpackDouble();
+        return unpacker.doubleValue(index++);
     }
 
     /** {@inheritDoc} */
     @Override
     public Double readDoubleBoxed() {
-        return unpacker.tryUnpackNil() ? null : unpacker.unpackDouble();
+        var idx = index++;
+        return unpacker.hasNullValue(idx) ? null : unpacker.doubleValue(idx);
     }
 
     /** {@inheritDoc} */
     @Override
     public String readString() {
-        return unpacker.tryUnpackNil() ? null : unpacker.unpackString();
+        var idx = index++;
+        return unpacker.hasNullValue(idx) ? null : unpacker.stringValue(idx);
     }
 
     /** {@inheritDoc} */
     @Override
     public UUID readUuid() {
-        return unpacker.tryUnpackNil() ? null : unpacker.unpackUuid();
+        var idx = index++;
+        return unpacker.hasNullValue(idx) ? null : unpacker.uuidValue(idx);
     }
 
     /** {@inheritDoc} */
     @Override
     public byte[] readBytes() {
-        return unpacker.tryUnpackNil() ? null : unpacker.readPayload(unpacker.unpackBinaryHeader());
+        var idx = index++;
+        return unpacker.hasNullValue(idx) ? null : unpacker.bytesValue(idx);
     }
 
     /** {@inheritDoc} */
     @Override
     public BitSet readBitSet() {
-        return unpacker.tryUnpackNil() ? null : unpacker.unpackBitSet();
+        var idx = index++;
+        return unpacker.hasNullValue(idx) ? null : unpacker.bitmaskValue(idx);
     }
 
     /** {@inheritDoc} */
     @Override
     public BigInteger readBigInt() {
-        return unpacker.tryUnpackNil() ? null : unpacker.unpackNumber();
+        var idx = index++;
+        return unpacker.hasNullValue(idx) ? null : unpacker.numberValue(idx);
     }
 
     /** {@inheritDoc} */
     @Override
     public BigDecimal readBigDecimal() {
-        return unpacker.tryUnpackNil() ? null : unpacker.unpackDecimal();
+        // TODO IGNITE-17632: Get scale from schema.
+        var idx = index++;
+        return unpacker.hasNullValue(idx) ? null : unpacker.decimalValue(idx, 100);
     }
 
     /** {@inheritDoc} */
     @Override
     public LocalDate readDate() {
-        return unpacker.tryUnpackNil() ? null : unpacker.unpackDate();
+        var idx = index++;
+        return unpacker.hasNullValue(idx) ? null : unpacker.dateValue(idx);
     }
 
     /** {@inheritDoc} */
     @Override
     public LocalTime readTime() {
-        return unpacker.tryUnpackNil() ? null : unpacker.unpackTime();
+        var idx = index++;
+        return unpacker.hasNullValue(idx) ? null : unpacker.timeValue(idx);
     }
 
     /** {@inheritDoc} */
     @Override
     public Instant readTimestamp() {
-        return unpacker.tryUnpackNil() ? null : unpacker.unpackTimestamp();
+        var idx = index++;
+        return unpacker.hasNullValue(idx) ? null : unpacker.timestampValue(idx);
     }
 
     /** {@inheritDoc} */
     @Override
     public LocalDateTime readDateTime() {
-        return unpacker.tryUnpackNil() ? null : unpacker.unpackDateTime();
+        var idx = index++;
+        return unpacker.hasNullValue(idx) ? null : unpacker.dateTimeValue(idx);
     }
 }
diff --git a/modules/client/src/main/java/org/apache/ignite/internal/marshaller/ClientMarshallerWriter.java b/modules/client/src/main/java/org/apache/ignite/internal/marshaller/ClientMarshallerWriter.java
index 1bec432172..bcaa375af6 100644
--- a/modules/client/src/main/java/org/apache/ignite/internal/marshaller/ClientMarshallerWriter.java
+++ b/modules/client/src/main/java/org/apache/ignite/internal/marshaller/ClientMarshallerWriter.java
@@ -25,6 +25,7 @@ import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.util.BitSet;
 import java.util.UUID;
+import org.apache.ignite.internal.binarytuple.BinaryTupleBuilder;
 import org.apache.ignite.internal.client.proto.ClientMessagePacker;
 
 /**
@@ -32,123 +33,127 @@ import org.apache.ignite.internal.client.proto.ClientMessagePacker;
  */
 public class ClientMarshallerWriter implements MarshallerWriter {
     /** Packer. */
-    private final ClientMessagePacker packer;
+    private final BinaryTupleBuilder packer;
+
+    /** No-value bit set. */
+    private final BitSet noValueSet;
 
     /**
      * Constructor.
      *
      * @param packer Packer.
      */
-    public ClientMarshallerWriter(ClientMessagePacker packer) {
+    public ClientMarshallerWriter(BinaryTupleBuilder packer, BitSet noValueSet) {
         this.packer = packer;
+        this.noValueSet = noValueSet;
     }
 
     /** {@inheritDoc} */
     @Override
     public void writeNull() {
-        packer.packNil();
+        packer.appendNull();
     }
 
     /** {@inheritDoc} */
     @Override
     public void writeAbsentValue() {
-        packer.packNoValue();
+        noValueSet.set(packer.elementIndex());
+        packer.appendDefault();
     }
 
     /** {@inheritDoc} */
     @Override
     public void writeByte(byte val) {
-        packer.packByte(val);
+        packer.appendByte(val);
     }
 
     /** {@inheritDoc} */
     @Override
     public void writeShort(short val) {
-        packer.packShort(val);
+        packer.appendShort(val);
     }
 
     /** {@inheritDoc} */
     @Override
     public void writeInt(int val) {
-        packer.packInt(val);
+        packer.appendInt(val);
     }
 
     /** {@inheritDoc} */
     @Override
     public void writeLong(long val) {
-        packer.packLong(val);
+        packer.appendLong(val);
     }
 
     /** {@inheritDoc} */
     @Override
     public void writeFloat(float val) {
-        packer.packFloat(val);
+        packer.appendFloat(val);
     }
 
     /** {@inheritDoc} */
     @Override
     public void writeDouble(double val) {
-        packer.packDouble(val);
+        packer.appendDouble(val);
     }
 
     /** {@inheritDoc} */
     @Override
     public void writeString(String val) {
-        packer.packString(val);
+        packer.appendString(val);
     }
 
     /** {@inheritDoc} */
     @Override
     public void writeUuid(UUID val) {
-        packer.packUuid(val);
+        packer.appendUuid(val);
     }
 
     /** {@inheritDoc} */
     @Override
     public void writeBytes(byte[] val) {
-        packer.packBinaryHeader(val.length);
-        packer.writePayload(val);
+        packer.appendBytes(val);
     }
 
     /** {@inheritDoc} */
     @Override
     public void writeBitSet(BitSet val) {
-        packer.packBitSet(val);
+        packer.appendBitmask(val);
     }
 
     /** {@inheritDoc} */
     @Override
     public void writeBigInt(BigInteger val) {
-        packer.packNumber(val);
+        packer.appendNumber(val);
     }
 
     /** {@inheritDoc} */
     @Override
     public void writeBigDecimal(BigDecimal val) {
-        packer.packDecimal(val);
+        packer.appendDecimal(val);
     }
 
     /** {@inheritDoc} */
     @Override
     public void writeDate(LocalDate val) {
-        packer.packDate(val);
+        packer.appendDate(val);
     }
 
     /** {@inheritDoc} */
     @Override
     public void writeTime(LocalTime val) {
-        packer.packTime(val);
+        packer.appendTime(val);
     }
 
     /** {@inheritDoc} */
     @Override
     public void writeTimestamp(Instant val) {
-        packer.packTimestamp(val);
+        packer.appendTimestamp(val);
     }
 
     /** {@inheritDoc} */
     @Override
     public void writeDateTime(LocalDateTime val) {
-        packer.packDateTime(val);
+        packer.appendDateTime(val);
     }
 }
diff --git a/modules/client/src/test/java/org/apache/ignite/client/AbstractClientTableTest.java b/modules/client/src/test/java/org/apache/ignite/client/AbstractClientTableTest.java
index 05308556ae..31c7a07015 100644
--- a/modules/client/src/test/java/org/apache/ignite/client/AbstractClientTableTest.java
+++ b/modules/client/src/test/java/org/apache/ignite/client/AbstractClientTableTest.java
@@ -102,7 +102,7 @@ public class AbstractClientTableTest extends AbstractClientTest {
         return client.tables().table(TABLE_WITH_DEFAULT_VALUES);
     }
 
-    protected static Tuple allClumnsTableKey(long id) {
+    protected static Tuple allColumnsTableKey(long id) {
         return Tuple.create().set("gid", id).set("id", String.valueOf(id));
     }
 
diff --git a/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueViewTest.java b/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueViewTest.java
index 36e3578484..e3aac3ea21 100644
--- a/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueViewTest.java
+++ b/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueViewTest.java
@@ -94,7 +94,7 @@ public class ClientKeyValueViewTest extends AbstractClientTableTest {
         KeyValueView<Tuple, Tuple> kvView = table.keyValueView();
         KeyValueView<IncompletePojo, IncompletePojo> pojoView = table.keyValueView(IncompletePojo.class, IncompletePojo.class);
 
-        kvView.put(null, allClumnsTableKey(1), allColumnsTableVal("x"));
+        kvView.put(null, allColumnsTableKey(1), allColumnsTableVal("x"));
 
         var key = new IncompletePojo();
         key.id = "1";
@@ -136,7 +136,7 @@ public class ClientKeyValueViewTest extends AbstractClientTableTest {
         assertEquals("foo", res.zstring);
         assertArrayEquals(new byte[]{1, 2}, res.zbytes);
         assertEquals(BitSet.valueOf(new byte[]{32}), res.zbitmask);
-        assertEquals(21, res.zdecimal.longValue());
+        assertEquals(21, res.zdecimal.unscaledValue().longValue()); // TODO: IGNITE-17632 check correct round-trip
         assertEquals(22, res.znumber.longValue());
         assertEquals(uuid, res.zuuid);
     }
@@ -174,10 +174,10 @@ public class ClientKeyValueViewTest extends AbstractClientTableTest {
 
         pojoView.put(null, val, val);
 
-        Tuple res = table.recordView().get(null, Tuple.create().set("id", "112").set("gid", 111));
+        Tuple res = table.recordView().get(null, Tuple.create().set("id", "112").set("gid", 111L));
 
         assertNotNull(res);
-        assertEquals(111, res.intValue("gid"));
+        assertEquals(111, res.longValue("gid"));
         assertEquals("112", res.stringValue("id"));
         assertEquals(113, res.byteValue("zbyte"));
         assertEquals(114, res.shortValue("zshort"));
@@ -191,7 +191,7 @@ public class ClientKeyValueViewTest extends AbstractClientTableTest {
         assertEquals("119", res.stringValue("zstring"));
         assertEquals(120, ((byte[]) res.value("zbytes"))[0]);
         assertEquals(BitSet.valueOf(new byte[]{121}), res.bitmaskValue("zbitmask"));
-        assertEquals(122, ((Number) res.value("zdecimal")).longValue());
+        assertEquals(122, ((BigDecimal) res.value("zdecimal")).unscaledValue().longValue()); // TODO: IGNITE-17632 check correct round-trip
         assertEquals(BigInteger.valueOf(123), res.value("znumber"));
         assertEquals(uuid, res.uuidValue("zuuid"));
     }
@@ -467,7 +467,7 @@ public class ClientKeyValueViewTest extends AbstractClientTableTest {
 
         pojoView.put(null, 1, new NamePojo());
 
-        var res = recordView.get(null, tupleKey(1));
+        var res = recordView.get(null, Tuple.create().set("id", 1));
 
         assertEquals("def_str", res.stringValue("str"));
         assertEquals("def_str2", res.stringValue("strNonNull"));
@@ -485,7 +485,7 @@ public class ClientKeyValueViewTest extends AbstractClientTableTest {
 
         pojoView.put(null, 1, pojo);
 
-        var res = recordView.get(null, tupleKey(1));
+        var res = recordView.get(null, Tuple.create().set("id", 1));
 
         assertNull(res.stringValue("str"));
     }
diff --git a/modules/client/src/test/java/org/apache/ignite/client/ClientRecordViewTest.java b/modules/client/src/test/java/org/apache/ignite/client/ClientRecordViewTest.java
index 8db9e210da..ff2fffa711 100644
--- a/modules/client/src/test/java/org/apache/ignite/client/ClientRecordViewTest.java
+++ b/modules/client/src/test/java/org/apache/ignite/client/ClientRecordViewTest.java
@@ -96,7 +96,7 @@ public class ClientRecordViewTest extends AbstractClientTableTest {
         KeyValueView<Tuple, Tuple> kvView = table.keyValueView();
         RecordView<IncompletePojo> pojoView = table.recordView(IncompletePojo.class);
 
-        kvView.put(null, allClumnsTableKey(1), allColumnsTableVal("x"));
+        kvView.put(null, allColumnsTableKey(1), allColumnsTableVal("x"));
 
         var key = new IncompletePojo();
         key.id = "1";
@@ -136,7 +136,7 @@ public class ClientRecordViewTest extends AbstractClientTableTest {
         assertEquals("foo", res.zstring);
         assertArrayEquals(new byte[]{1, 2}, res.zbytes);
         assertEquals(BitSet.valueOf(new byte[]{32}), res.zbitmask);
-        assertEquals(21, res.zdecimal.longValue());
+        assertEquals(21, res.zdecimal.unscaledValue().longValue()); // TODO: IGNITE-17632 check correct round-trip
         assertEquals(22, res.znumber.longValue());
         assertEquals(uuid, res.zuuid);
     }
@@ -172,10 +172,10 @@ public class ClientRecordViewTest extends AbstractClientTableTest {
 
         pojoView.upsert(null, val);
 
-        Tuple res = table.recordView().get(null, Tuple.create().set("id", "112").set("gid", 111));
+        Tuple res = table.recordView().get(null, Tuple.create().set("id", "112").set("gid", 111L));
 
         assertNotNull(res);
-        assertEquals(111, res.intValue("gid"));
+        assertEquals(111, res.longValue("gid"));
         assertEquals("112", res.stringValue("id"));
         assertEquals(113, res.byteValue("zbyte"));
         assertEquals(114, res.shortValue("zshort"));
@@ -189,7 +189,7 @@ public class ClientRecordViewTest extends AbstractClientTableTest {
         assertEquals("119", res.stringValue("zstring"));
         assertEquals(120, ((byte[]) res.value("zbytes"))[0]);
         assertEquals(BitSet.valueOf(new byte[]{121}), res.bitmaskValue("zbitmask"));
-        assertEquals(122, ((Number) res.value("zdecimal")).longValue());
+        assertEquals(122, ((BigDecimal) res.value("zdecimal")).unscaledValue().longValue());
         assertEquals(BigInteger.valueOf(123), res.value("znumber"));
         assertEquals(uuid, res.uuidValue("zuuid"));
     }
@@ -492,7 +492,7 @@ public class ClientRecordViewTest extends AbstractClientTableTest {
 
         pojoView.upsert(null, new PersonPojo());
 
-        var res = recordView.get(null, tupleKey(0));
+        var res = recordView.get(null, Tuple.create().set("id", 0));
 
         assertEquals("def_str", res.stringValue("str"));
         assertEquals("def_str2", res.stringValue("strNonNull"));
@@ -511,7 +511,7 @@ public class ClientRecordViewTest extends AbstractClientTableTest {
 
         pojoView.upsert(null, pojo);
 
-        var res = recordView.get(null, tupleKey(1));
+        var res = recordView.get(null, Tuple.create().set("id", 1));
 
         assertNull(res.stringValue("str"));
     }
diff --git a/modules/client/src/test/java/org/apache/ignite/client/ClientTableTest.java b/modules/client/src/test/java/org/apache/ignite/client/ClientTableTest.java
index 2b2904e4ba..e79a264157 100644
--- a/modules/client/src/test/java/org/apache/ignite/client/ClientTableTest.java
+++ b/modules/client/src/test/java/org/apache/ignite/client/ClientTableTest.java
@@ -383,7 +383,8 @@ public class ClientTableTest extends AbstractClientTableTest {
 
         var ex = assertThrows(IgniteException.class, () -> defaultTable().recordView().upsert(null, tuple));
 
-        assertTrue(ex.getMessage().contains("Incorrect value type for column 'ID': Expected Integer, but got String"), ex.getMessage());
+        String expectedErr = "Incorrect value type for column 'ID': class java.lang.String cannot be cast to class java.lang.Long";
+        assertThat(ex.getMessage(), containsString(expectedErr));
     }
 
     @Test
diff --git a/modules/client/src/test/java/org/apache/ignite/client/RetryPolicyTest.java b/modules/client/src/test/java/org/apache/ignite/client/RetryPolicyTest.java
index a37c1bb7fc..9776aa26d5 100644
--- a/modules/client/src/test/java/org/apache/ignite/client/RetryPolicyTest.java
+++ b/modules/client/src/test/java/org/apache/ignite/client/RetryPolicyTest.java
@@ -143,8 +143,8 @@ public class RetryPolicyTest {
 
         try (var client = getClient(plc)) {
             RecordView<Tuple> recView = client.tables().table("t").recordView();
-            recView.get(null, Tuple.create().set("id", 1));
-            recView.get(null, Tuple.create().set("id", 1));
+            recView.get(null, Tuple.create().set("id", 1L));
+            recView.get(null, Tuple.create().set("id", 1L));
 
             assertEquals(1, plc.invocations.size());
         }
@@ -172,8 +172,8 @@ public class RetryPolicyTest {
 
         try (var client = getClient(new RetryReadPolicy())) {
             RecordView<Tuple> recView = client.tables().table("t").recordView();
-            recView.get(null, Tuple.create().set("id", 1));
-            recView.get(null, Tuple.create().set("id", 1));
+            recView.get(null, Tuple.create().set("id", 1L));
+            recView.get(null, Tuple.create().set("id", 1L));
         }
     }
 
@@ -183,8 +183,8 @@ public class RetryPolicyTest {
 
         try (var client = getClient(new RetryReadPolicy())) {
             RecordView<Tuple> recView = client.tables().table("t").recordView();
-            recView.upsert(null, Tuple.create().set("id", 1));
-            assertThrows(IgniteClientConnectionException.class, () -> recView.upsert(null, Tuple.create().set("id", 1)));
+            recView.upsert(null, Tuple.create().set("id", 1L));
+            assertThrows(IgniteClientConnectionException.class, () -> recView.upsert(null, Tuple.create().set("id", 1L)));
         }
     }
 
diff --git a/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeIgniteTables.java b/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeIgniteTables.java
index eb8532b45a..82787d6dc0 100644
--- a/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeIgniteTables.java
+++ b/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeIgniteTables.java
@@ -224,7 +224,7 @@ public class FakeIgniteTables implements IgniteTables, IgniteTablesInternal {
         return new SchemaDescriptor(
                 v,
                 new Column[]{
-                        new Column("gid".toUpperCase(), NativeTypes.INT32, false),
+                        new Column("gid".toUpperCase(), NativeTypes.INT64, false),
                         new Column("id".toUpperCase(), NativeTypes.STRING, false)
                 },
                 new Column[]{
@@ -241,7 +241,7 @@ public class FakeIgniteTables implements IgniteTables, IgniteTablesInternal {
                         new Column("zbytes".toUpperCase(), NativeTypes.BYTES, true),
                         new Column("zuuid".toUpperCase(), NativeTypes.UUID, true),
                         new Column("zbitmask".toUpperCase(), NativeTypes.bitmaskOf(16), true),
-                        new Column("zdecimal".toUpperCase(), NativeTypes.decimalOf(20, 3), true),
+                        new Column("zdecimal".toUpperCase(), NativeTypes.decimalOf(20, 100), true),
                         new Column("znumber".toUpperCase(), NativeTypes.numberOf(24), true),
                 });
     }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerBenchmarksBase.cs b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerBenchmarksBase.cs
index 13c3a04abe..a675657f61 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerBenchmarksBase.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerBenchmarksBase.cs
@@ -63,7 +63,7 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
         {
             var bytesWritten = pooledWriter.GetWrittenMemory().Length;
 
-            if (bytesWritten != 29)
+            if (bytesWritten != 31)
             {
                 throw new Exception("Unexpected number of bytes written: " + bytesWritten);
             }
@@ -77,7 +77,7 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
             TupleSerializerHandler.Instance.Write(ref writer, Schema, Tuple);
 
             writer.Flush();
-            return pooledWriter.GetWrittenMemory().ToArray();
+            return pooledWriter.GetWrittenMemory().Slice(3).ToArray();
         }
 
         protected internal class Car
diff --git a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerReadBenchmarks.cs b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerReadBenchmarks.cs
index 9a14a671d3..1e3dddd427 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerReadBenchmarks.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerReadBenchmarks.cs
@@ -20,11 +20,13 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
     using System.Diagnostics.CodeAnalysis;
     using BenchmarkDotNet.Attributes;
     using Internal.Proto;
+    using Internal.Proto.BinaryTuple;
     using Internal.Table.Serialization;
     using MessagePack;
 
     /// <summary>
     /// Benchmarks for <see cref="IRecordSerializerHandler{T}.Read"/> implementations.
+    ///
     /// Results on Intel Core i7-9700K, .NET SDK 3.1.416, Ubuntu 20.04:
     /// |           Method |       Mean |   Error |  StdDev | Ratio | RatioSD |  Gen 0 | Allocated |
     /// |----------------- |-----------:|--------:|--------:|------:|--------:|-------:|----------:|
@@ -32,6 +34,21 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
     /// |       ReadObject |   257.5 ns | 1.41 ns | 1.25 ns |  1.22 |    0.01 | 0.0124 |      80 B |
     /// |        ReadTuple |   561.0 ns | 3.09 ns | 2.89 ns |  2.66 |    0.01 | 0.0849 |     536 B |
     /// |    ReadObjectOld | 1,020.9 ns | 9.05 ns | 8.47 ns |  4.84 |    0.05 | 0.0744 |     472 B |.
+    ///
+    /// Results on i7-7700HQ, .NET SDK 6.0.400, Ubuntu 20.04:
+    /// MsgPack (old)
+    /// |           Method |     Mean |   Error |  StdDev | Ratio | RatioSD |  Gen 0 | Allocated |
+    /// |----------------- |---------:|--------:|--------:|------:|--------:|-------:|----------:|
+    /// | ReadObjectManual | 289.4 ns | 2.92 ns | 2.59 ns |  1.00 |    0.00 | 0.0024 |      80 B |
+    /// |       ReadObject | 364.3 ns | 3.28 ns | 3.07 ns |  1.26 |    0.02 | 0.0024 |      80 B |
+    /// |        ReadTuple | 755.4 ns | 2.82 ns | 2.35 ns |  2.61 |    0.03 | 0.0181 |     536 B |
+    ///
+    /// BinaryTuple (new)
+    /// |           Method |     Mean |   Error |  StdDev | Ratio | RatioSD |  Gen 0 | Allocated |
+    /// |----------------- |---------:|--------:|--------:|------:|--------:|-------:|----------:|
+    /// | ReadObjectManual | 299.3 ns | 3.42 ns | 3.20 ns |  1.00 |    0.00 | 0.0024 |      80 B |
+    /// |       ReadObject | 382.9 ns | 2.49 ns | 2.21 ns |  1.28 |    0.02 | 0.0024 |      80 B |
+    /// |        ReadTuple | 769.0 ns | 6.06 ns | 5.37 ns |  2.57 |    0.04 | 0.0181 |     536 B |.
     /// </summary>
     [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Benchmarks.")]
     [MemoryDiagnoser]
@@ -41,12 +58,13 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
         public void ReadObjectManual()
         {
             var reader = new MessagePackReader(SerializedData);
+            var tupleReader = new BinaryTupleReader(reader.ReadBytesAsMemory(), 3);
 
             var res = new Car
             {
-                Id = reader.TryReadNoValue() ? default : reader.ReadGuid(),
-                BodyType = reader.TryReadNoValue() ? default! : reader.ReadString(),
-                Seats = reader.TryReadNoValue() ? default : reader.ReadInt32()
+                Id = tupleReader.GetGuid(0),
+                BodyType = tupleReader.GetString(1),
+                Seats = tupleReader.GetInt(2)
             };
 
             Consumer.Consume(res);
@@ -70,13 +88,13 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
             Consumer.Consume(res);
         }
 
-        [Benchmark]
-        public void ReadObjectOld()
-        {
-            var reader = new MessagePackReader(SerializedData);
-            var res = ObjectSerializerHandlerOld.Read(ref reader, Schema);
-
-            Consumer.Consume(res);
-        }
+        // [Benchmark]
+        // public void ReadObjectOld()
+        // {
+        //     var reader = new MessagePackReader(SerializedData);
+        //     var res = ObjectSerializerHandlerOld.Read(ref reader, Schema);
+        //
+        //     Consumer.Consume(res);
+        // }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerWriteBenchmarks.cs b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerWriteBenchmarks.cs
index 3eb504d0ac..57f0698ec8 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerWriteBenchmarks.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerWriteBenchmarks.cs
@@ -21,6 +21,7 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
     using BenchmarkDotNet.Attributes;
     using Internal.Buffers;
     using Internal.Proto;
+    using Internal.Proto.BinaryTuple;
     using Internal.Table.Serialization;
 
     /// <summary>
@@ -43,9 +44,13 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
             using var pooledWriter = new PooledArrayBufferWriter();
             var writer = pooledWriter.GetMessageWriter();
 
-            writer.Write(Object.Id);
-            writer.Write(Object.BodyType);
-            writer.Write(Object.Seats);
+            using var tupleBuilder = new BinaryTupleBuilder(3);
+            tupleBuilder.AppendGuid(Object.Id);
+            tupleBuilder.AppendString(Object.BodyType);
+            tupleBuilder.AppendInt(Object.Seats);
+
+            writer.WriteBitSet(3);
+            writer.Write(tupleBuilder.Build().Span);
 
             writer.Flush();
             VerifyWritten(pooledWriter);
@@ -75,7 +80,7 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
             VerifyWritten(pooledWriter);
         }
 
-        [Benchmark]
+        // [Benchmark]
         public void WriteObjectOld()
         {
             using var pooledWriter = new PooledArrayBufferWriter();
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeTests.cs
index 94dd005217..795cbccde1 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/ComputeTests.cs
@@ -190,7 +190,7 @@ namespace Apache.Ignite.Tests.Compute
         [TestCase(2, "_2")]
         [TestCase(3, "")]
         [TestCase(5, "_2")]
-        public async Task TestExecuteColocated(int key, string nodeName)
+        public async Task TestExecuteColocated(long key, string nodeName)
         {
             var keyTuple = new IgniteTuple { [KeyCol] = key };
             var resNodeName = await Client.Compute.ExecuteColocatedAsync<string>(TableName, keyTuple, NodeNameJob);
@@ -230,7 +230,7 @@ namespace Apache.Ignite.Tests.Compute
 
             try
             {
-                var keyTuple = new IgniteTuple { [KeyCol] = 1 };
+                var keyTuple = new IgniteTuple { [KeyCol] = 1L };
                 var resNodeName = await Client.Compute.ExecuteColocatedAsync<string>(tableName, keyTuple, NodeNameJob);
 
                 // Drop table and create a new one with a different ID, then execute a computation again.
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/BinaryTuple/BinaryTupleTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/BinaryTuple/BinaryTupleTests.cs
index 4e60f61b27..ba96516c73 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/BinaryTuple/BinaryTupleTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Proto/BinaryTuple/BinaryTupleTests.cs
@@ -18,7 +18,6 @@
 namespace Apache.Ignite.Tests.Proto.BinaryTuple
 {
     using System;
-    using System.Linq;
     using Internal.Proto.BinaryTuple;
     using NUnit.Framework;
 
@@ -27,6 +26,8 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
     /// </summary>
     public class BinaryTupleTests
     {
+        private delegate void BinaryTupleBuilderAction(ref BinaryTupleBuilder builder);
+
         [Test]
         public void TestNullValue()
         {
@@ -43,23 +44,21 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
         [Test]
         public void TestGetValueThrowsOnNull()
         {
-            var reader = BuildAndRead(b => b.AppendNull());
-
-            var getters = new Action<BinaryTupleReader>[]
+            var getters = new Action[]
             {
-                x => x.GetString(0),
-                x => x.GetByte(0),
-                x => x.GetShort(0),
-                x => x.GetInt(0),
-                x => x.GetLong(0),
-                x => x.GetFloat(0),
-                x => x.GetDouble(0),
-                x => x.GetGuid(0)
+                () => BuildAndRead((ref BinaryTupleBuilder b) => b.AppendNull()).GetString(0),
+                () => BuildAndRead((ref BinaryTupleBuilder b) => b.AppendNull()).GetByte(0),
+                () => BuildAndRead((ref BinaryTupleBuilder b) => b.AppendNull()).GetShort(0),
+                () => BuildAndRead((ref BinaryTupleBuilder b) => b.AppendNull()).GetInt(0),
+                () => BuildAndRead((ref BinaryTupleBuilder b) => b.AppendNull()).GetLong(0),
+                () => BuildAndRead((ref BinaryTupleBuilder b) => b.AppendNull()).GetFloat(0),
+                () => BuildAndRead((ref BinaryTupleBuilder b) => b.AppendNull()).GetDouble(0),
+                () => BuildAndRead((ref BinaryTupleBuilder b) => b.AppendNull()).GetGuid(0)
             };
 
             foreach (var getter in getters)
             {
-                var ex = Assert.Throws<InvalidOperationException>(() => getter(reader));
+                var ex = Assert.Throws<InvalidOperationException>(() => getter());
                 Assert.AreEqual("Binary tuple element with index 0 is null.", ex!.Message);
             }
         }
@@ -67,7 +66,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
         [Test]
         public void TestAppendNull()
         {
-            var reader = BuildAndRead(b => b.AppendNull());
+            var reader = BuildAndRead((ref BinaryTupleBuilder b) => b.AppendNull());
 
             Assert.IsTrue(reader.HasNullMap);
             Assert.IsTrue(reader.IsNull(0));
@@ -113,7 +112,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
         [Test]
         public void TestByte([Values(0, 1, sbyte.MaxValue, sbyte.MinValue)] sbyte value)
         {
-            var res = Build(b => b.AppendByte(value));
+            var res = Build((ref BinaryTupleBuilder b) => b.AppendByte(value));
 
             Assert.AreEqual(value != 0 ? 1 : 0, res[1]);
             Assert.AreEqual(value != 0 ? 3 : 2, res.Length);
@@ -129,7 +128,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
 
             foreach (var value in values)
             {
-                var bytes = Build(b => b.AppendShort(value));
+                var bytes = Build((ref BinaryTupleBuilder b) => b.AppendShort(value));
 
                 Assert.AreEqual(value != 0 ? 1 : 0, bytes[1]);
                 Assert.AreEqual(value != 0 ? 3 : 2, bytes.Length);
@@ -142,7 +141,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
 
             foreach (var value in values)
             {
-                var bytes = Build(b => b.AppendShort(value));
+                var bytes = Build((ref BinaryTupleBuilder b) => b.AppendShort(value));
 
                 Assert.AreEqual(2, bytes[1]);
                 Assert.AreEqual(4, bytes.Length);
@@ -158,7 +157,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
             int[] values = { sbyte.MinValue, -1, 0, 1, sbyte.MaxValue };
             foreach (var value in values)
             {
-                var bytes = Build(b => b.AppendInt(value));
+                var bytes = Build((ref BinaryTupleBuilder b) => b.AppendInt(value));
 
                 Assert.AreEqual(value != 0 ? 1 : 0, bytes[1]);
                 Assert.AreEqual(value != 0 ? 3 : 2, bytes.Length);
@@ -170,7 +169,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
             values = new[] { short.MinValue, sbyte.MinValue - 1, sbyte.MaxValue + 1, short.MaxValue };
             foreach (var value in values)
             {
-                var bytes = Build(b => b.AppendInt(value));
+                var bytes = Build((ref BinaryTupleBuilder b) => b.AppendInt(value));
 
                 Assert.AreEqual(2, bytes[1]);
                 Assert.AreEqual(4, bytes.Length);
@@ -182,7 +181,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
             values = new[] { int.MinValue, short.MinValue - 1, short.MaxValue + 1, int.MaxValue };
             foreach (var value in values)
             {
-                var bytes = Build(b => b.AppendInt(value));
+                var bytes = Build((ref BinaryTupleBuilder b) => b.AppendInt(value));
 
                 Assert.AreEqual(4, bytes[1]);
                 Assert.AreEqual(6, bytes.Length);
@@ -198,7 +197,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
             long[] values = { sbyte.MinValue, -1, 0, 1, sbyte.MaxValue };
             foreach (var value in values)
             {
-                var bytes = Build(b => b.AppendLong(value));
+                var bytes = Build((ref BinaryTupleBuilder b) => b.AppendLong(value));
 
                 Assert.AreEqual(value != 0 ? 1 : 0, bytes[1]);
                 Assert.AreEqual(value != 0 ? 3 : 2, bytes.Length);
@@ -210,7 +209,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
             values = new long[] { short.MinValue, sbyte.MinValue - 1, sbyte.MaxValue + 1, short.MaxValue };
             foreach (var value in values)
             {
-                var bytes = Build(b => b.AppendLong(value));
+                var bytes = Build((ref BinaryTupleBuilder b) => b.AppendLong(value));
 
                 Assert.AreEqual(2, bytes[1]);
                 Assert.AreEqual(4, bytes.Length);
@@ -222,7 +221,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
             values = new long[] { int.MinValue, short.MinValue - 1, short.MaxValue + 1, int.MaxValue };
             foreach (var value in values)
             {
-                var bytes = Build(b => b.AppendLong(value));
+                var bytes = Build((ref BinaryTupleBuilder b) => b.AppendLong(value));
 
                 Assert.AreEqual(4, bytes[1]);
                 Assert.AreEqual(6, bytes.Length);
@@ -234,7 +233,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
             values = new[] { long.MinValue, int.MinValue - 1L, int.MaxValue + 1L, long.MaxValue };
             foreach (var value in values)
             {
-                var bytes = Build(b => b.AppendLong(value));
+                var bytes = Build((ref BinaryTupleBuilder b) => b.AppendLong(value));
 
                 Assert.AreEqual(8, bytes[1]);
                 Assert.AreEqual(10, bytes.Length);
@@ -249,7 +248,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
         {
             {
                 float value = 0.0F;
-                var bytes = Build(b => b.AppendFloat(value));
+                var bytes = Build((ref BinaryTupleBuilder b) => b.AppendFloat(value));
 
                 Assert.AreEqual(0, bytes[1]);
                 Assert.AreEqual(2, bytes.Length);
@@ -260,7 +259,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
 
             {
                 float value = 0.5F;
-                var bytes = Build(b => b.AppendFloat(value));
+                var bytes = Build((ref BinaryTupleBuilder b) => b.AppendFloat(value));
 
                 Assert.AreEqual(4, bytes[1]);
                 Assert.AreEqual(6, bytes.Length);
@@ -275,7 +274,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
         {
             {
                 double value = 0.0;
-                var bytes = Build(b => b.AppendDouble(value));
+                var bytes = Build((ref BinaryTupleBuilder b) => b.AppendDouble(value));
 
                 Assert.AreEqual(0, bytes[1]);
                 Assert.AreEqual(2, bytes.Length);
@@ -286,7 +285,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
 
             {
                 double value = 0.5;
-                var bytes = Build(b => b.AppendDouble(value));
+                var bytes = Build((ref BinaryTupleBuilder b) => b.AppendDouble(value));
 
                 Assert.AreEqual(4, bytes[1]);
                 Assert.AreEqual(6, bytes.Length);
@@ -297,7 +296,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
 
             {
                 double value = 0.1;
-                var bytes = Build(b => b.AppendDouble(value));
+                var bytes = Build((ref BinaryTupleBuilder b) => b.AppendDouble(value));
 
                 Assert.AreEqual(8, bytes[1]);
                 Assert.AreEqual(10, bytes.Length);
@@ -312,7 +311,15 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
         {
             var values = new[] {"ascii", "我愛Java", string.Empty, "a string with a bit more characters"};
 
-            var reader = BuildAndRead(b => values.ToList().ForEach(b.AppendString), numElements: values.Length);
+            var reader = BuildAndRead(
+                (ref BinaryTupleBuilder b) =>
+                {
+                    foreach (var value in values)
+                    {
+                        b.AppendString(value);
+                    }
+                },
+                numElements: values.Length);
 
             for (var i = 0; i < values.Length; i++)
             {
@@ -326,7 +333,7 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
             var guid = Guid.NewGuid();
 
             var reader = BuildAndRead(
-                b =>
+                (ref BinaryTupleBuilder b) =>
                 {
                     b.AppendGuid(Guid.Empty);
                     b.AppendGuid(guid);
@@ -337,20 +344,27 @@ namespace Apache.Ignite.Tests.Proto.BinaryTuple
             Assert.AreEqual(guid, reader.GetGuid(1));
         }
 
-        private static BinaryTupleReader BuildAndRead(Action<BinaryTupleBuilder> build, int numElements = 1)
+        private static BinaryTupleReader BuildAndRead(BinaryTupleBuilderAction build, int numElements = 1)
         {
             var bytes = Build(build, numElements);
 
             return new BinaryTupleReader(bytes, numElements);
         }
 
-        private static byte[] Build(Action<BinaryTupleBuilder> build, int numElements = 1)
+        private static byte[] Build(BinaryTupleBuilderAction build, int numElements = 1)
         {
-            using var builder = new BinaryTupleBuilder(numElements);
+            var builder = new BinaryTupleBuilder(numElements);
 
-            build(builder);
+            try
+            {
+                build.Invoke(ref builder);
 
-            return builder.Build().ToArray();
+                return builder.Build().ToArray();
+            }
+            finally
+            {
+                builder.Dispose();
+            }
         }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/CustomTestIgniteTuple.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/CustomTestIgniteTuple.cs
index 42ee0bfcd1..a46302d65b 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/CustomTestIgniteTuple.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/CustomTestIgniteTuple.cs
@@ -24,7 +24,7 @@ namespace Apache.Ignite.Tests.Table
     /// </summary>
     public class CustomTestIgniteTuple : IIgniteTuple
     {
-        public const int Key = 42;
+        public const long Key = 42;
 
         public const string Value = "Val1";
 
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/IgniteTupleTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/IgniteTupleTests.cs
index b49a5250a0..50756b94c1 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/IgniteTupleTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/IgniteTupleTests.cs
@@ -147,7 +147,7 @@ namespace Apache.Ignite.Tests.Table
         [Test]
         public void TestCustomTupleEquality()
         {
-            var tuple = new IgniteTuple { ["key"] = 42, ["val"] = "Val1" };
+            var tuple = new IgniteTuple { ["key"] = 42L, ["val"] = "Val1" };
             var customTuple = new CustomTestIgniteTuple();
 
             Assert.IsTrue(IIgniteTuple.Equals(tuple, customTuple));
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/CustomTestIgniteTuple.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Poco2.cs
similarity index 51%
copy from modules/platforms/dotnet/Apache.Ignite.Tests/Table/CustomTestIgniteTuple.cs
copy to modules/platforms/dotnet/Apache.Ignite.Tests/Table/Poco2.cs
index 42ee0bfcd1..716859fbaa 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/CustomTestIgniteTuple.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Poco2.cs
@@ -17,33 +17,28 @@
 
 namespace Apache.Ignite.Tests.Table
 {
-    using Ignite.Table;
-
-    /// <summary>
-    /// Custom tuple implementation for tests.
-    /// </summary>
-    public class CustomTestIgniteTuple : IIgniteTuple
+    public class Poco2
     {
-        public const int Key = 42;
+        public int Id { get; set; }
+
+        public sbyte Prop1 { get; set; }
+
+        public short Prop2 { get; set; }
+
+        public int Prop3 { get; set; }
+
+        public long Prop4 { get; set; }
 
-        public const string Value = "Val1";
+        public float Prop5 { get; set; }
 
-        public int FieldCount => 2;
+        public double Prop6 { get; set; }
 
-        public object? this[int ordinal]
-        {
-            get => ordinal switch { 0 => Key, _ => Value };
-            set => throw new System.NotImplementedException();
-        }
+        public long Prop7 { get; set; }
 
-        public object? this[string name]
-        {
-            get => name switch { "KEY" => Key, _ => Value };
-            set => throw new System.NotImplementedException();
-        }
+        public string? Prop8 { get; set; }
 
-        public string GetName(int ordinal) => ordinal switch { 0 => "KEY", _ => "VAL" };
+        public int Prop9 { get; set; }
 
-        public int GetOrdinal(string name) => name switch { "KEY" => 0, _ => 1 };
+        public int Prop10 { get; set; }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPocoTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPocoTests.cs
index e9fbea0db8..a11b046f4e 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPocoTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPocoTests.cs
@@ -524,5 +524,50 @@ namespace Apache.Ignite.Tests.Table
             Assert.AreEqual(key, resTuple.Key);
             Assert.AreEqual(val, resTuple.Val);
         }
+
+        [Test]
+        public async Task TestBigPoco()
+        {
+            var sql = "CREATE TABLE IF NOT EXISTS TestBigPoco(ID INT PRIMARY KEY, PROP1 TINYINT, PROP2 SMALLINT, PROP3 INT, " +
+                      "PROP4 BIGINT, PROP5 FLOAT, PROP6 DOUBLE, PROP7 BIGINT, PROP8 VARCHAR, PROP9 INT, PROP10 INT)";
+
+            await Client.Sql.ExecuteAsync(null, sql);
+
+            using var deferDropTable = new DisposeAction(
+                () => Client.Sql.ExecuteAsync(null, "DROP TABLE TestBigPoco").GetAwaiter().GetResult());
+
+            var table = await Client.Tables.GetTableAsync("PUBLIC.TestBigPoco");
+            var pocoView = table!.GetRecordView<Poco2>();
+
+            var poco = new Poco2
+            {
+                Id = -1,
+                Prop1 = 1,
+                Prop2 = 2,
+                Prop3 = 3,
+                Prop4 = 4,
+                Prop5 = 5,
+                Prop6 = 6,
+                Prop7 = 7,
+                Prop8 = "8",
+                Prop9 = 9,
+                Prop10 = 10
+            };
+
+            await pocoView.UpsertAsync(null, poco);
+
+            var res = await pocoView.GetAsync(null, new Poco2 { Id = -1 });
+
+            Assert.AreEqual(poco.Prop1, res!.Prop1);
+            Assert.AreEqual(poco.Prop2, res.Prop2);
+            Assert.AreEqual(poco.Prop3, res.Prop3);
+            Assert.AreEqual(poco.Prop4, res.Prop4);
+            Assert.AreEqual(poco.Prop5, res.Prop5);
+            Assert.AreEqual(poco.Prop6, res.Prop6);
+            Assert.AreEqual(poco.Prop7, res.Prop7);
+            Assert.AreEqual(poco.Prop8, res.Prop8);
+            Assert.AreEqual(poco.Prop9, res.Prop9);
+            Assert.AreEqual(poco.Prop10, res.Prop10);
+        }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/ObjectSerializerHandlerTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/ObjectSerializerHandlerTests.cs
index 705f277d3e..638d192be4 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/ObjectSerializerHandlerTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/ObjectSerializerHandlerTests.cs
@@ -20,6 +20,7 @@ namespace Apache.Ignite.Tests.Table.Serialization
     using System;
     using Internal.Buffers;
     using Internal.Proto;
+    using Internal.Proto.BinaryTuple;
     using Internal.Table;
     using Internal.Table.Serialization;
     using MessagePack;
@@ -40,31 +41,18 @@ namespace Apache.Ignite.Tests.Table.Serialization
         [Test]
         public void TestWrite()
         {
-            var reader = WriteAndGetReader();
-
-            Assert.AreEqual(1234, reader.ReadInt32());
-            Assert.AreEqual("foo", reader.ReadString());
-            Assert.IsTrue(reader.End);
-        }
-
-        [Test]
-        public void TestWriteUnsigned()
-        {
-            var bytes = Write(new UnsignedPoco(ulong.MaxValue, "foo"));
-            var reader = new MessagePackReader(bytes);
+            var reader = WriteAndGetTupleReader();
 
-            Assert.AreEqual(ulong.MaxValue, reader.ReadUInt64());
-            Assert.AreEqual("foo", reader.ReadString());
-            Assert.IsTrue(reader.End);
+            Assert.AreEqual(1234, reader.GetInt(0));
+            Assert.AreEqual("foo", reader.GetString(1));
         }
 
         [Test]
         public void TestWriteKeyOnly()
         {
-            var reader = WriteAndGetReader(keyOnly: true);
+            var reader = WriteAndGetTupleReader(keyOnly: true);
 
-            Assert.AreEqual(1234, reader.ReadInt32());
-            Assert.IsTrue(reader.End);
+            Assert.AreEqual(1234, reader.GetInt(0));
         }
 
         [Test]
@@ -80,24 +68,13 @@ namespace Apache.Ignite.Tests.Table.Serialization
         [Test]
         public void TestReadKeyOnly()
         {
-            var reader = WriteAndGetReader();
+            var reader = WriteAndGetReader(true);
             var resPoco = new ObjectSerializerHandler<Poco>().Read(ref reader, Schema, keyOnly: true);
 
             Assert.AreEqual(1234, resPoco.Key);
             Assert.IsNull(resPoco.Val);
         }
 
-        [Test]
-        public void TestReadValuePart()
-        {
-            var reader = WriteAndGetReader();
-            reader.Skip(); // Skip key.
-            var resPoco = new ObjectSerializerHandler<Poco>().ReadValuePart(ref reader, Schema, new Poco{Key = 4321});
-
-            Assert.AreEqual(4321, resPoco.Key);
-            Assert.AreEqual("foo", resPoco.Val);
-        }
-
         [Test]
         public void TestReadUnsupportedFieldTypeThrowsException()
         {
@@ -131,6 +108,14 @@ namespace Apache.Ignite.Tests.Table.Serialization
             return new MessagePackReader(bytes);
         }
 
+        private static BinaryTupleReader WriteAndGetTupleReader(bool keyOnly = false)
+        {
+            var msgPackReader = WriteAndGetReader(keyOnly);
+            var bytes = msgPackReader.ReadBytesAsMemory();
+
+            return new BinaryTupleReader(bytes, keyOnly ? 1 : 2);
+        }
+
         private static byte[] Write<T>(T obj, bool keyOnly = false)
             where T : class
         {
@@ -141,11 +126,11 @@ namespace Apache.Ignite.Tests.Table.Serialization
 
             handler.Write(ref writer, Schema, obj, keyOnly);
             writer.Flush();
-            return pooledWriter.GetWrittenMemory().ToArray();
+
+            // Slice NoValueSet.
+            return pooledWriter.GetWrittenMemory().Slice(3).ToArray();
         }
 
         private record BadPoco(Guid Key, DateTimeOffset Val);
-
-        private record UnsignedPoco(ulong Key, string Val);
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleBuilder.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleBuilder.cs
index 1381d05f32..781e51d58b 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleBuilder.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleBuilder.cs
@@ -19,13 +19,12 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
 {
     using System;
     using System.Diagnostics;
-    using System.Text;
     using Buffers;
 
     /// <summary>
     /// Binary tuple builder.
     /// </summary>
-    internal sealed class BinaryTupleBuilder : IDisposable // TODO: Support all types (IGNITE-15431).
+    internal ref struct BinaryTupleBuilder // TODO: Support all types (IGNITE-15431).
     {
         /** Number of elements in the tuple. */
         private readonly int _numElements;
@@ -40,7 +39,7 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         private readonly int _valueBase;
 
         /** Buffer for tuple content. */
-        private readonly PooledArrayBufferWriter _buffer = new();
+        private readonly PooledArrayBufferWriter _buffer;
 
         /** Flag indicating if any NULL values were really put here. */
         private bool _hasNullValues;
@@ -49,16 +48,19 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         private int _elementIndex;
 
         /// <summary>
-        /// Initializes a new instance of the <see cref="BinaryTupleBuilder"/> class.
+        /// Initializes a new instance of the <see cref="BinaryTupleBuilder"/> struct.
         /// </summary>
         /// <param name="numElements">Capacity.</param>
         /// <param name="allowNulls">Whether nulls are allowed.</param>
         /// <param name="totalValueSize">Total value size, -1 when unknown.</param>
         public BinaryTupleBuilder(int numElements, bool allowNulls = true, int totalValueSize = -1)
         {
-            Debug.Assert(numElements > 0, "numElements > 0");
+            Debug.Assert(numElements >= 0, "numElements >= 0");
 
             _numElements = numElements;
+            _buffer = new();
+            _elementIndex = 0;
+            _hasNullValues = false;
 
             int baseOffset = BinaryTupleCommon.HeaderSize;
             if (allowNulls)
@@ -108,6 +110,14 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
             OnWrite();
         }
 
+        /// <summary>
+        /// Appends a default value.
+        /// </summary>
+        public void AppendDefault()
+        {
+            OnWrite();
+        }
+
         /// <summary>
         /// Appends a byte.
         /// </summary>
@@ -229,6 +239,23 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
             OnWrite();
         }
 
+        /// <summary>
+        /// Appends a string.
+        /// </summary>
+        /// <param name="value">Value.</param>
+        public void AppendStringNullable(string? value)
+        {
+            if (value == null)
+            {
+                AppendNull();
+                return;
+            }
+
+            PutString(value);
+
+            OnWrite();
+        }
+
         /// <summary>
         /// Appends bytes.
         /// </summary>
@@ -253,6 +280,65 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
             OnWrite();
         }
 
+        /// <summary>
+        /// Appends an object.
+        /// </summary>
+        /// <param name="value">Value.</param>
+        /// <param name="colType">Column type.</param>
+        public void AppendObject(object? value, ClientDataType colType)
+        {
+            if (value == null)
+            {
+                AppendNull();
+                return;
+            }
+
+            switch (colType)
+            {
+                case ClientDataType.Int8:
+                    AppendByte((sbyte)value);
+                    break;
+
+                case ClientDataType.Int16:
+                    AppendShort((short)value);
+                    break;
+
+                case ClientDataType.Int32:
+                    AppendInt((int)value);
+                    break;
+
+                case ClientDataType.Int64:
+                    AppendLong((long)value);
+                    break;
+
+                case ClientDataType.Float:
+                    AppendFloat((float)value);
+                    break;
+
+                case ClientDataType.Double:
+                    AppendDouble((double)value);
+                    break;
+
+                case ClientDataType.Uuid:
+                    AppendGuid((Guid)value);
+                    break;
+
+                case ClientDataType.String:
+                    AppendString((string)value);
+                    break;
+
+                case ClientDataType.Bytes:
+                    AppendBytes((byte[])value);
+                    break;
+
+                case ClientDataType.BitMask:
+                case ClientDataType.Decimal:
+                default:
+                    // TODO: Support all types (IGNITE-15431).
+                    throw new IgniteClientException("Unsupported type: " + colType);
+            }
+        }
+
         /// <summary>
         /// Builds the tuple.
         /// <para />
@@ -328,7 +414,9 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
             return _buffer.GetWrittenMemory().Slice(offset);
         }
 
-        /// <inheritdoc/>
+        /// <summary>
+        /// Disposes this instance.
+        /// </summary>
         public void Dispose()
         {
             _buffer.Dispose();
@@ -355,10 +443,10 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
                 return;
             }
 
-            var maxByteCount = Encoding.UTF8.GetMaxByteCount(value.Length);
+            var maxByteCount = BinaryTupleCommon.StringEncoding.GetMaxByteCount(value.Length);
             var span = _buffer.GetSpan(maxByteCount);
 
-            var actualBytes = Encoding.UTF8.GetBytes(value, span);
+            var actualBytes = BinaryTupleCommon.StringEncoding.GetBytes(value, span);
 
             _buffer.Advance(actualBytes);
         }
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleCommon.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleCommon.cs
index 6bfb494e13..ed5704f343 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleCommon.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleCommon.cs
@@ -17,6 +17,9 @@
 
 namespace Apache.Ignite.Internal.Proto.BinaryTuple
 {
+    using System.Diagnostics;
+    using System.Text;
+
     /// <summary>
     /// Common binary tuple constants and utils.
     /// </summary>
@@ -37,6 +40,11 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         /// </summary>
         public const int NullmapFlag = 0b100;
 
+        /// <summary>
+        /// UTF8 encoding without preamble (as opposed to <see cref="Encoding.UTF8"/>).
+        /// </summary>
+        public static readonly Encoding StringEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
+
         /// <summary>
         /// Calculates flags for a given size of variable-length area.
         /// </summary>
@@ -54,12 +62,9 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
                 return 0b01;
             }
 
-            if (size <= int.MaxValue)
-            {
-                return 0b10;
-            }
+            Debug.Assert(size <= int.MaxValue, "size <= int.MaxValue");
 
-            throw new IgniteClientException("Too big binary tuple size");
+            return 0b10;
         }
 
         /// <summary>
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleReader.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleReader.cs
index 36cabae670..7ea6189474 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleReader.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleReader.cs
@@ -20,12 +20,11 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
     using System;
     using System.Buffers.Binary;
     using System.Diagnostics;
-    using System.Text;
 
     /// <summary>
     /// Binary tuple reader.
     /// </summary>
-    internal sealed class BinaryTupleReader // TODO: Support all types (IGNITE-15431).
+    internal readonly ref struct BinaryTupleReader // TODO: Support all types (IGNITE-15431).
     {
         /** Buffer. */
         private readonly ReadOnlyMemory<byte> _buffer;
@@ -43,7 +42,7 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         private readonly int _valueBase;
 
         /// <summary>
-        /// Initializes a new instance of the <see cref="BinaryTupleReader"/> class.
+        /// Initializes a new instance of the <see cref="BinaryTupleReader"/> struct.
         /// </summary>
         /// <param name="buffer">Buffer.</param>
         /// <param name="numElements">Number of elements in the tuple.</param>
@@ -158,9 +157,16 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
         public string GetString(int index) => Seek(index) switch
         {
             { IsEmpty: true } => string.Empty,
-            var s => Encoding.UTF8.GetString(s)
+            var s => BinaryTupleCommon.StringEncoding.GetString(s)
         };
 
+        /// <summary>
+        /// Gets a string value.
+        /// </summary>
+        /// <param name="index">Index.</param>
+        /// <returns>Value.</returns>
+        public string? GetStringNullable(int index) => IsNull(index) ? null : GetString(index);
+
         /// <summary>
         /// Gets a float value.
         /// </summary>
@@ -184,6 +190,35 @@ namespace Apache.Ignite.Internal.Proto.BinaryTuple
             var s => BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64LittleEndian(s))
         };
 
+        /// <summary>
+        /// Gets an object value according to the specified type.
+        /// </summary>
+        /// <param name="index">Index.</param>
+        /// <param name="columnType">Column type.</param>
+        /// <returns>Value.</returns>
+        public object? GetObject(int index, ClientDataType columnType)
+        {
+            if (IsNull(index))
+            {
+                return null;
+            }
+
+            return columnType switch
+            {
+                ClientDataType.Int8 => GetByte(index),
+                ClientDataType.Int16 => GetShort(index),
+                ClientDataType.Int32 => GetInt(index),
+                ClientDataType.Int64 => GetLong(index),
+                ClientDataType.Float => GetFloat(index),
+                ClientDataType.Double => GetDouble(index),
+                ClientDataType.Uuid => GetGuid(index),
+                ClientDataType.String => GetString(index),
+
+                // TODO: Support all types (IGNITE-15431).
+                _ => throw new IgniteClientException("Unsupported type: " + columnType)
+            };
+        }
+
         private int GetOffset(int position)
         {
             var span = _buffer.Span[position..];
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackReaderExtensions.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackReaderExtensions.cs
index d694abe61b..7f51ebad72 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackReaderExtensions.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackReaderExtensions.cs
@@ -170,6 +170,20 @@ namespace Apache.Ignite.Internal.Proto
             return false;
         }
 
+        /// <summary>
+        /// Reads binary value as <see cref="ReadOnlyMemory{T}"/>.
+        /// </summary>
+        /// <param name="reader">Reader.</param>
+        /// <returns>Binary value.</returns>
+        public static ReadOnlyMemory<byte> ReadBytesAsMemory(this ref MessagePackReader reader)
+        {
+            ReadOnlySequence<byte> tupleSeq = reader.ReadBytes()!.Value;
+
+            Debug.Assert(tupleSeq.IsSingleSegment, "tupleSeq.IsSingleSegment");
+
+            return tupleSeq.First;
+        }
+
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         private static void ValidateExtensionType(
             ref MessagePackReader reader,
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackWriterExtensions.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackWriterExtensions.cs
index 9428590ec7..b4f7792636 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackWriterExtensions.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackWriterExtensions.cs
@@ -57,6 +57,24 @@ namespace Apache.Ignite.Internal.Proto
             writer.Advance(1);
         }
 
+        /// <summary>
+        /// Writes BitSet header and reserves space for bits, returns a span to write bits to.
+        /// </summary>
+        /// <param name="writer">Writer.</param>
+        /// <param name="bitCount">Bit count.</param>
+        /// <returns>Span to write bits to.</returns>
+        public static Span<byte> WriteBitSet(this ref MessagePackWriter writer, int bitCount)
+        {
+            var byteCount = bitCount / 8 + 1;
+            writer.WriteExtensionFormatHeader(new ExtensionHeader((sbyte)ClientMessagePackType.Bitmask, byteCount));
+
+            var span = writer.GetSpan(byteCount)[..byteCount];
+            span.Clear();
+            writer.Advance(byteCount);
+
+            return span;
+        }
+
         /// <summary>
         /// Writes an object.
         /// </summary>
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/CustomTestIgniteTuple.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/BinaryTupleBuilderExtensions.cs
similarity index 52%
copy from modules/platforms/dotnet/Apache.Ignite.Tests/Table/CustomTestIgniteTuple.cs
copy to modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/BinaryTupleBuilderExtensions.cs
index 42ee0bfcd1..a93f02297e 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/CustomTestIgniteTuple.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/BinaryTupleBuilderExtensions.cs
@@ -15,35 +15,25 @@
  * limitations under the License.
  */
 
-namespace Apache.Ignite.Tests.Table
+namespace Apache.Ignite.Internal.Table.Serialization
 {
-    using Ignite.Table;
+    using System;
+    using Proto.BinaryTuple;
 
     /// <summary>
-    /// Custom tuple implementation for tests.
+    /// Extensions for <see cref="BinaryTupleBuilder"/>.
     /// </summary>
-    public class CustomTestIgniteTuple : IIgniteTuple
+    internal static class BinaryTupleBuilderExtensions
     {
-        public const int Key = 42;
-
-        public const string Value = "Val1";
-
-        public int FieldCount => 2;
-
-        public object? this[int ordinal]
-        {
-            get => ordinal switch { 0 => Key, _ => Value };
-            set => throw new System.NotImplementedException();
-        }
-
-        public object? this[string name]
+        /// <summary>
+        /// Appends a no-value marker.
+        /// </summary>
+        /// <param name="builder">Builder.</param>
+        /// <param name="noValueSet">No-value bit set.</param>
+        public static void AppendNoValue(this ref BinaryTupleBuilder builder, Span<byte> noValueSet)
         {
-            get => name switch { "KEY" => Key, _ => Value };
-            set => throw new System.NotImplementedException();
+            noValueSet.SetBit(builder.ElementIndex);
+            builder.AppendDefault();
         }
-
-        public string GetName(int ordinal) => ordinal switch { 0 => "KEY", _ => "VAL" };
-
-        public int GetOrdinal(string name) => name switch { "KEY" => 0, _ => 1 };
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/BinaryTupleMethods.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/BinaryTupleMethods.cs
new file mode 100644
index 0000000000..b461a71ab9
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/BinaryTupleMethods.cs
@@ -0,0 +1,99 @@
+/*
+ * 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.Internal.Table.Serialization
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Reflection;
+    using Proto.BinaryTuple;
+
+    /// <summary>
+    /// MethodInfos for <see cref="BinaryTupleBuilder"/> and <see cref="BinaryTupleReader"/>.
+    /// </summary>
+    internal static class BinaryTupleMethods
+    {
+        /// <summary>
+        /// No-value writer.
+        /// </summary>
+        public static readonly MethodInfo WriteNoValue =
+            typeof(BinaryTupleBuilderExtensions).GetMethod(nameof(BinaryTupleBuilderExtensions.AppendNoValue))!;
+
+        private static readonly MethodInfo AppendByte = typeof(BinaryTupleBuilder).GetMethod(nameof(BinaryTupleBuilder.AppendByte))!;
+        private static readonly MethodInfo AppendShort = typeof(BinaryTupleBuilder).GetMethod(nameof(BinaryTupleBuilder.AppendShort))!;
+        private static readonly MethodInfo AppendInt = typeof(BinaryTupleBuilder).GetMethod(nameof(BinaryTupleBuilder.AppendInt))!;
+        private static readonly MethodInfo AppendLong = typeof(BinaryTupleBuilder).GetMethod(nameof(BinaryTupleBuilder.AppendLong))!;
+        private static readonly MethodInfo AppendFloat = typeof(BinaryTupleBuilder).GetMethod(nameof(BinaryTupleBuilder.AppendFloat))!;
+        private static readonly MethodInfo AppendDouble = typeof(BinaryTupleBuilder).GetMethod(nameof(BinaryTupleBuilder.AppendDouble))!;
+        private static readonly MethodInfo AppendGuid = typeof(BinaryTupleBuilder).GetMethod(nameof(BinaryTupleBuilder.AppendGuid))!;
+        private static readonly MethodInfo AppendString = typeof(BinaryTupleBuilder).GetMethod(nameof(BinaryTupleBuilder.AppendStringNullable))!;
+
+        private static readonly MethodInfo GetByte = typeof(BinaryTupleReader).GetMethod(nameof(BinaryTupleReader.GetByte))!;
+        private static readonly MethodInfo GetShort = typeof(BinaryTupleReader).GetMethod(nameof(BinaryTupleReader.GetShort))!;
+        private static readonly MethodInfo GetInt = typeof(BinaryTupleReader).GetMethod(nameof(BinaryTupleReader.GetInt))!;
+        private static readonly MethodInfo GetLong = typeof(BinaryTupleReader).GetMethod(nameof(BinaryTupleReader.GetLong))!;
+        private static readonly MethodInfo GetFloat = typeof(BinaryTupleReader).GetMethod(nameof(BinaryTupleReader.GetFloat))!;
+        private static readonly MethodInfo GetDouble = typeof(BinaryTupleReader).GetMethod(nameof(BinaryTupleReader.GetDouble))!;
+        private static readonly MethodInfo GetGuid = typeof(BinaryTupleReader).GetMethod(nameof(BinaryTupleReader.GetGuid))!;
+        private static readonly MethodInfo GetString = typeof(BinaryTupleReader).GetMethod(nameof(BinaryTupleReader.GetStringNullable))!;
+
+        // TODO: Support all types (IGNITE-15431).
+        private static readonly IReadOnlyDictionary<Type, MethodInfo> WriteMethods = new Dictionary<Type, MethodInfo>
+        {
+            { typeof(string), AppendString },
+            { typeof(sbyte), AppendByte },
+            { typeof(short), AppendShort },
+            { typeof(int), AppendInt },
+            { typeof(long), AppendLong },
+            { typeof(float), AppendFloat },
+            { typeof(double), AppendDouble },
+            { typeof(Guid), AppendGuid }
+        };
+
+        private static readonly IReadOnlyDictionary<Type, MethodInfo> ReadMethods = new Dictionary<Type, MethodInfo>
+        {
+            { typeof(string), GetString },
+            { typeof(sbyte), GetByte },
+            { typeof(short), GetShort },
+            { typeof(int), GetInt },
+            { typeof(long), GetLong },
+            { typeof(float), GetFloat },
+            { typeof(double), GetDouble },
+            { typeof(Guid), GetGuid }
+        };
+
+        /// <summary>
+        /// Gets the write method.
+        /// </summary>
+        /// <param name="valueType">Type of the value to write.</param>
+        /// <returns>Write method for the specified value type.</returns>
+        public static MethodInfo GetWriteMethod(Type valueType) =>
+            WriteMethods.TryGetValue(valueType, out var method)
+                ? method
+                : throw new IgniteClientException("Unsupported type: " + valueType);
+
+        /// <summary>
+        /// Gets the read method.
+        /// </summary>
+        /// <param name="valueType">Type of the value to read.</param>
+        /// <returns>Read method for the specified value type.</returns>
+        public static MethodInfo GetReadMethod(Type valueType) =>
+            ReadMethods.TryGetValue(valueType, out var method)
+                ? method
+                : throw new IgniteClientException("Unsupported type: " + valueType);
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/CustomTestIgniteTuple.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ByteSpanExtensions.cs
similarity index 51%
copy from modules/platforms/dotnet/Apache.Ignite.Tests/Table/CustomTestIgniteTuple.cs
copy to modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ByteSpanExtensions.cs
index 42ee0bfcd1..4edd6f82ae 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/CustomTestIgniteTuple.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ByteSpanExtensions.cs
@@ -15,35 +15,20 @@
  * limitations under the License.
  */
 
-namespace Apache.Ignite.Tests.Table
+namespace Apache.Ignite.Internal.Table.Serialization
 {
-    using Ignite.Table;
+    using System;
 
     /// <summary>
-    /// Custom tuple implementation for tests.
+    /// Extensions for <see cref="Span{T}"/>.
     /// </summary>
-    public class CustomTestIgniteTuple : IIgniteTuple
+    internal static class ByteSpanExtensions
     {
-        public const int Key = 42;
-
-        public const string Value = "Val1";
-
-        public int FieldCount => 2;
-
-        public object? this[int ordinal]
-        {
-            get => ordinal switch { 0 => Key, _ => Value };
-            set => throw new System.NotImplementedException();
-        }
-
-        public object? this[string name]
-        {
-            get => name switch { "KEY" => Key, _ => Value };
-            set => throw new System.NotImplementedException();
-        }
-
-        public string GetName(int ordinal) => ordinal switch { 0 => "KEY", _ => "VAL" };
-
-        public int GetOrdinal(string name) => name switch { "KEY" => 0, _ => 1 };
+        /// <summary>
+        /// Sets the bit at the specified index.
+        /// </summary>
+        /// <param name="span">Span.</param>
+        /// <param name="index">Bit index.</param>
+        public static void SetBit(this Span<byte> span, int index) => span[index / 8] |= (byte)(1 << (index % 8));
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/MessagePackMethods.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/MessagePackMethods.cs
deleted file mode 100644
index 4ca4de7d24..0000000000
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/MessagePackMethods.cs
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * 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.Internal.Table.Serialization
-{
-    using System;
-    using System.Collections.Generic;
-    using System.Reflection;
-    using MessagePack;
-    using Proto;
-
-    /// <summary>
-    /// MethodInfos for <see cref="MessagePackWriter"/> and <see cref="MessagePackReader"/>.
-    /// </summary>
-    internal static class MessagePackMethods
-    {
-        /// <summary>
-        /// No-value writer.
-        /// </summary>
-        public static readonly MethodInfo WriteNoValue =
-            typeof(MessagePackWriterExtensions).GetMethod(nameof(MessagePackWriterExtensions.WriteNoValue))!;
-
-        /// <summary>
-        /// No-value reader.
-        /// </summary>
-        public static readonly MethodInfo TryReadNoValue =
-            typeof(MessagePackReaderExtensions).GetMethod(nameof(MessagePackReaderExtensions.TryReadNoValue))!;
-
-        /// <summary>
-        /// Skip reader.
-        /// </summary>
-        public static readonly MethodInfo Skip =
-            typeof(MessagePackReaderExtensions).GetMethod(nameof(MessagePackReaderExtensions.Skip))!;
-
-        // TODO: Support all types (IGNITE-15431).
-        private static readonly IReadOnlyDictionary<Type, MethodInfo> WriteMethods = new Dictionary<Type, MethodInfo>
-        {
-            { typeof(string), GetWriteMethod<string>() },
-            { typeof(byte), GetWriteMethod<byte>() },
-            { typeof(sbyte), GetWriteMethod<sbyte>() },
-            { typeof(short), GetWriteMethod<short>() },
-            { typeof(ushort), GetWriteMethod<ushort>() },
-            { typeof(int), GetWriteMethod<int>() },
-            { typeof(uint), GetWriteMethod<uint>() },
-            { typeof(long), GetWriteMethod<long>() },
-            { typeof(ulong), GetWriteMethod<ulong>() },
-            { typeof(float), GetWriteMethod<float>() },
-            { typeof(Guid), GetWriteMethod<Guid>() },
-        };
-
-        private static readonly IReadOnlyDictionary<Type, MethodInfo> ReadMethods = new Dictionary<Type, MethodInfo>
-        {
-            { typeof(string), GetReadMethod<string>() },
-            { typeof(byte), GetReadMethod<byte>() },
-            { typeof(sbyte), GetReadMethod<sbyte>() },
-            { typeof(short), GetReadMethod<short>() },
-            { typeof(ushort), GetReadMethod<ushort>() },
-            { typeof(int), GetReadMethod<int>() },
-            { typeof(uint), GetReadMethod<uint>() },
-            { typeof(long), GetReadMethod<long>() },
-            { typeof(ulong), GetReadMethod<ulong>() },
-            { typeof(float), GetReadMethod<float>() },
-            { typeof(Guid), GetReadMethod<Guid>() },
-        };
-
-        /// <summary>
-        /// Gets the write method.
-        /// </summary>
-        /// <param name="valueType">Type of the value to write.</param>
-        /// <returns>Write method for the specified value type.</returns>
-        public static MethodInfo GetWriteMethod(Type valueType) =>
-            WriteMethods.TryGetValue(valueType, out var method)
-                ? method
-                : throw new IgniteClientException("Unsupported type: " + valueType);
-
-        /// <summary>
-        /// Gets the read method.
-        /// </summary>
-        /// <param name="valueType">Type of the value to read.</param>
-        /// <returns>Read method for the specified value type.</returns>
-        public static MethodInfo GetReadMethod(Type valueType) =>
-            ReadMethods.TryGetValue(valueType, out var method)
-                ? method
-                : throw new IgniteClientException("Unsupported type: " + valueType);
-
-        private static MethodInfo GetWriteMethod<TArg>()
-        {
-            const string methodName = nameof(MessagePackWriter.Write);
-
-            var methodInfo = typeof(MessagePackWriter).GetMethod(methodName, new[] { typeof(TArg) }) ??
-                             typeof(MessagePackWriterExtensions).GetMethod(
-                                 methodName, new[] { typeof(MessagePackWriter).MakeByRefType(), typeof(TArg) });
-
-            if (methodInfo == null)
-            {
-                throw new InvalidOperationException($"Method not found: Write({typeof(TArg).Name})");
-            }
-
-            return methodInfo;
-        }
-
-        private static MethodInfo GetReadMethod<TRes>()
-        {
-            var methodName = "Read" + typeof(TRes).Name;
-
-            var methodInfo = typeof(MessagePackReader).GetMethod(methodName) ??
-                             typeof(MessagePackReaderExtensions).GetMethod(methodName);
-
-            if (methodInfo == null)
-            {
-                throw new InvalidOperationException($"Method not found: {methodName}");
-            }
-
-            return methodInfo;
-        }
-    }
-}
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 9cd7f33005..8fc4f257bc 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
@@ -17,11 +17,13 @@
 
 namespace Apache.Ignite.Internal.Table.Serialization
 {
+    using System;
     using System.Collections.Concurrent;
     using System.Reflection;
     using System.Reflection.Emit;
     using MessagePack;
     using Proto;
+    using Proto.BinaryTuple;
 
     /// <summary>
     /// Object serializer handler.
@@ -36,11 +38,11 @@ namespace Apache.Ignite.Internal.Table.Serialization
 
         private readonly ConcurrentDictionary<int, ReadValuePartDelegate<T>> _valuePartReaders = new();
 
-        private delegate void WriteDelegate<in TV>(ref MessagePackWriter writer, TV value);
+        private delegate void WriteDelegate<in TV>(ref BinaryTupleBuilder writer, Span<byte> noValueSet, TV value);
 
-        private delegate TV ReadDelegate<out TV>(ref MessagePackReader reader);
+        private delegate TV ReadDelegate<out TV>(ref BinaryTupleReader reader);
 
-        private delegate TV ReadValuePartDelegate<TV>(ref MessagePackReader reader, TV key);
+        private delegate TV ReadValuePartDelegate<TV>(ref BinaryTupleReader reader, TV key);
 
         /// <inheritdoc/>
         public T Read(ref MessagePackReader reader, Schema schema, bool keyOnly = false)
@@ -51,7 +53,11 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 ? w
                 : _readers.GetOrAdd(cacheKey, EmitReader(schema, keyOnly));
 
-            return readDelegate(ref reader);
+            var columnCount = keyOnly ? schema.KeyColumnCount : schema.Columns.Count;
+
+            var binaryTupleReader = new BinaryTupleReader(reader.ReadBytesAsMemory(), columnCount);
+
+            return readDelegate(ref binaryTupleReader);
         }
 
         /// <inheritdoc/>
@@ -61,7 +67,9 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 ? w
                 : _valuePartReaders.GetOrAdd(schema.Version, EmitValuePartReader(schema));
 
-            return readDelegate(ref reader, key);
+            var binaryTupleReader = new BinaryTupleReader(reader.ReadBytesAsMemory(), schema.Columns.Count - schema.KeyColumnCount);
+
+            return readDelegate(ref binaryTupleReader, key);
         }
 
         /// <inheritdoc/>
@@ -73,7 +81,21 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 ? w
                 : _writers.GetOrAdd(cacheKey, EmitWriter(schema, keyOnly));
 
-            writeDelegate(ref writer, record);
+            var count = keyOnly ? schema.KeyColumnCount : schema.Columns.Count;
+            var noValueSet = writer.WriteBitSet(count);
+            var tupleBuilder = new BinaryTupleBuilder(count);
+
+            try
+            {
+                writeDelegate(ref tupleBuilder, noValueSet, record);
+
+                var binaryTupleMemory = tupleBuilder.Build();
+                writer.Write(binaryTupleMemory.Span);
+            }
+            finally
+            {
+                tupleBuilder.Dispose();
+            }
         }
 
         private static WriteDelegate<T> EmitWriter(Schema schema, bool keyOnly)
@@ -83,7 +105,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
             var method = new DynamicMethod(
                 name: "Write" + type.Name,
                 returnType: typeof(void),
-                parameterTypes: new[] { typeof(MessagePackWriter).MakeByRefType(), type },
+                parameterTypes: new[] { typeof(BinaryTupleBuilder).MakeByRefType(), typeof(Span<byte>), type },
                 m: typeof(IIgnite).Module,
                 skipVisibility: true);
 
@@ -100,16 +122,17 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 if (fieldInfo == null)
                 {
                     il.Emit(OpCodes.Ldarg_0); // writer
-                    il.Emit(OpCodes.Call, MessagePackMethods.WriteNoValue);
+                    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_1); // record
+                    il.Emit(OpCodes.Ldarg_2); // record
                     il.Emit(OpCodes.Ldfld, fieldInfo);
 
-                    var writeMethod = MessagePackMethods.GetWriteMethod(fieldInfo.FieldType);
+                    var writeMethod = BinaryTupleMethods.GetWriteMethod(fieldInfo.FieldType);
                     il.Emit(OpCodes.Call, writeMethod);
                 }
             }
@@ -126,7 +149,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
             var method = new DynamicMethod(
                 name: "Read" + type.Name,
                 returnType: type,
-                parameterTypes: new[] { typeof(MessagePackReader).MakeByRefType() },
+                parameterTypes: new[] { typeof(BinaryTupleReader).MakeByRefType() },
                 m: typeof(IIgnite).Module,
                 skipVisibility: true);
 
@@ -147,7 +170,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 var col = columns[i];
                 var fieldInfo = type.GetFieldIgnoreCase(col.Name);
 
-                EmitFieldRead(fieldInfo, il, col);
+                EmitFieldRead(fieldInfo, il, col, i);
             }
 
             il.Emit(OpCodes.Ldloc_0); // res
@@ -163,7 +186,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
             var method = new DynamicMethod(
                 name: "ReadValuePart" + type.Name,
                 returnType: type,
-                parameterTypes: new[] { typeof(MessagePackReader).MakeByRefType(), type },
+                parameterTypes: new[] { typeof(BinaryTupleReader).MakeByRefType(), type },
                 m: typeof(IIgnite).Module,
                 skipVisibility: true);
 
@@ -196,7 +219,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
                     continue;
                 }
 
-                EmitFieldRead(fieldInfo, il, col);
+                EmitFieldRead(fieldInfo, il, col, i - schema.KeyColumnCount);
             }
 
             il.Emit(OpCodes.Ldloc_0); // res
@@ -205,31 +228,68 @@ namespace Apache.Ignite.Internal.Table.Serialization
             return (ReadValuePartDelegate<T>)method.CreateDelegate(typeof(ReadValuePartDelegate<T>));
         }
 
-        private static void EmitFieldRead(FieldInfo? fieldInfo, ILGenerator il, Column col)
+        private static void EmitFieldRead(FieldInfo? fieldInfo, ILGenerator il, Column col, int elemIdx)
         {
             if (fieldInfo == null)
             {
-                il.Emit(OpCodes.Ldarg_0); // reader
-                il.Emit(OpCodes.Call, MessagePackMethods.Skip);
+                return;
             }
-            else
+
+            ValidateFieldType(fieldInfo, col);
+
+            var readMethod = BinaryTupleMethods.GetReadMethod(fieldInfo.FieldType);
+
+            il.Emit(OpCodes.Ldloc_0); // res
+            il.Emit(OpCodes.Ldarg_0); // reader
+            EmitLdcI4(il, elemIdx); // index
+
+            il.Emit(OpCodes.Call, readMethod);
+            il.Emit(OpCodes.Stfld, fieldInfo); // res.field = value
+        }
+
+        private static void EmitLdcI4(ILGenerator il, int elemIdx)
+        {
+            switch (elemIdx)
             {
-                ValidateFieldType(fieldInfo, col);
-                il.Emit(OpCodes.Ldarg_0); // reader
-                il.Emit(OpCodes.Call, MessagePackMethods.TryReadNoValue);
+                case 0:
+                    il.Emit(OpCodes.Ldc_I4_0);
+                    break;
+
+                case 1:
+                    il.Emit(OpCodes.Ldc_I4_1);
+                    break;
+
+                case 2:
+                    il.Emit(OpCodes.Ldc_I4_2);
+                    break;
+
+                case 3:
+                    il.Emit(OpCodes.Ldc_I4_3);
+                    break;
+
+                case 4:
+                    il.Emit(OpCodes.Ldc_I4_4);
+                    break;
 
-                Label noValueLabel = il.DefineLabel();
-                il.Emit(OpCodes.Brtrue_S, noValueLabel);
+                case 5:
+                    il.Emit(OpCodes.Ldc_I4_5);
+                    break;
 
-                var readMethod = MessagePackMethods.GetReadMethod(fieldInfo.FieldType);
+                case 6:
+                    il.Emit(OpCodes.Ldc_I4_6);
+                    break;
 
-                il.Emit(OpCodes.Ldloc_0); // res
-                il.Emit(OpCodes.Ldarg_0); // reader
+                case 7:
+                    il.Emit(OpCodes.Ldc_I4_7);
+                    break;
 
-                il.Emit(OpCodes.Call, readMethod);
-                il.Emit(OpCodes.Stfld, fieldInfo); // res.field = value
+                case 8:
+                    il.Emit(OpCodes.Ldc_I4_8);
+                    break;
 
-                il.MarkLabel(noValueLabel);
+                default:
+                    il.Emit(OpCodes.Ldc_I4, elemIdx);
+                    break;
             }
         }
 
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/TupleSerializerHandler.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/TupleSerializerHandler.cs
index e2f29523d5..747a5a53a1 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/TupleSerializerHandler.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/TupleSerializerHandler.cs
@@ -20,6 +20,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
     using Ignite.Table;
     using MessagePack;
     using Proto;
+    using Proto.BinaryTuple;
 
     /// <summary>
     /// Serializer handler for <see cref="IIgniteTuple"/>.
@@ -45,16 +46,12 @@ namespace Apache.Ignite.Internal.Table.Serialization
             var columns = schema.Columns;
             var count = keyOnly ? schema.KeyColumnCount : columns.Count;
             var tuple = new IgniteTuple(count);
+            var tupleReader = new BinaryTupleReader(reader.ReadBytesAsMemory(), count);
 
             for (var index = 0; index < count; index++)
             {
-                if (reader.TryReadNoValue())
-                {
-                    continue;
-                }
-
                 var column = columns[index];
-                tuple[column.Name] = reader.ReadObject(column.Type);
+                tuple[column.Name] = tupleReader.GetObject(index, column.Type);
             }
 
             return tuple;
@@ -65,6 +62,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
         {
             var columns = schema.Columns;
             var tuple = new IgniteTuple(columns.Count);
+            var tupleReader = new BinaryTupleReader(reader.ReadBytesAsMemory(), schema.Columns.Count - schema.KeyColumnCount);
 
             for (var i = 0; i < columns.Count; i++)
             {
@@ -76,12 +74,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 }
                 else
                 {
-                    if (reader.TryReadNoValue())
-                    {
-                        continue;
-                    }
-
-                    tuple[column.Name] = reader.ReadObject(column.Type);
+                    tuple[column.Name] = tupleReader.GetObject(i - schema.KeyColumnCount, column.Type);
                 }
             }
 
@@ -93,20 +86,33 @@ namespace Apache.Ignite.Internal.Table.Serialization
         {
             var columns = schema.Columns;
             var count = keyOnly ? schema.KeyColumnCount : columns.Count;
+            var noValueSet = writer.WriteBitSet(count);
 
-            for (var index = 0; index < count; index++)
-            {
-                var col = columns[index];
-                var colIdx = record.GetOrdinal(col.Name);
+            var tupleBuilder = new BinaryTupleBuilder(count);
 
-                if (colIdx < 0)
-                {
-                    writer.WriteNoValue();
-                }
-                else
+            try
+            {
+                for (var index = 0; index < count; index++)
                 {
-                    writer.WriteObject(record[colIdx]);
+                    var col = columns[index];
+                    var colIdx = record.GetOrdinal(col.Name);
+
+                    if (colIdx >= 0)
+                    {
+                        tupleBuilder.AppendObject(record[colIdx], col.Type);
+                    }
+                    else
+                    {
+                        tupleBuilder.AppendNoValue(noValueSet);
+                    }
                 }
+
+                var binaryTupleMemory = tupleBuilder.Build();
+                writer.Write(binaryTupleMemory.Span);
+            }
+            finally
+            {
+                tupleBuilder.Dispose();
             }
         }
     }