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 34d7af9  IGNITE-16341 .NET: Emit efficient user object serialization 
code (#599)
34d7af9 is described below

commit 34d7af9a97d69461e6bdf344dc24069dd5ee6112
Author: Pavel Tupitsyn <[email protected]>
AuthorDate: Fri Jan 28 15:03:56 2022 +0300

    IGNITE-16341 .NET: Emit efficient user object serialization code (#599)
    
    * Use IL.Emit in `ObjectSerializerHandler` to compile efficient 
serialization code that reads data directly into user object fields, which 
provides maximum performance and minimum allocations.
    * Use fields instead of properties, so that all scenarios are supported 
(user-defined fields, automatic properties, records, anonymous types).
    * Cache tables and views so that compiled delegates can be reused.
    
    Benchmark results (`ReadObject` and `WriteObject` correspond to changes in 
this ticket, `Old` is previous implementation, `Tuple` is `RecordBinaryView`):
    ```
     |           Method |       Mean |   Error |  StdDev | Ratio | RatioSD |  
Gen 0 | Allocated |
     |----------------- 
|-----------:|--------:|--------:|------:|--------:|-------:|----------:|
     | ReadObjectManual |   210.9 ns | 0.73 ns | 0.65 ns |  1.00 |    0.00 | 
0.0126 |      80 B |
     |       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 |
    
     |            Method |     Mean |   Error |  StdDev | Ratio | RatioSD |  
Gen 0 | Allocated |
     |------------------ 
|---------:|--------:|--------:|------:|--------:|-------:|----------:|
     | WriteObjectManual | 155.8 ns | 1.15 ns | 1.07 ns |  1.00 |    0.00 | 
0.0062 |      40 B |
     |       WriteObject | 167.0 ns | 0.76 ns | 0.75 ns |  1.07 |    0.01 | 
0.0062 |      40 B |
     |        WriteTuple | 324.7 ns | 4.35 ns | 4.07 ns |  2.08 |    0.02 | 
0.0229 |     144 B |
     |    WriteObjectOld | 798.5 ns | 5.10 ns | 4.77 ns |  5.13 |    0.04 | 
0.0381 |     240 B |
    ```
---
 .../dotnet/Apache.Ignite.Benchmarks/Program.cs     |   4 +-
 .../Serialization/ObjectSerializerHandlerOld.cs}   |  27 +--
 .../SerializerHandlerBenchmarksBase.cs             |  92 ++++++++
 .../SerializerHandlerReadBenchmarks.cs             |  82 +++++++
 .../SerializerHandlerWriteBenchmarks.cs            |  90 ++++++++
 .../Table/RecordViewDefaultMappingTest.cs          | 100 +++++++++
 .../Serialization/ObjectSerializerHandlerTests.cs  | 154 +++++++++++++
 .../Table/Serialization/ReflectionUtilsTests.cs    | 111 ++++++++++
 .../Apache.Ignite.Tests/Table/TablesTests.cs       |  12 ++
 .../Internal/Proto/ClientDataTypeExtensions.cs     |  52 +++++
 .../Apache.Ignite/Internal/Proto/IgniteUuid.cs     |  38 +++-
 .../Internal/Proto/MessagePackReaderExtensions.cs  |   6 +-
 .../Internal/Proto/MessagePackWriterExtensions.cs  |   2 +-
 .../dotnet/Apache.Ignite/Internal/Table/Schema.cs  |   4 +-
 .../Serialization/IRecordSerializerHandler.cs      |   5 +-
 .../Table/Serialization/MessagePackMethods.cs      | 131 ++++++++++++
 .../Table/Serialization/ObjectSerializerHandler.cs | 238 +++++++++++++++------
 .../Table/Serialization/RecordSerializer.cs        |   6 +-
 .../Table/Serialization/ReflectionUtils.cs         | 125 +++++++++++
 .../Table/Serialization/TupleSerializerHandler.cs  |  11 +-
 .../dotnet/Apache.Ignite/Internal/Table/Table.cs   |  16 +-
 .../dotnet/Apache.Ignite/Internal/Table/Tables.cs  |  10 +-
 .../platforms/dotnet/Apache.Ignite/Table/ITable.cs |   3 +
 23 files changed, 1198 insertions(+), 121 deletions(-)

diff --git a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Program.cs 
b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Program.cs
index 9178420..475523e 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Program.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Program.cs
@@ -18,13 +18,13 @@
 namespace Apache.Ignite.Benchmarks
 {
     using BenchmarkDotNet.Running;
-    using Proto;
+    using Table.Serialization;
 
     internal static class Program
     {
         private static void Main()
         {
-            BenchmarkRunner.Run<WriteGuidBenchmarks>();
+            BenchmarkRunner.Run<SerializerHandlerReadBenchmarks>();
         }
     }
 }
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/ObjectSerializerHandlerOld.cs
similarity index 83%
copy from 
modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
copy to 
modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/ObjectSerializerHandlerOld.cs
index 11280d0..7d7059b 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/ObjectSerializerHandlerOld.cs
@@ -15,25 +15,25 @@
  * limitations under the License.
  */
 
-namespace Apache.Ignite.Internal.Table.Serialization
+namespace Apache.Ignite.Benchmarks.Table.Serialization
 {
     using System;
     using System.Reflection;
-    using Buffers;
+    using Internal.Proto;
+    using Internal.Table;
+    using Internal.Table.Serialization;
     using MessagePack;
-    using Proto;
 
     /// <summary>
-    /// Object serializer handler.
+    /// Old object serializer handler implementation as a baseline for 
benchmarks.
     /// </summary>
     /// <typeparam name="T">Object type.</typeparam>
-    internal class ObjectSerializerHandler<T> : IRecordSerializerHandler<T>
+    internal class ObjectSerializerHandlerOld<T> : IRecordSerializerHandler<T>
         where T : class
     {
         /// <inheritdoc/>
         public T Read(ref MessagePackReader reader, Schema schema, bool 
keyOnly = false)
         {
-            // TODO: Emit code for efficient serialization (IGNITE-16341).
             var columns = schema.Columns;
             var count = keyOnly ? schema.KeyColumnCount : columns.Count;
             var res = Activator.CreateInstance<T>();
@@ -64,13 +64,8 @@ namespace Apache.Ignite.Internal.Table.Serialization
         }
 
         /// <inheritdoc/>
-        public T ReadValuePart(PooledBuffer buf, Schema schema, T key)
+        public T ReadValuePart(ref MessagePackReader reader, Schema schema, T 
key)
         {
-            // TODO: Emit code for efficient serialization (IGNITE-16341).
-            // Skip schema version.
-            var r = buf.GetReader();
-            r.Skip();
-
             var columns = schema.Columns;
             var res = Activator.CreateInstance<T>();
             var type = typeof(T);
@@ -89,18 +84,18 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 }
                 else
                 {
-                    if (r.TryReadNoValue())
+                    if (reader.TryReadNoValue())
                     {
                         continue;
                     }
 
                     if (prop != null)
                     {
-                        prop.SetValue(res, r.ReadObject(col.Type));
+                        prop.SetValue(res, reader.ReadObject(col.Type));
                     }
                     else
                     {
-                        r.Skip();
+                        reader.Skip();
                     }
                 }
             }
@@ -111,7 +106,6 @@ namespace Apache.Ignite.Internal.Table.Serialization
         /// <inheritdoc/>
         public void Write(ref MessagePackWriter writer, Schema schema, T 
record, bool keyOnly = false)
         {
-            // TODO: Emit code for efficient serialization (IGNITE-16341).
             var columns = schema.Columns;
             var count = keyOnly ? schema.KeyColumnCount : columns.Count;
             var type = record.GetType();
@@ -134,7 +128,6 @@ namespace Apache.Ignite.Internal.Table.Serialization
 
         private static PropertyInfo? GetPropertyIgnoreCase(Type type, string 
name)
         {
-            // TODO: Use fields, not properties (IGNITE-16341).
             foreach (var p in type.GetProperties())
             {
                 if (p.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerBenchmarksBase.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerBenchmarksBase.cs
new file mode 100644
index 0000000..9ed54ee
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerBenchmarksBase.cs
@@ -0,0 +1,92 @@
+/*
+ * 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.Benchmarks.Table.Serialization
+{
+    using System;
+    using BenchmarkDotNet.Engines;
+    using Ignite.Table;
+    using Internal.Buffers;
+    using Internal.Proto;
+    using Internal.Table;
+    using Internal.Table.Serialization;
+
+    /// <summary>
+    /// Base class for <see cref="IRecordSerializerHandler{T}"/> benchmarks.
+    /// </summary>
+    public abstract class SerializerHandlerBenchmarksBase
+    {
+        internal static readonly Car Object = new()
+        {
+            Id = Guid.NewGuid(),
+            BodyType = "Sedan",
+            Seats = 5
+        };
+
+        internal static readonly IgniteTuple Tuple = new()
+        {
+            [nameof(Car.Id)] = Object.Id,
+            [nameof(Car.BodyType)] = Object.BodyType,
+            [nameof(Car.Seats)] = Object.Seats
+        };
+
+        internal static readonly Schema Schema = new(1, 1, new[]
+        {
+            new Column(nameof(Car.Id), ClientDataType.Uuid, Nullable: false, 
IsKey: true, SchemaIndex: 0),
+            new Column(nameof(Car.BodyType), ClientDataType.String, Nullable: 
false, IsKey: false, SchemaIndex: 1),
+            new Column(nameof(Car.Seats), ClientDataType.Int32, Nullable: 
false, IsKey: false, SchemaIndex: 2)
+        });
+
+        internal static readonly byte[] SerializedData = GetSerializedData();
+
+        internal static readonly ObjectSerializerHandler<Car> 
ObjectSerializerHandler = new();
+
+        internal static readonly ObjectSerializerHandlerOld<Car> 
ObjectSerializerHandlerOld = new();
+
+        protected Consumer Consumer { get; } = new();
+
+        internal static void VerifyWritten(PooledArrayBufferWriter 
pooledWriter)
+        {
+            var bytesWritten = pooledWriter.GetWrittenMemory().Length;
+
+            if (bytesWritten != 29)
+            {
+                throw new Exception("Unexpected number of bytes written: " + 
bytesWritten);
+            }
+        }
+
+        private static byte[] GetSerializedData()
+        {
+            using var pooledWriter = new PooledArrayBufferWriter();
+            var writer = pooledWriter.GetMessageWriter();
+
+            TupleSerializerHandler.Instance.Write(ref writer, Schema, Tuple);
+
+            writer.Flush();
+            return pooledWriter.GetWrittenMemory().Slice(4).ToArray();
+        }
+
+        protected internal class Car
+        {
+            public Guid Id { get; set; }
+
+            public string BodyType { get; set; } = null!;
+
+            public int Seats { get; set; }
+        }
+    }
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerReadBenchmarks.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerReadBenchmarks.cs
new file mode 100644
index 0000000..9a14a67
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerReadBenchmarks.cs
@@ -0,0 +1,82 @@
+/*
+ * 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.Benchmarks.Table.Serialization
+{
+    using System.Diagnostics.CodeAnalysis;
+    using BenchmarkDotNet.Attributes;
+    using Internal.Proto;
+    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 |
+    /// |----------------- 
|-----------:|--------:|--------:|------:|--------:|-------:|----------:|
+    /// | ReadObjectManual |   210.9 ns | 0.73 ns | 0.65 ns |  1.00 |    0.00 
| 0.0126 |      80 B |
+    /// |       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 |.
+    /// </summary>
+    [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", 
Justification = "Benchmarks.")]
+    [MemoryDiagnoser]
+    public class SerializerHandlerReadBenchmarks : 
SerializerHandlerBenchmarksBase
+    {
+        [Benchmark(Baseline = true)]
+        public void ReadObjectManual()
+        {
+            var reader = new MessagePackReader(SerializedData);
+
+            var res = new Car
+            {
+                Id = reader.TryReadNoValue() ? default : reader.ReadGuid(),
+                BodyType = reader.TryReadNoValue() ? default! : 
reader.ReadString(),
+                Seats = reader.TryReadNoValue() ? default : reader.ReadInt32()
+            };
+
+            Consumer.Consume(res);
+        }
+
+        [Benchmark]
+        public void ReadObject()
+        {
+            var reader = new MessagePackReader(SerializedData);
+            var res = ObjectSerializerHandler.Read(ref reader, Schema);
+
+            Consumer.Consume(res);
+        }
+
+        [Benchmark]
+        public void ReadTuple()
+        {
+            var reader = new MessagePackReader(SerializedData);
+            var res = TupleSerializerHandler.Instance.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
new file mode 100644
index 0000000..3eb504d
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerWriteBenchmarks.cs
@@ -0,0 +1,90 @@
+/*
+ * 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.Benchmarks.Table.Serialization
+{
+    using System.Diagnostics.CodeAnalysis;
+    using BenchmarkDotNet.Attributes;
+    using Internal.Buffers;
+    using Internal.Proto;
+    using Internal.Table.Serialization;
+
+    /// <summary>
+    /// Benchmarks for <see cref="IRecordSerializerHandler{T}.Write"/> 
implementations.
+    /// Results on Intel Core i7-9700K, .NET SDK 3.1.416, Ubuntu 20.04:
+    /// |            Method |     Mean |   Error |  StdDev | Ratio | RatioSD | 
 Gen 0 | Allocated |
+    /// |------------------ 
|---------:|--------:|--------:|------:|--------:|-------:|----------:|
+    /// | WriteObjectManual | 155.8 ns | 1.15 ns | 1.07 ns |  1.00 |    0.00 | 
0.0062 |      40 B |
+    /// |       WriteObject | 167.0 ns | 0.76 ns | 0.75 ns |  1.07 |    0.01 | 
0.0062 |      40 B |
+    /// |        WriteTuple | 324.7 ns | 4.35 ns | 4.07 ns |  2.08 |    0.02 | 
0.0229 |     144 B |
+    /// |    WriteObjectOld | 798.5 ns | 5.10 ns | 4.77 ns |  5.13 |    0.04 | 
0.0381 |     240 B |.
+    /// </summary>
+    [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", 
Justification = "Benchmarks.")]
+    [MemoryDiagnoser]
+    public class SerializerHandlerWriteBenchmarks : 
SerializerHandlerBenchmarksBase
+    {
+        [Benchmark(Baseline = true)]
+        public void WriteObjectManual()
+        {
+            using var pooledWriter = new PooledArrayBufferWriter();
+            var writer = pooledWriter.GetMessageWriter();
+
+            writer.Write(Object.Id);
+            writer.Write(Object.BodyType);
+            writer.Write(Object.Seats);
+
+            writer.Flush();
+            VerifyWritten(pooledWriter);
+        }
+
+        [Benchmark]
+        public void WriteObject()
+        {
+            using var pooledWriter = new PooledArrayBufferWriter();
+            var writer = pooledWriter.GetMessageWriter();
+
+            ObjectSerializerHandler.Write(ref writer, Schema, Object);
+
+            writer.Flush();
+            VerifyWritten(pooledWriter);
+        }
+
+        [Benchmark]
+        public void WriteTuple()
+        {
+            using var pooledWriter = new PooledArrayBufferWriter();
+            var writer = pooledWriter.GetMessageWriter();
+
+            TupleSerializerHandler.Instance.Write(ref writer, Schema, Tuple);
+
+            writer.Flush();
+            VerifyWritten(pooledWriter);
+        }
+
+        [Benchmark]
+        public void WriteObjectOld()
+        {
+            using var pooledWriter = new PooledArrayBufferWriter();
+            var writer = pooledWriter.GetMessageWriter();
+
+            ObjectSerializerHandlerOld.Write(ref writer, Schema, Object);
+
+            writer.Flush();
+            VerifyWritten(pooledWriter);
+        }
+    }
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewDefaultMappingTest.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewDefaultMappingTest.cs
new file mode 100644
index 0000000..5c49224
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewDefaultMappingTest.cs
@@ -0,0 +1,100 @@
+/*
+ * 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.
+ */
+
+// ReSharper disable InconsistentNaming, NotAccessedField.Local, 
NotAccessedPositionalProperty.Local
+#pragma warning disable SA1201 // Type member order
+namespace Apache.Ignite.Tests.Table
+{
+    using System;
+    using System.Threading.Tasks;
+    using Ignite.Table;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests the default user type mapping behavior in <see 
cref="IRecordView{T}"/>.
+    /// </summary>
+    public class RecordViewDefaultMappingTest : IgniteTestsBase
+    {
+        [SetUp]
+        public async Task SetUp()
+        {
+            await Table.RecordBinaryView.UpsertAsync(null, GetTuple(1, "2"));
+        }
+
+        [Test]
+        public void TestPropertyMapping()
+        {
+            Poco res = Get(new Poco { Key = 1 });
+
+            Assert.AreEqual("2", res.Val);
+        }
+
+        [Test]
+        public void TestFieldMappingNoDefaultConstructor()
+        {
+            FieldsTest res = Get(new FieldsTest(1, null));
+
+            Assert.AreEqual("2", res.GetVal());
+        }
+
+        [Test]
+        public void TestRecordMapping()
+        {
+            RecordTest res = Get(new RecordTest(1, null, Guid.Empty));
+
+            Assert.AreEqual("2", res.Val);
+        }
+
+        [Test]
+        public void TestAnonymousTypeMapping()
+        {
+            var key = new
+            {
+                Key = 1L,
+                Val = "unused",
+                Date = DateTime.Now
+            };
+
+            var res = Get(key);
+
+            Assert.AreEqual("2", res.Val);
+        }
+
+        private T Get<T>(T key)
+            where T : class
+        {
+            return Table.GetRecordView<T>().GetAsync(null, 
key).GetAwaiter().GetResult()!;
+        }
+
+        private class FieldsTest
+        {
+            private readonly long key;
+
+            private readonly string? val;
+
+            public FieldsTest(long key, string? val)
+            {
+                this.key = key;
+                this.val = val;
+            }
+
+            public string? GetVal() => val;
+        }
+
+        private record RecordTest(long Key, string? Val, Guid Id);
+    }
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/ObjectSerializerHandlerTests.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/ObjectSerializerHandlerTests.cs
new file mode 100644
index 0000000..f18df9f
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/ObjectSerializerHandlerTests.cs
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Apache.Ignite.Tests.Table.Serialization
+{
+    using System;
+    using Internal.Buffers;
+    using Internal.Proto;
+    using Internal.Table;
+    using Internal.Table.Serialization;
+    using MessagePack;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests for <see cref="ObjectSerializerHandler{T}"/>.
+    /// </summary>
+    public class ObjectSerializerHandlerTests
+    {
+        // ReSharper disable NotAccessedPositionalProperty.Local
+        private record BadPoco(Guid Key, DateTimeOffset Val);
+
+        private record UnsignedPoco(ulong Key, string Val);
+
+        private static readonly Schema Schema = new(1, 1, new[]
+        {
+            new Column("Key", ClientDataType.Int64, false, true, 0),
+            new Column("Val", ClientDataType.String, false, false, 1)
+        });
+
+        [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 pooledWriter = Write(new UnsignedPoco(ulong.MaxValue, "foo"));
+
+            var resMem = pooledWriter.GetWrittenMemory()[4..]; // Skip length 
header.
+            var reader = new MessagePackReader(resMem);
+
+            Assert.AreEqual(ulong.MaxValue, reader.ReadUInt64());
+            Assert.AreEqual("foo", reader.ReadString());
+            Assert.IsTrue(reader.End);
+        }
+
+        [Test]
+        public void TestWriteKeyOnly()
+        {
+            var reader = WriteAndGetReader(keyOnly: true);
+
+            Assert.AreEqual(1234, reader.ReadInt32());
+            Assert.IsTrue(reader.End);
+        }
+
+        [Test]
+        public void TestRead()
+        {
+            var reader = WriteAndGetReader();
+            var resPoco = new ObjectSerializerHandler<Poco>().Read(ref reader, 
Schema);
+
+            Assert.AreEqual(1234, resPoco.Key);
+            Assert.AreEqual("foo", resPoco.Val);
+        }
+
+        [Test]
+        public void TestReadKeyOnly()
+        {
+            var reader = WriteAndGetReader();
+            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()
+        {
+            var ex = Assert.Throws<IgniteClientException>(() =>
+            {
+                var reader = WriteAndGetReader();
+                new ObjectSerializerHandler<BadPoco>().Read(ref reader, 
Schema);
+            });
+
+            Assert.AreEqual(
+                "Can't map field 'BadPoco.<Key>k__BackingField' of type 
'System.Guid'" +
+                " to column 'Key' of type 'System.Int64' - types do not 
match.",
+                ex!.Message);
+        }
+
+        [Test]
+        public void TestWriteUnsupportedFieldTypeThrowsException()
+        {
+            var ex = Assert.Throws<IgniteClientException>(() => Write(new 
BadPoco(Guid.Empty, DateTimeOffset.Now)));
+
+            Assert.AreEqual(
+                "Can't map field 'BadPoco.<Key>k__BackingField' of type 
'System.Guid'" +
+                " to column 'Key' of type 'System.Int64' - types do not 
match.",
+                ex!.Message);
+        }
+
+        private static MessagePackReader WriteAndGetReader(bool keyOnly = 
false)
+        {
+            var pooledWriter = Write(new Poco { Key = 1234, Val = "foo" }, 
keyOnly);
+
+            var resMem = pooledWriter.GetWrittenMemory()[4..]; // Skip length 
header.
+            return new MessagePackReader(resMem);
+        }
+
+        private static PooledArrayBufferWriter Write<T>(T obj, bool keyOnly = 
false)
+            where T : class
+        {
+            var handler = new ObjectSerializerHandler<T>();
+
+            using var pooledWriter = new PooledArrayBufferWriter();
+            var writer = pooledWriter.GetMessageWriter();
+
+            handler.Write(ref writer, Schema, obj, keyOnly);
+            writer.Flush();
+            return pooledWriter;
+        }
+    }
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/ReflectionUtilsTests.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/ReflectionUtilsTests.cs
new file mode 100644
index 0000000..5fa550d
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/Serialization/ReflectionUtilsTests.cs
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+
+// ReSharper disable InconsistentNaming
+// ReSharper disable UnusedMember.Local
+#pragma warning disable SA1306, SA1401, CS0649, CS0169, CA1823, CA1812
+namespace Apache.Ignite.Tests.Table.Serialization
+{
+    using System.Linq;
+    using Internal.Table.Serialization;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests for <see cref="ReflectionUtils"/>.
+    /// </summary>
+    public class ReflectionUtilsTests
+    {
+        [Test]
+        public void TestGetAllFieldsIncludesPrivatePublicAndInherited()
+        {
+            var fields = typeof(Derived).GetAllFields().Select(f => 
f.Name).OrderBy(x => x).ToArray();
+
+            var expected = new[]
+            {
+                "<BaseProp>k__BackingField",
+                "<BaseTwoProp>k__BackingField",
+                "<DerivedProp>k__BackingField",
+                "BaseFieldInternal",
+                "BaseFieldPrivate",
+                "BaseFieldProtected",
+                "BaseFieldPublic",
+                "BaseTwoFieldInternal",
+                "BaseTwoFieldPrivate",
+                "BaseTwoFieldProtected",
+                "BaseTwoFieldPublic",
+                "DerivedFieldInternal",
+                "DerivedFieldPrivate",
+                "DerivedFieldProtected",
+                "DerivedFieldPublic"
+            };
+
+            CollectionAssert.AreEqual(expected, fields);
+        }
+
+        [Test]
+        public void TestCleanFieldNameReturnsPropertyNameForBackingField()
+        {
+            var fieldNames = typeof(Derived).GetAllFields().Select(f => 
ReflectionUtils.CleanFieldName(f.Name)).ToArray();
+
+            CollectionAssert.Contains(fieldNames, nameof(Base.BaseProp));
+            CollectionAssert.Contains(fieldNames, nameof(BaseTwo.BaseTwoProp));
+            CollectionAssert.Contains(fieldNames, nameof(Derived.DerivedProp));
+        }
+
+        [Test]
+        [TestCase("Field", "Field")]
+        [TestCase("_foo", "_foo")]
+        [TestCase("m_fooBar", "m_fooBar")]
+        [TestCase("<MyProperty>k__BackingField", "MyProperty")]
+        [TestCase("FSharpProp@", "FSharpProp")]
+        [TestCase("<AnonTypeProp>i__Field", "AnonTypeProp")]
+        public void TestCleanFieldName(string name, string expected)
+        {
+            Assert.AreEqual(expected, ReflectionUtils.CleanFieldName(name));
+        }
+
+        private class Base
+        {
+            public int BaseFieldPublic;
+            internal int BaseFieldInternal;
+            protected int BaseFieldProtected;
+            private int BaseFieldPrivate;
+
+            public int BaseProp { get; set; }
+        }
+
+        private class BaseTwo : Base
+        {
+            public int BaseTwoFieldPublic;
+            internal int BaseTwoFieldInternal;
+            protected int BaseTwoFieldProtected;
+            private int BaseTwoFieldPrivate;
+
+            public int BaseTwoProp { get; set; }
+        }
+
+        private class Derived : BaseTwo
+        {
+            public int DerivedFieldPublic;
+            internal int DerivedFieldInternal;
+            protected int DerivedFieldProtected;
+            private int DerivedFieldPrivate;
+
+            public int DerivedProp { get; set; }
+        }
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/TablesTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/TablesTests.cs
index 1c93a65..0edc3f7 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/TablesTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/TablesTests.cs
@@ -46,6 +46,18 @@ namespace Apache.Ignite.Tests.Table
         }
 
         [Test]
+        public async Task TestGetExistingTableReturnsSameInstanceEveryTime()
+        {
+            var table = await Client.Tables.GetTableAsync(TableName);
+            var table2 = await Client.Tables.GetTableAsync(TableName);
+
+            // Tables and views are cached to avoid extra allocations and 
serializer handler initializations.
+            Assert.AreSame(table, table2);
+            Assert.AreSame(table!.RecordBinaryView, table2!.RecordBinaryView);
+            Assert.AreSame(table.GetRecordView<Poco>(), 
table2.GetRecordView<Poco>());
+        }
+
+        [Test]
         public async Task TestGetNonExistentTableReturnsNull()
         {
             var table = await 
Client.Tables.GetTableAsync(Guid.NewGuid().ToString());
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/ClientDataTypeExtensions.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/ClientDataTypeExtensions.cs
new file mode 100644
index 0000000..8bdb4fb
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/ClientDataTypeExtensions.cs
@@ -0,0 +1,52 @@
+/*
+ * 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.Proto
+{
+    using System;
+    using System.Collections;
+
+    /// <summary>
+    /// Extension methods for <see cref="ClientDataType"/>.
+    /// </summary>
+    internal static class ClientDataTypeExtensions
+    {
+        /// <summary>
+        /// Converts client data type to <see cref="Type"/>.
+        /// </summary>
+        /// <param name="clientDataType">Client data type.</param>
+        /// <returns>Corresponding CLR type.</returns>
+        public static (Type Primary, Type? Alternative) ToType(this 
ClientDataType clientDataType)
+        {
+            return clientDataType switch
+            {
+                ClientDataType.Int8 => (typeof(byte), typeof(sbyte)),
+                ClientDataType.Int16 => (typeof(short), typeof(ushort)),
+                ClientDataType.Int32 => (typeof(int), typeof(uint)),
+                ClientDataType.Int64 => (typeof(long), typeof(ulong)),
+                ClientDataType.Float => (typeof(float), null),
+                ClientDataType.Double => (typeof(double), null),
+                ClientDataType.Decimal => (typeof(decimal), null),
+                ClientDataType.Uuid => (typeof(Guid), null),
+                ClientDataType.String => (typeof(string), null),
+                ClientDataType.Bytes => (typeof(byte[]), null),
+                ClientDataType.BitMask => (typeof(BitArray), null),
+                _ => throw new 
ArgumentOutOfRangeException(nameof(clientDataType), clientDataType, null)
+            };
+        }
+    }
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/IgniteUuid.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/IgniteUuid.cs
index a1d7f25..6501704 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/IgniteUuid.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/IgniteUuid.cs
@@ -19,28 +19,50 @@
 namespace Apache.Ignite.Internal.Proto
 {
     using System;
+    using System.Runtime.InteropServices;
 
     /// <summary>
     /// Ignite UUID implementation combines a global UUID (generated once per 
node) and a node-local 8-byte id.
     /// </summary>
-    internal unsafe struct IgniteUuid
+    [StructLayout(LayoutKind.Sequential)]
+    internal struct IgniteUuid : IEquatable<IgniteUuid>
     {
         /// <summary>
-        /// Struct max size.
+        /// High part.
         /// </summary>
-        public const int MaxSize = 24;
+        public long Hi;
 
         /// <summary>
-        /// IgniteUuid bytes.
-        /// <para />
-        /// We could deserialize the data into <see cref="Guid"/> and <see 
cref="long"/>, but there is no need to deal
-        /// with the parts separately on the client.
+        /// Low part.
         /// </summary>
-        public fixed byte Bytes[MaxSize];
+        public long Low;
+
+        /// <summary>
+        /// Local part.
+        /// </summary>
+        public long Local;
 
         /// <summary>
         /// Struct size.
         /// </summary>
         public byte Size;
+
+        /// <inheritdoc/>
+        public override bool Equals(object? obj)
+        {
+            return obj is IgniteUuid other && Equals(other);
+        }
+
+        /// <inheritdoc/>
+        public override int GetHashCode()
+        {
+            return HashCode.Combine(Hi, Low, Local, Size);
+        }
+
+        /// <inheritdoc/>
+        public bool Equals(IgniteUuid other)
+        {
+            return Hi == other.Hi && Low == other.Low && Local == other.Local 
&& Size == other.Size;
+        }
     }
 }
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackReaderExtensions.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackReaderExtensions.cs
index 46bb371..5b50aec 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackReaderExtensions.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackReaderExtensions.cs
@@ -135,10 +135,12 @@ namespace Apache.Ignite.Internal.Proto
         public static unsafe IgniteUuid ReadIgniteUuid(this ref 
MessagePackReader reader)
         {
             var size = ValidateExtensionType(ref reader, 
ClientMessagePackType.IgniteUuid);
+            Debug.Assert(size < byte.MaxValue, "size < byte.MaxValue");
+
             var bytes = reader.ReadRaw(size);
 
-            var res = default(IgniteUuid);
-            bytes.CopyTo(new Span<byte>(res.Bytes, size));
+            IgniteUuid res = default;
+            bytes.CopyTo(new Span<byte>(&res, size));
             res.Size = (byte)size;
 
             return res;
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackWriterExtensions.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackWriterExtensions.cs
index d364e6b..71f728c 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackWriterExtensions.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/MessagePackWriterExtensions.cs
@@ -79,7 +79,7 @@ namespace Apache.Ignite.Internal.Proto
             writer.WriteExtensionFormatHeader(
                 new ExtensionHeader((sbyte)ClientMessagePackType.IgniteUuid, 
igniteUuid.Size));
 
-            writer.WriteRaw(new Span<byte>(igniteUuid.Bytes, igniteUuid.Size));
+            writer.WriteRaw(new Span<byte>(&igniteUuid, igniteUuid.Size));
         }
 
         /// <summary>
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Schema.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Schema.cs
index 89b6704..54c0df7 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Schema.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Schema.cs
@@ -29,10 +29,8 @@ namespace Apache.Ignite.Internal.Table
     /// <param name="Version">Version.</param>
     /// <param name="KeyColumnCount">Key column count.</param>
     /// <param name="Columns">Columns in schema order.</param>
-    /// <param name="ColumnsMap">Columns by name.</param>
     internal record Schema(
         int Version,
         int KeyColumnCount,
-        IReadOnlyList<Column> Columns,
-        IReadOnlyDictionary<string, Column> ColumnsMap);
+        IReadOnlyList<Column> Columns);
 }
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/IRecordSerializerHandler.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/IRecordSerializerHandler.cs
index a9fc56e..2b33037 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/IRecordSerializerHandler.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/IRecordSerializerHandler.cs
@@ -17,7 +17,6 @@
 
 namespace Apache.Ignite.Internal.Table.Serialization
 {
-    using Buffers;
     using MessagePack;
 
     /// <summary>
@@ -39,11 +38,11 @@ namespace Apache.Ignite.Internal.Table.Serialization
         /// <summary>
         /// Reads the value part and combines with the specified key part into 
a new object.
         /// </summary>
-        /// <param name="buf">Buffer.</param>
+        /// <param name="reader">Reader.</param>
         /// <param name="schema">Schema.</param>
         /// <param name="key">Key part.</param>
         /// <returns>Resulting record with key and value parts.</returns>
-        T? ReadValuePart(PooledBuffer buf, Schema schema, T key);
+        T? ReadValuePart(ref MessagePackReader reader, Schema schema, T key);
 
         /// <summary>
         /// Writes a record.
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/MessagePackMethods.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/MessagePackMethods.cs
new file mode 100644
index 0000000..4ca4de7
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/MessagePackMethods.cs
@@ -0,0 +1,131 @@
+/*
+ * 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 11280d0..9cd7f33 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
@@ -17,9 +17,9 @@
 
 namespace Apache.Ignite.Internal.Table.Serialization
 {
-    using System;
+    using System.Collections.Concurrent;
     using System.Reflection;
-    using Buffers;
+    using System.Reflection.Emit;
     using MessagePack;
     using Proto;
 
@@ -30,120 +30,220 @@ namespace Apache.Ignite.Internal.Table.Serialization
     internal class ObjectSerializerHandler<T> : IRecordSerializerHandler<T>
         where T : class
     {
+        private readonly ConcurrentDictionary<(int, bool), WriteDelegate<T>> 
_writers = new();
+
+        private readonly ConcurrentDictionary<(int, bool), ReadDelegate<T>> 
_readers = new();
+
+        private readonly ConcurrentDictionary<int, ReadValuePartDelegate<T>> 
_valuePartReaders = new();
+
+        private delegate void WriteDelegate<in TV>(ref MessagePackWriter 
writer, TV value);
+
+        private delegate TV ReadDelegate<out TV>(ref MessagePackReader reader);
+
+        private delegate TV ReadValuePartDelegate<TV>(ref MessagePackReader 
reader, TV key);
+
         /// <inheritdoc/>
         public T Read(ref MessagePackReader reader, Schema schema, bool 
keyOnly = false)
         {
-            // TODO: Emit code for efficient serialization (IGNITE-16341).
+            var cacheKey = (schema.Version, keyOnly);
+
+            var readDelegate = _readers.TryGetValue(cacheKey, out var w)
+                ? w
+                : _readers.GetOrAdd(cacheKey, EmitReader(schema, keyOnly));
+
+            return readDelegate(ref reader);
+        }
+
+        /// <inheritdoc/>
+        public T ReadValuePart(ref MessagePackReader reader, Schema schema, T 
key)
+        {
+            var readDelegate = _valuePartReaders.TryGetValue(schema.Version, 
out var w)
+                ? w
+                : _valuePartReaders.GetOrAdd(schema.Version, 
EmitValuePartReader(schema));
+
+            return readDelegate(ref reader, key);
+        }
+
+        /// <inheritdoc/>
+        public void Write(ref MessagePackWriter writer, Schema schema, T 
record, bool keyOnly = false)
+        {
+            var cacheKey = (schema.Version, keyOnly);
+
+            var writeDelegate = _writers.TryGetValue(cacheKey, out var w)
+                ? w
+                : _writers.GetOrAdd(cacheKey, EmitWriter(schema, keyOnly));
+
+            writeDelegate(ref writer, record);
+        }
+
+        private static WriteDelegate<T> EmitWriter(Schema schema, bool keyOnly)
+        {
+            var type = typeof(T);
+
+            var method = new DynamicMethod(
+                name: "Write" + type.Name,
+                returnType: typeof(void),
+                parameterTypes: new[] { 
typeof(MessagePackWriter).MakeByRefType(), type },
+                m: typeof(IIgnite).Module,
+                skipVisibility: true);
+
+            var il = method.GetILGenerator();
+
             var columns = schema.Columns;
             var count = keyOnly ? schema.KeyColumnCount : columns.Count;
-            var res = Activator.CreateInstance<T>();
-            var type = typeof(T);
 
             for (var index = 0; index < count; index++)
             {
-                if (reader.TryReadNoValue())
-                {
-                    continue;
-                }
-
                 var col = columns[index];
-                var prop = GetPropertyIgnoreCase(type, col.Name);
+                var fieldInfo = type.GetFieldIgnoreCase(col.Name);
 
-                if (prop != null)
+                if (fieldInfo == null)
                 {
-                    var value = reader.ReadObject(col.Type);
-                    prop.SetValue(res, value);
+                    il.Emit(OpCodes.Ldarg_0); // writer
+                    il.Emit(OpCodes.Call, MessagePackMethods.WriteNoValue);
                 }
                 else
                 {
-                    reader.Skip();
+                    ValidateFieldType(fieldInfo, col);
+                    il.Emit(OpCodes.Ldarg_0); // writer
+                    il.Emit(OpCodes.Ldarg_1); // record
+                    il.Emit(OpCodes.Ldfld, fieldInfo);
+
+                    var writeMethod = 
MessagePackMethods.GetWriteMethod(fieldInfo.FieldType);
+                    il.Emit(OpCodes.Call, writeMethod);
                 }
             }
 
-            return (T)(object)res;
+            il.Emit(OpCodes.Ret);
+
+            return 
(WriteDelegate<T>)method.CreateDelegate(typeof(WriteDelegate<T>));
         }
 
-        /// <inheritdoc/>
-        public T ReadValuePart(PooledBuffer buf, Schema schema, T key)
+        private static ReadDelegate<T> EmitReader(Schema schema, bool keyOnly)
         {
-            // TODO: Emit code for efficient serialization (IGNITE-16341).
-            // Skip schema version.
-            var r = buf.GetReader();
-            r.Skip();
+            var type = typeof(T);
+
+            var method = new DynamicMethod(
+                name: "Read" + type.Name,
+                returnType: type,
+                parameterTypes: new[] { 
typeof(MessagePackReader).MakeByRefType() },
+                m: typeof(IIgnite).Module,
+                skipVisibility: true);
+
+            var il = method.GetILGenerator();
+            il.DeclareLocal(type);
+
+            il.Emit(OpCodes.Ldtoken, type);
+            il.Emit(OpCodes.Call, ReflectionUtils.GetTypeFromHandleMethod);
+            il.Emit(OpCodes.Call, 
ReflectionUtils.GetUninitializedObjectMethod);
+
+            il.Emit(OpCodes.Stloc_0); // T res
 
             var columns = schema.Columns;
-            var res = Activator.CreateInstance<T>();
+            var count = keyOnly ? schema.KeyColumnCount : columns.Count;
+
+            for (var i = 0; i < count; i++)
+            {
+                var col = columns[i];
+                var fieldInfo = type.GetFieldIgnoreCase(col.Name);
+
+                EmitFieldRead(fieldInfo, il, col);
+            }
+
+            il.Emit(OpCodes.Ldloc_0); // res
+            il.Emit(OpCodes.Ret);
+
+            return 
(ReadDelegate<T>)method.CreateDelegate(typeof(ReadDelegate<T>));
+        }
+
+        private static ReadValuePartDelegate<T> EmitValuePartReader(Schema 
schema)
+        {
             var type = typeof(T);
 
+            var method = new DynamicMethod(
+                name: "ReadValuePart" + type.Name,
+                returnType: type,
+                parameterTypes: new[] { 
typeof(MessagePackReader).MakeByRefType(), type },
+                m: typeof(IIgnite).Module,
+                skipVisibility: true);
+
+            var il = method.GetILGenerator();
+            il.DeclareLocal(type);
+
+            il.Emit(OpCodes.Ldtoken, type);
+            il.Emit(OpCodes.Call, ReflectionUtils.GetTypeFromHandleMethod);
+            il.Emit(OpCodes.Call, 
ReflectionUtils.GetUninitializedObjectMethod);
+
+            il.Emit(OpCodes.Stloc_0); // T res
+
+            var columns = schema.Columns;
+
             for (var i = 0; i < columns.Count; i++)
             {
                 var col = columns[i];
-                var prop = GetPropertyIgnoreCase(type, col.Name);
+                var fieldInfo = type.GetFieldIgnoreCase(col.Name);
 
                 if (i < schema.KeyColumnCount)
                 {
-                    if (prop != null)
+                    if (fieldInfo != null)
                     {
-                        prop.SetValue(res, prop.GetValue(key));
-                    }
-                }
-                else
-                {
-                    if (r.TryReadNoValue())
-                    {
-                        continue;
+                        il.Emit(OpCodes.Ldloc_0); // res
+                        il.Emit(OpCodes.Ldarg_1); // key
+                        il.Emit(OpCodes.Ldfld, fieldInfo);
+                        il.Emit(OpCodes.Stfld, fieldInfo);
                     }
 
-                    if (prop != null)
-                    {
-                        prop.SetValue(res, r.ReadObject(col.Type));
-                    }
-                    else
-                    {
-                        r.Skip();
-                    }
+                    continue;
                 }
+
+                EmitFieldRead(fieldInfo, il, col);
             }
 
-            return res;
+            il.Emit(OpCodes.Ldloc_0); // res
+            il.Emit(OpCodes.Ret);
+
+            return 
(ReadValuePartDelegate<T>)method.CreateDelegate(typeof(ReadValuePartDelegate<T>));
         }
 
-        /// <inheritdoc/>
-        public void Write(ref MessagePackWriter writer, Schema schema, T 
record, bool keyOnly = false)
+        private static void EmitFieldRead(FieldInfo? fieldInfo, ILGenerator 
il, Column col)
         {
-            // TODO: Emit code for efficient serialization (IGNITE-16341).
-            var columns = schema.Columns;
-            var count = keyOnly ? schema.KeyColumnCount : columns.Count;
-            var type = record.GetType();
-
-            for (var index = 0; index < count; index++)
+            if (fieldInfo == null)
             {
-                var col = columns[index];
-                var prop = GetPropertyIgnoreCase(type, col.Name);
+                il.Emit(OpCodes.Ldarg_0); // reader
+                il.Emit(OpCodes.Call, MessagePackMethods.Skip);
+            }
+            else
+            {
+                ValidateFieldType(fieldInfo, col);
+                il.Emit(OpCodes.Ldarg_0); // reader
+                il.Emit(OpCodes.Call, MessagePackMethods.TryReadNoValue);
 
-                if (prop == null)
-                {
-                    writer.WriteNoValue();
-                }
-                else
-                {
-                    writer.WriteObject(prop.GetValue(record));
-                }
+                Label noValueLabel = il.DefineLabel();
+                il.Emit(OpCodes.Brtrue_S, noValueLabel);
+
+                var readMethod = 
MessagePackMethods.GetReadMethod(fieldInfo.FieldType);
+
+                il.Emit(OpCodes.Ldloc_0); // res
+                il.Emit(OpCodes.Ldarg_0); // reader
+
+                il.Emit(OpCodes.Call, readMethod);
+                il.Emit(OpCodes.Stfld, fieldInfo); // res.field = value
+
+                il.MarkLabel(noValueLabel);
             }
         }
 
-        private static PropertyInfo? GetPropertyIgnoreCase(Type type, string 
name)
+        private static void ValidateFieldType(FieldInfo fieldInfo, Column 
column)
         {
-            // TODO: Use fields, not properties (IGNITE-16341).
-            foreach (var p in type.GetProperties())
+            var (columnTypePrimary, columnTypeAlternative) = 
column.Type.ToType();
+            var fieldType = fieldInfo.FieldType;
+
+            if (fieldType != columnTypePrimary && fieldType != 
columnTypeAlternative)
             {
-                if (p.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
-                {
-                    return p;
-                }
+                throw new IgniteClientException(
+                    $"Can't map field 
'{fieldInfo.DeclaringType?.Name}.{fieldInfo.Name}' of type '{fieldType}' " +
+                    $"to column '{column.Name}' of type '{columnTypePrimary}' 
- types do not match.");
             }
-
-            return null;
         }
     }
 }
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/RecordSerializer.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/RecordSerializer.cs
index 111f389..f30a57d 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/RecordSerializer.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/RecordSerializer.cs
@@ -62,7 +62,11 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 return null;
             }
 
-            return _handler.ReadValuePart(buf, schema, key);
+            // Skip schema version.
+            var r = buf.GetReader();
+            r.Skip();
+
+            return _handler.ReadValuePart(ref r, schema, key);
         }
 
         /// <summary>
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ReflectionUtils.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ReflectionUtils.cs
new file mode 100644
index 0000000..d976239
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ReflectionUtils.cs
@@ -0,0 +1,125 @@
+/*
+ * 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.Concurrent;
+    using System.Collections.Generic;
+    using System.Linq;
+    using System.Linq.Expressions;
+    using System.Reflection;
+    using System.Runtime.Serialization;
+
+    /// <summary>
+    /// Reflection utilities.
+    /// </summary>
+    internal static class ReflectionUtils
+    {
+        /// <summary>
+        /// GetUninitializedObject method.
+        /// </summary>
+        public static readonly MethodInfo GetUninitializedObjectMethod = 
GetMethodInfo(
+            () => FormatterServices.GetUninitializedObject(null!));
+
+        /// <summary>
+        /// GetTypeFromHandle method.
+        /// </summary>
+        public static readonly MethodInfo GetTypeFromHandleMethod = 
GetMethodInfo(() => Type.GetTypeFromHandle(default));
+
+        private static readonly ConcurrentDictionary<Type, 
IReadOnlyDictionary<string, FieldInfo>> FieldsByNameCache = new();
+
+        /// <summary>
+        /// Gets all fields from the type, including non-public and inherited.
+        /// </summary>
+        /// <param name="type">Type.</param>
+        /// <returns>Fields.</returns>
+        public static IEnumerable<FieldInfo> GetAllFields(this Type type)
+        {
+            const BindingFlags flags = BindingFlags.Instance | 
BindingFlags.Public |
+                                       BindingFlags.NonPublic | 
BindingFlags.DeclaredOnly;
+
+            var t = type;
+
+            while (t != null)
+            {
+                foreach (var field in t.GetFields(flags))
+                {
+                    yield return field;
+                }
+
+                t = t.BaseType;
+            }
+        }
+
+        /// <summary>
+        /// Gets the field by name ignoring case.
+        /// </summary>
+        /// <param name="type">Type.</param>
+        /// <param name="name">Field name.</param>
+        /// <returns>Field info, or null when no matching fields 
exist.</returns>
+        public static FieldInfo? GetFieldIgnoreCase(this Type type, string 
name)
+        {
+            // ReSharper disable once HeapView.CanAvoidClosure, 
ConvertClosureToMethodGroup
+            return FieldsByNameCache.GetOrAdd(type, t => 
GetFieldsByName(t)).TryGetValue(name, out var fieldInfo)
+                ? fieldInfo
+                : null;
+
+            static IReadOnlyDictionary<string, FieldInfo> GetFieldsByName(Type 
type) =>
+                type.GetAllFields().ToDictionary(f => f.GetCleanName(), 
StringComparer.OrdinalIgnoreCase);
+        }
+
+        /// <summary>
+        /// Gets cleaned up member name without compiler-generated prefixes 
and suffixes.
+        /// </summary>
+        /// <param name="memberInfo">Member.</param>
+        /// <returns>Clean name.</returns>
+        public static string GetCleanName(this MemberInfo memberInfo) => 
CleanFieldName(memberInfo.Name);
+
+        /// <summary>
+        /// Cleans the field name and removes compiler-generated prefixes and 
suffixes.
+        /// </summary>
+        /// <param name="fieldName">Field name to clean.</param>
+        /// <returns>Resulting field name.</returns>
+        public static string CleanFieldName(string fieldName)
+        {
+            // C# auto property backing field (<MyProperty>k__BackingField)
+            // or anonymous type backing field (<MyProperty>i__Field):
+            if (fieldName.StartsWith("<", StringComparison.Ordinal)
+                && fieldName.IndexOf(">", StringComparison.Ordinal) is var 
endIndex and > 0)
+            {
+                return fieldName.Substring(1, endIndex - 1);
+            }
+
+            // F# backing field:
+            if (fieldName.EndsWith("@", StringComparison.Ordinal))
+            {
+                return fieldName.Substring(0, fieldName.Length - 1);
+            }
+
+            return fieldName;
+        }
+
+        /// <summary>
+        /// Gets the method info.
+        /// </summary>
+        /// <param name="expression">Expression.</param>
+        /// <typeparam name="T">Argument type.</typeparam>
+        /// <returns>Corresponding MethodInfo.</returns>
+        public static MethodInfo GetMethodInfo<T>(Expression<Func<T>> 
expression) => ((MethodCallExpression)expression.Body).Method;
+    }
+}
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 aa740d4..e2f2952 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/TupleSerializerHandler.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/TupleSerializerHandler.cs
@@ -17,7 +17,6 @@
 
 namespace Apache.Ignite.Internal.Table.Serialization
 {
-    using Buffers;
     using Ignite.Table;
     using MessagePack;
     using Proto;
@@ -62,12 +61,8 @@ namespace Apache.Ignite.Internal.Table.Serialization
         }
 
         /// <inheritdoc/>
-        public IIgniteTuple ReadValuePart(PooledBuffer buf, Schema schema, 
IIgniteTuple key)
+        public IIgniteTuple ReadValuePart(ref MessagePackReader reader, Schema 
schema, IIgniteTuple key)
         {
-            // Skip schema version.
-            var r = buf.GetReader();
-            r.Skip();
-
             var columns = schema.Columns;
             var tuple = new IgniteTuple(columns.Count);
 
@@ -81,12 +76,12 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 }
                 else
                 {
-                    if (r.TryReadNoValue())
+                    if (reader.TryReadNoValue())
                     {
                         continue;
                     }
 
-                    tuple[column.Name] = r.ReadObject(column.Type);
+                    tuple[column.Name] = reader.ReadObject(column.Type);
                 }
             }
 
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
index 73f6e93..d9f6873 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
@@ -17,8 +17,8 @@
 
 namespace Apache.Ignite.Internal.Table
 {
+    using System;
     using System.Collections.Concurrent;
-    using System.Collections.Generic;
     using System.Diagnostics;
     using System.Threading.Tasks;
     using Buffers;
@@ -41,6 +41,9 @@ namespace Apache.Ignite.Internal.Table
         /** Schemas. */
         private readonly ConcurrentDictionary<int, Schema> _schemas = new();
 
+        /** Cached record views. */
+        private readonly ConcurrentDictionary<Type, object> _recordViews = 
new();
+
         /** */
         private readonly object _latestSchemaLock = new();
 
@@ -74,7 +77,10 @@ namespace Apache.Ignite.Internal.Table
         public IRecordView<T> GetRecordView<T>()
             where T : class
         {
-            return new RecordView<T>(this, new RecordSerializer<T>(this, new 
ObjectSerializerHandler<T>()));
+            // ReSharper disable once HeapView.CanAvoidClosure (generics 
prevent this)
+            return (IRecordView<T>)_recordViews.GetOrAdd(
+                typeof(T),
+                _ => new RecordView<T>(this, new RecordSerializer<T>(this, new 
ObjectSerializerHandler<T>())));
         }
 
         /// <summary>
@@ -92,7 +98,7 @@ namespace Apache.Ignite.Internal.Table
             }
             else
             {
-                w.WriteInt64(tx.Id);
+                w.Write(tx.Id);
             }
         }
 
@@ -226,7 +232,6 @@ namespace Apache.Ignite.Internal.Table
             var keyColumnCount = 0;
 
             var columns = new Column[columnCount];
-            var columnsMap = new Dictionary<string, Column>(columnCount);
 
             for (var i = 0; i < columnCount; i++)
             {
@@ -244,7 +249,6 @@ namespace Apache.Ignite.Internal.Table
                 var column = new Column(name, (ClientDataType)type, 
isNullable, isKey, i);
 
                 columns[i] = column;
-                columnsMap[column.Name] = column;
 
                 if (isKey)
                 {
@@ -252,7 +256,7 @@ namespace Apache.Ignite.Internal.Table
                 }
             }
 
-            var schema = new Schema(schemaVersion, keyColumnCount, columns, 
columnsMap);
+            var schema = new Schema(schemaVersion, keyColumnCount, columns);
 
             _schemas[schemaVersion] = schema;
 
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Tables.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Tables.cs
index fcecb0e..4f33354 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Tables.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Tables.cs
@@ -17,6 +17,7 @@
 
 namespace Apache.Ignite.Internal.Table
 {
+    using System.Collections.Concurrent;
     using System.Collections.Generic;
     using System.Threading.Tasks;
     using Buffers;
@@ -33,6 +34,9 @@ namespace Apache.Ignite.Internal.Table
         /** Socket. */
         private readonly ClientFailoverSocket _socket;
 
+        /** Cached tables. */
+        private readonly ConcurrentDictionary<IgniteUuid, ITable> _tables = 
new();
+
         /// <summary>
         /// Initializes a new instance of the <see cref="Tables"/> class.
         /// </summary>
@@ -59,10 +63,14 @@ namespace Apache.Ignite.Internal.Table
                 w.Flush();
             }
 
+            // ReSharper disable once LambdaExpressionMustBeStatic (requires 
.NET 5+)
             ITable? Read(MessagePackReader r) =>
                 r.NextMessagePackType == MessagePackType.Nil
                     ? null
-                    : new Table(name, r.ReadIgniteUuid(), _socket);
+                    : _tables.GetOrAdd(
+                        r.ReadIgniteUuid(),
+                        (IgniteUuid id, (string Name, ClientFailoverSocket 
Socket) arg) => new Table(arg.Name, id, arg.Socket),
+                        (name, _socket));
         }
 
         /// <inheritdoc/>
diff --git a/modules/platforms/dotnet/Apache.Ignite/Table/ITable.cs 
b/modules/platforms/dotnet/Apache.Ignite/Table/ITable.cs
index a44b472..82e75c5 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Table/ITable.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Table/ITable.cs
@@ -34,6 +34,9 @@ namespace Apache.Ignite.Table
 
         /// <summary>
         /// Gets the record view mapped to specified type <typeparamref 
name="T"/>.
+        /// <para />
+        /// Table columns will be mapped to properties or fields by name, 
ignoring case. Any fields are supported,
+        /// including private and readonly.
         /// </summary>
         /// <typeparam name="T">Record type.</typeparam>
         /// <returns>Record view.</returns>

Reply via email to