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 1b0c1f8336d IGNITE-27705 .NET: Allow primitive mapping without 
reflection (#7505)
1b0c1f8336d is described below

commit 1b0c1f8336d79963dd78f6220010596348517d8f
Author: Pavel Tupitsyn <[email protected]>
AuthorDate: Tue Feb 3 14:06:26 2026 +0200

    IGNITE-27705 .NET: Allow primitive mapping without reflection (#7505)
    
    Add predefined mappers for simple types, so that reflection and runtime 
codegen are not required for things like table.GetRecordView<long>(), 
table.GetKeyValueView<Guid, string>().
---
 .../SerializerHandlerBenchmarksBase.cs             |   4 +
 .../SerializerHandlerReadBenchmarks.cs             |   9 +
 .../SerializerHandlerWriteBenchmarks.cs            |  12 ++
 .../Apache.Ignite.Tests.Aot/Table/TableTests.cs    |  71 +++++++
 .../Apache.Ignite.Tests.Common/Table/TestTables.cs |   2 +-
 .../Table/KeyValueViewPrimitiveTests.cs            |  11 ++
 .../Table/RecordViewPrimitiveTests.cs              |   3 +-
 .../Table/Serialization/Mappers/KeyValueMappers.cs |  43 ++++
 .../Mappers/KeyValuePairCompositeMapper.cs         |  85 ++++++++
 .../Table/Serialization/Mappers/MapperReader.cs    |  29 +++
 .../Table/Serialization/Mappers/MapperWriter.cs    |  29 +++
 .../Table/Serialization/Mappers/OneColumnMapper.cs |  51 +++++
 .../Serialization/Mappers/OneColumnMappers.cs      | 219 +++++++++++++++++++++
 .../dotnet/Apache.Ignite/Internal/Table/Table.cs   |  46 ++++-
 14 files changed, 609 insertions(+), 5 deletions(-)

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 34d900de19c..91ef77c0639 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerBenchmarksBase.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerBenchmarksBase.cs
@@ -18,6 +18,7 @@
 namespace Apache.Ignite.Benchmarks.Table.Serialization
 {
     using System;
+    using System.Collections.Generic;
     using BenchmarkDotNet.Engines;
     using Ignite.Sql;
     using Ignite.Table;
@@ -25,6 +26,7 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
     using Internal.Buffers;
     using Internal.Table;
     using Internal.Table.Serialization;
+    using Internal.Table.Serialization.Mappers;
 
     /// <summary>
     /// Base class for <see cref="IRecordSerializerHandler{T}"/> benchmarks.
@@ -63,6 +65,8 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
 
         internal static readonly IRecordSerializerHandler<Car> 
MapperKnownOrderSerializerHandler = new MapperSerializerHandler<Car>(new 
CarMapperKnownOrder());
 
+        internal static readonly IRecordSerializerHandler<KvPair<Guid, 
string>> MapperPairSerializerHandler = new MapperPairSerializerHandler<Guid, 
string>(KeyValueMappers.TryCreate<Guid, string>()!);
+
         protected Consumer Consumer { get; } = new();
 
         internal static void VerifyWritten(PooledArrayBuffer pooledWriter)
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 b0c5172425b..2a66b7dfc53 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerReadBenchmarks.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerReadBenchmarks.cs
@@ -103,5 +103,14 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
             Consumer.Consume(res[1]!);
             Consumer.Consume(res[2]!);
         }
+
+        [Benchmark]
+        public void ReadKeyValuePair()
+        {
+            var reader = new MsgPackReader(SerializedData);
+            var res = MapperPairSerializerHandler.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 9a7717a036b..178dbdd7b90 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerWriteBenchmarks.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerWriteBenchmarks.cs
@@ -17,6 +17,7 @@
 
 namespace Apache.Ignite.Benchmarks.Table.Serialization
 {
+    using System;
     using System.Diagnostics.CodeAnalysis;
     using BenchmarkDotNet.Attributes;
     using Internal.Buffers;
@@ -100,5 +101,16 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
 
             VerifyWritten(pooledWriter);
         }
+
+        [Benchmark]
+        public void WriteKeyValuePair()
+        {
+            using var pooledWriter = new PooledArrayBuffer();
+            var writer = pooledWriter.MessageWriter;
+
+            MapperPairSerializerHandler.Write(ref writer, Schema, new 
KvPair<Guid, string>(Object.Id, Object.BodyType));
+
+            VerifyWritten(pooledWriter);
+        }
     }
 }
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests.Aot/Table/TableTests.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests.Aot/Table/TableTests.cs
index afb5822e23b..ecef33e922d 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests.Aot/Table/TableTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests.Aot/Table/TableTests.cs
@@ -17,6 +17,7 @@
 
 namespace Apache.Ignite.Tests.Aot.Table;
 
+using System.Diagnostics.CodeAnalysis;
 using Common.Table;
 using Ignite.Table;
 using JetBrains.Annotations;
@@ -107,6 +108,76 @@ public class TableTests(IIgniteClient client)
         Assert.AreEqual(poco.Uuid, res.Uuid);
     }
 
+    [UsedImplicitly]
+    [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = 
"Test.")]
+    [SuppressMessage("ReSharper", "RedundantTypeArgumentsOfMethod", 
Justification = "Consistency.")]
+    public async Task TestRecordViewPrimitiveMapping()
+    {
+        await Test<sbyte>(TableInt8Name, 42);
+        await Test<short>(TableInt16Name, 42);
+        await Test<int>(TableInt32Name, 42);
+        await Test<long>(TableInt64Name, 42);
+        await Test<float>(TableFloatName, 3.14f);
+        await Test<double>(TableDoubleName, 3.14);
+        await Test<string>(TableStringName, "Hello, Ignite!");
+        await Test<Guid>(TableUuidName, Guid.NewGuid());
+        await Test<bool>(TableBoolName, true);
+        await Test<decimal>(TableDecimalName, 123.456m);
+        await Test<BigDecimal>(TableDecimalName, new BigDecimal(12345.67m));
+        await Test<LocalDate>(TableDateName, new LocalDate(2024, 6, 30));
+        await Test<LocalTime>(TableTimeName, new LocalTime(14, 30, 0));
+        await Test<LocalDateTime>(TableDateTimeName, new LocalDateTime(2024, 
6, 30, 14, 30, 0));
+        await Test<Instant>(TableTimestampName, Instant.FromUtc(2024, 6, 30, 
14, 30, 0));
+        await Test<byte[]>(TableBytesName, [1, 2, 3, 4, 5]);
+
+        async Task Test<T>(string tableName, T value)
+            where T : notnull
+        {
+            var tbl = await client.Tables.GetTableAsync(tableName);
+            var view = tbl!.GetRecordView<T>();
+
+            await view.UpsertAsync(null, value);
+            var res = await view.GetAsync(null, value);
+
+            Assert.AreEqual(value, res.Value);
+        }
+    }
+
+    [UsedImplicitly]
+    [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = 
"Test.")]
+    [SuppressMessage("ReSharper", "RedundantTypeArgumentsOfMethod", 
Justification = "Consistency.")]
+    public async Task TestKeyValueViewPrimitiveMapping()
+    {
+        await Test<sbyte>(TableInt8Name, 42);
+        await Test<short>(TableInt16Name, 42);
+        await Test<int>(TableInt32Name, 42);
+        await Test<long>(TableInt64Name, 42);
+        await Test<float>(TableFloatName, 3.14f);
+        await Test<double>(TableDoubleName, 3.14);
+        await Test<string>(TableStringName, "key");
+        await Test<Guid>(TableUuidName, Guid.NewGuid());
+        await Test<bool>(TableBoolName, true);
+        await Test<decimal>(TableDecimalName, 123.456m);
+        await Test<BigDecimal>(TableDecimalName, new BigDecimal(123.45m));
+        await Test<LocalDate>(TableDateName, new LocalDate(2024, 6, 30));
+        await Test<LocalTime>(TableTimeName, new LocalTime(14, 30, 0));
+        await Test<LocalDateTime>(TableDateTimeName, new LocalDateTime(2024, 
6, 30, 14, 30, 0));
+        await Test<Instant>(TableTimestampName, Instant.FromUtc(2024, 6, 30, 
14, 30, 0));
+        await Test<byte[]>(TableBytesName, [1, 2, 3]);
+
+        async Task Test<T>(string tableName, T key)
+            where T : notnull
+        {
+            var tbl = await client.Tables.GetTableAsync(tableName);
+            var view = tbl!.GetKeyValueView<T, T>();
+
+            await view.PutAsync(null, key, key);
+            var res = await view.GetAsync(null, key);
+
+            Assert.AreEqual(key, res.Value);
+        }
+    }
+
     [UsedImplicitly]
     public async Task TestDataStreamer()
     {
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests.Common/Table/TestTables.cs 
b/modules/platforms/dotnet/Apache.Ignite.Tests.Common/Table/TestTables.cs
index ae065e1d703..505f3e81447 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests.Common/Table/TestTables.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests.Common/Table/TestTables.cs
@@ -39,8 +39,8 @@ public static class TestTables
     public const string TableDateTimeName = "TBL_DATETIME";
     public const string TableTimeName = "TBL_TIME";
     public const string TableTimestampName = "TBL_TIMESTAMP";
-    public const string TableNumberName = "TBL_NUMBER";
     public const string TableBytesName = "TBL_BYTE_ARRAY";
+    public const string TableUuidName = "TBL_UUID";
 
     public const string KeyCol = "key";
     public const string ValCol = "val";
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPrimitiveTests.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPrimitiveTests.cs
index d8348afa53e..6fe27371502 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPrimitiveTests.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/KeyValueViewPrimitiveTests.cs
@@ -113,6 +113,16 @@ public class KeyValueViewPrimitiveTests : IgniteTestsBase
         Assert.AreEqual("Can't map 'System.Int64' to column 'VAL' - column is 
nullable, but field is not.", ex!.Message);
     }
 
+    [Test]
+    public async Task TestTypeMismatch()
+    {
+        var table = await Client.Tables.GetTableAsync(TableInt64Name);
+        var view = table!.GetKeyValueView<long, double?>();
+
+        var ex = Assert.ThrowsAsync<IgniteClientException>(async () => await 
view.GetAsync(null, 2));
+        Assert.AreEqual("Can't read a value of type 'Double' from column 'VAL' 
of type 'Int64'.", ex!.Message);
+    }
+
     [Test]
     public async Task TestGetNonExistentKeyReturnsEmptyOption()
     {
@@ -381,6 +391,7 @@ public class KeyValueViewPrimitiveTests : IgniteTestsBase
         await TestKey(instant, (Instant?)instant, TableTimestampName);
 
         await TestKey(new byte[] { 1, 2, 3 }, new byte[] { 1, 2, 3, 4 }, 
TableBytesName);
+        await TestKey(Guid.NewGuid(), Guid.NewGuid(), TableUuidName);
     }
 
     [Test]
diff --git 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPrimitiveTests.cs
 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPrimitiveTests.cs
index dd04aab6390..a5937195880 100644
--- 
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPrimitiveTests.cs
+++ 
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPrimitiveTests.cs
@@ -54,13 +54,14 @@ public class RecordViewPrimitiveTests(string mode) : 
IgniteTestsBase(useMapper:
         await TestKey(new LocalTime(3, 4, 5), TableTimeName);
         await TestKey(Instant.FromUnixTimeMilliseconds(123456789101112), 
TableTimestampName);
         await TestKey(new byte[] { 1, 2, 3 }, TableBytesName);
+        await TestKey(Guid.NewGuid(), TableUuidName);
     }
 
     [Test]
     public void TestColumnTypeMismatchThrowsException()
     {
         var ex = Assert.ThrowsAsync<IgniteClientException>(async () => await 
TestKey(1f, Table.GetRecordView<float>()));
-        Assert.AreEqual("Can't map 'System.Single' to column 'KEY' of type 
'System.Int64' - types do not match.", ex!.Message);
+        Assert.AreEqual("Can't write a value of type 'Float' to column 'KEY' 
of type 'Int64'.", ex!.Message);
     }
 
     [Test]
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/Mappers/KeyValueMappers.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/Mappers/KeyValueMappers.cs
new file mode 100644
index 00000000000..cb3d1fc838b
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/Mappers/KeyValueMappers.cs
@@ -0,0 +1,43 @@
+/*
+ * 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.Mappers;
+
+/// <summary>
+/// Key-value mapper helpers.
+/// </summary>
+internal static class KeyValueMappers
+{
+    /// <summary>
+    /// Creates a key-value pair mapper for the specified types if supported; 
otherwise, returns null.
+    /// </summary>
+    /// <typeparam name="TKey">Key type.</typeparam>
+    /// <typeparam name="TValue">Value type.</typeparam>
+    /// <returns>Key-value pair mapper or null.</returns>
+    public static KeyValuePairCompositeMapper<TKey, TValue>? TryCreate<TKey, 
TValue>()
+    {
+        var keyMapper = OneColumnMappers.TryCreate<TKey>();
+        var valueMapper = OneColumnMappers.TryCreate<TValue>();
+
+        if (keyMapper == null || valueMapper == null)
+        {
+            return null;
+        }
+
+        return new KeyValuePairCompositeMapper<TKey, TValue>(keyMapper, 
valueMapper);
+    }
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/Mappers/KeyValuePairCompositeMapper.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/Mappers/KeyValuePairCompositeMapper.cs
new file mode 100644
index 00000000000..e4751979079
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/Mappers/KeyValuePairCompositeMapper.cs
@@ -0,0 +1,85 @@
+/*
+ * 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.Mappers;
+
+using System.Collections.Generic;
+using Apache.Ignite.Table.Mapper;
+
+/// <summary>
+/// Key-value mapper.
+/// </summary>
+/// <typeparam name="TKey">Key type.</typeparam>
+/// <typeparam name="TValue">Value type.</typeparam>
+internal sealed record KeyValuePairCompositeMapper<TKey, 
TValue>(OneColumnMapper<TKey> KeyMapper, OneColumnMapper<TValue> ValMapper)
+    : IMapper<KeyValuePair<TKey, TValue>>
+{
+    /// <inheritdoc />
+    public void Write(KeyValuePair<TKey, TValue> obj, ref RowWriter rowWriter, 
IMapperSchema schema)
+    {
+        bool keyWritten = false;
+        bool valueWritten = false;
+
+        foreach (var column in schema.Columns)
+        {
+            if (!keyWritten && column is Column { IsKey: true })
+            {
+                KeyMapper.Writer(obj.Key, ref rowWriter, column);
+                keyWritten = true;
+            }
+            else if (!valueWritten)
+            {
+                ValMapper.Writer(obj.Value, ref rowWriter, column);
+                valueWritten = true;
+            }
+            else
+            {
+                rowWriter.Skip();
+            }
+        }
+    }
+
+    /// <inheritdoc />
+    public KeyValuePair<TKey, TValue> Read(ref RowReader rowReader, 
IMapperSchema schema)
+    {
+        TKey key = default!;
+        TValue value = default!;
+
+        bool keyRead = false;
+        bool valueRead = false;
+
+        foreach (var column in schema.Columns)
+        {
+            if (!keyRead && column is Column { IsKey: true })
+            {
+                key = KeyMapper.Reader(ref rowReader, column);
+                keyRead = true;
+            }
+            else if (!valueRead)
+            {
+                value = ValMapper.Reader(ref rowReader, column);
+                valueRead = true;
+            }
+            else
+            {
+                rowReader.Skip();
+            }
+        }
+
+        return KeyValuePair.Create(key, value);
+    }
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/Mappers/MapperReader.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/Mappers/MapperReader.cs
new file mode 100644
index 00000000000..73ce2cecf99
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/Mappers/MapperReader.cs
@@ -0,0 +1,29 @@
+/*
+ * 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.Mappers;
+
+using Ignite.Table.Mapper;
+
+/// <summary>
+/// Reader delegate.
+/// </summary>
+/// <param name="rowReader">Reader.</param>
+/// <param name="column">Column.</param>
+/// <typeparam name="T">Type.</typeparam>
+/// <returns>Result.</returns>
+internal delegate T MapperReader<out T>(ref RowReader rowReader, IMapperColumn 
column);
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/Mappers/MapperWriter.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/Mappers/MapperWriter.cs
new file mode 100644
index 00000000000..f73be0e8ea3
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/Mappers/MapperWriter.cs
@@ -0,0 +1,29 @@
+/*
+ * 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.Mappers;
+
+using Ignite.Table.Mapper;
+
+/// <summary>
+/// Writer delegate.
+/// </summary>
+/// <param name="obj">Object.</param>
+/// <param name="rowWriter">Writer.</param>
+/// <param name="column">Column.</param>
+/// <typeparam name="T">Type.</typeparam>
+internal delegate void MapperWriter<in T>(T obj, ref RowWriter rowWriter, 
IMapperColumn column);
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/Mappers/OneColumnMapper.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/Mappers/OneColumnMapper.cs
new file mode 100644
index 00000000000..30dac1a70ef
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/Mappers/OneColumnMapper.cs
@@ -0,0 +1,51 @@
+/*
+ * 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.Mappers;
+
+using System.Diagnostics.CodeAnalysis;
+using Apache.Ignite.Table.Mapper;
+
+/// <summary>
+/// One column mapper. Maps the first schema column to a simple type.
+/// The rest of the columns are ignored (consistent with existing <see 
cref="ObjectSerializerHandler{T}"/> behavior).
+/// </summary>
+/// <typeparam name="T">Type.</typeparam>
+[SuppressMessage("MaintainabilityRules", "SA1402:File may only contain a 
single type", Justification = "Reviewed.")]
+internal sealed record OneColumnMapper<T>(MapperReader<T> Reader, 
MapperWriter<T> Writer) : IMapper<T>
+{
+    /// <inheritdoc/>
+    public void Write(T obj, ref RowWriter rowWriter, IMapperSchema schema)
+    {
+        for (int i = 0; i < schema.Columns.Count; i++)
+        {
+            if (i == 0)
+            {
+                Writer(obj, ref rowWriter, schema.Columns[i]);
+            }
+            else
+            {
+                // Every column must be handled (written or skipped).
+                rowWriter.Skip();
+            }
+        }
+    }
+
+    /// <inheritdoc/>
+    public T Read(ref RowReader rowReader, IMapperSchema schema) =>
+        Reader(ref rowReader, schema.Columns[0]);
+}
diff --git 
a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/Mappers/OneColumnMappers.cs
 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/Mappers/OneColumnMappers.cs
new file mode 100644
index 00000000000..176e702b4c7
--- /dev/null
+++ 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/Mappers/OneColumnMappers.cs
@@ -0,0 +1,219 @@
+/*
+ * 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.Mappers;
+
+using System;
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using Ignite.Table.Mapper;
+using NodaTime;
+
+/// <summary>
+/// Primitive mapper helper.
+/// </summary>
+internal static class OneColumnMappers
+{
+    private static readonly OneColumnMapper<sbyte> SByteMapper = new(
+        (ref RowReader reader, IMapperColumn column) => 
UnwrapNullable(reader.ReadByte(), column),
+        (sbyte obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteByte(obj));
+
+    private static readonly OneColumnMapper<sbyte?> SByteNullableMapper = new(
+        (ref RowReader reader, IMapperColumn _) => reader.ReadByte(),
+        (sbyte? obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteByte(obj));
+
+    private static readonly OneColumnMapper<bool> BoolMapper = new(
+        (ref RowReader reader, IMapperColumn column) => 
UnwrapNullable(reader.ReadBool(), column),
+        (bool obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteBool(obj));
+
+    private static readonly OneColumnMapper<bool?> BoolNullableMapper = new(
+        (ref RowReader reader, IMapperColumn _) => reader.ReadBool(),
+        (bool? obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteBool(obj));
+
+    private static readonly OneColumnMapper<short> ShortMapper = new(
+        (ref RowReader reader, IMapperColumn column) => 
UnwrapNullable(reader.ReadShort(), column),
+        (short obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteShort(obj));
+
+    private static readonly OneColumnMapper<short?> ShortNullableMapper = new(
+        (ref RowReader reader, IMapperColumn _) => reader.ReadShort(),
+        (short? obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteShort(obj));
+
+    private static readonly OneColumnMapper<int> IntMapper = new(
+        (ref RowReader reader, IMapperColumn column) => 
UnwrapNullable(reader.ReadInt(), column),
+        (int obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteInt(obj));
+
+    private static readonly OneColumnMapper<int?> IntNullableMapper = new(
+        (ref RowReader reader, IMapperColumn _) => reader.ReadInt(),
+        (int? obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteInt(obj));
+
+    private static readonly OneColumnMapper<long> LongMapper = new(
+        (ref RowReader reader, IMapperColumn column) => 
UnwrapNullable(reader.ReadLong(), column),
+        (long obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteLong(obj));
+
+    private static readonly OneColumnMapper<long?> LongNullableMapper = new(
+        (ref RowReader reader, IMapperColumn _) => reader.ReadLong(),
+        (long? obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteLong(obj));
+
+    private static readonly OneColumnMapper<float> FloatMapper = new(
+        (ref RowReader reader, IMapperColumn column) => 
UnwrapNullable(reader.ReadFloat(), column),
+        (float obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteFloat(obj));
+
+    private static readonly OneColumnMapper<float?> FloatNullableMapper = new(
+        (ref RowReader reader, IMapperColumn _) => reader.ReadFloat(),
+        (float? obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteFloat(obj));
+
+    private static readonly OneColumnMapper<double> DoubleMapper = new(
+        (ref RowReader reader, IMapperColumn column) => 
UnwrapNullable(reader.ReadDouble(), column),
+        (double obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteDouble(obj));
+
+    private static readonly OneColumnMapper<double?> DoubleNullableMapper = 
new(
+        (ref RowReader reader, IMapperColumn _) => reader.ReadDouble(),
+        (double? obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteDouble(obj));
+
+    private static readonly OneColumnMapper<string?> StringMapper = new(
+        (ref RowReader reader, IMapperColumn _) => reader.ReadString(),
+        (string? obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteString(obj));
+
+    private static readonly OneColumnMapper<byte[]?> ByteArrayMapper = new(
+        (ref RowReader reader, IMapperColumn _) => reader.ReadBytes(),
+        (byte[]? obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteBytes(obj));
+
+    private static readonly OneColumnMapper<Guid> GuidMapper = new(
+        (ref RowReader reader, IMapperColumn column) => 
UnwrapNullable(reader.ReadGuid(), column),
+        (Guid obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteGuid(obj));
+
+    private static readonly OneColumnMapper<Guid?> GuidNullableMapper = new(
+        (ref RowReader reader, IMapperColumn _) => reader.ReadGuid(),
+        (Guid? obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteGuid(obj));
+
+    private static readonly OneColumnMapper<decimal> DecimalMapper = new(
+        (ref RowReader reader, IMapperColumn column) => 
UnwrapNullable(reader.ReadDecimal(), column),
+        (decimal obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteDecimal(obj));
+
+    private static readonly OneColumnMapper<decimal?> DecimalNullableMapper = 
new(
+        (ref RowReader reader, IMapperColumn _) => reader.ReadDecimal(),
+        (decimal? obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteDecimal(obj));
+
+    private static readonly OneColumnMapper<BigDecimal> BigDecimalMapper = new(
+        (ref RowReader reader, IMapperColumn column) => 
UnwrapNullable(reader.ReadBigDecimal(), column),
+        (BigDecimal obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteBigDecimal(obj));
+
+    private static readonly OneColumnMapper<BigDecimal?> 
BigDecimalNullableMapper = new(
+        (ref RowReader reader, IMapperColumn _) => reader.ReadBigDecimal(),
+        (BigDecimal? obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteBigDecimal(obj));
+
+    private static readonly OneColumnMapper<LocalDate> LocalDateMapper = new(
+        (ref RowReader reader, IMapperColumn column) => 
UnwrapNullable(reader.ReadDate(), column),
+        (LocalDate obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteDate(obj));
+
+    private static readonly OneColumnMapper<LocalDate?> 
LocalDateNullableMapper = new(
+        (ref RowReader reader, IMapperColumn _) => reader.ReadDate(),
+        (LocalDate? obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteDate(obj));
+
+    private static readonly OneColumnMapper<LocalTime> LocalTimeMapper = new(
+        (ref RowReader reader, IMapperColumn column) => 
UnwrapNullable(reader.ReadTime(), column),
+        (LocalTime obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteTime(obj));
+
+    private static readonly OneColumnMapper<LocalTime?> 
LocalTimeNullableMapper = new(
+        (ref RowReader reader, IMapperColumn _) => reader.ReadTime(),
+        (LocalTime? obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteTime(obj));
+
+    private static readonly OneColumnMapper<LocalDateTime> LocalDateTimeMapper 
= new(
+        (ref RowReader reader, IMapperColumn column) => 
UnwrapNullable(reader.ReadDateTime(), column),
+        (LocalDateTime obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteDateTime(obj));
+
+    private static readonly OneColumnMapper<LocalDateTime?> 
LocalDateTimeNullableMapper = new(
+        (ref RowReader reader, IMapperColumn _) => reader.ReadDateTime(),
+        (LocalDateTime? obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteDateTime(obj));
+
+    private static readonly OneColumnMapper<Instant> InstantMapper = new(
+        (ref RowReader reader, IMapperColumn column) => 
UnwrapNullable(reader.ReadTimestamp(), column),
+        (Instant obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteTimestamp(obj));
+
+    private static readonly OneColumnMapper<Instant?> InstantNullableMapper = 
new(
+        (ref RowReader reader, IMapperColumn _) => reader.ReadTimestamp(),
+        (Instant? obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteTimestamp(obj));
+
+    private static readonly OneColumnMapper<Duration> DurationMapper = new(
+        (ref RowReader reader, IMapperColumn column) => 
UnwrapNullable(reader.ReadDuration(), column),
+        (Duration obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteDuration(obj));
+
+    private static readonly OneColumnMapper<Duration?> DurationNullableMapper 
= new(
+        (ref RowReader reader, IMapperColumn _) => reader.ReadDuration(),
+        (Duration? obj, ref RowWriter writer, IMapperColumn _) => 
writer.WriteDuration(obj));
+
+    private static readonly OneColumnMapper<Period?> PeriodMapper = new(
+        (ref RowReader reader, IMapperColumn _) => reader.ReadPeriod(),
+        (Period? obj, ref RowWriter writer, IMapperColumn _) => 
writer.WritePeriod(obj));
+
+    private static readonly FrozenDictionary<Type, object> Mappers = new 
Dictionary<Type, object>
+    {
+        { typeof(sbyte), SByteMapper },
+        { typeof(sbyte?), SByteNullableMapper },
+        { typeof(bool), BoolMapper },
+        { typeof(bool?), BoolNullableMapper },
+        { typeof(short), ShortMapper },
+        { typeof(short?), ShortNullableMapper },
+        { typeof(int), IntMapper },
+        { typeof(int?), IntNullableMapper },
+        { typeof(long), LongMapper },
+        { typeof(long?), LongNullableMapper },
+        { typeof(float), FloatMapper },
+        { typeof(float?), FloatNullableMapper },
+        { typeof(double), DoubleMapper },
+        { typeof(double?), DoubleNullableMapper },
+        { typeof(string), StringMapper },
+        { typeof(byte[]), ByteArrayMapper },
+        { typeof(Guid), GuidMapper },
+        { typeof(Guid?), GuidNullableMapper },
+        { typeof(decimal), DecimalMapper },
+        { typeof(decimal?), DecimalNullableMapper },
+        { typeof(BigDecimal), BigDecimalMapper },
+        { typeof(BigDecimal?), BigDecimalNullableMapper },
+        { typeof(LocalDate), LocalDateMapper },
+        { typeof(LocalDate?), LocalDateNullableMapper },
+        { typeof(LocalTime), LocalTimeMapper },
+        { typeof(LocalTime?), LocalTimeNullableMapper },
+        { typeof(LocalDateTime), LocalDateTimeMapper },
+        { typeof(LocalDateTime?), LocalDateTimeNullableMapper },
+        { typeof(Instant), InstantMapper },
+        { typeof(Instant?), InstantNullableMapper },
+        { typeof(Duration), DurationMapper },
+        { typeof(Duration?), DurationNullableMapper },
+        { typeof(Period), PeriodMapper }
+    }.ToFrozenDictionary();
+
+    /// <summary>
+    /// Creates a primitive mapper for the specified type if supported; 
otherwise, returns null.
+    /// </summary>
+    /// <typeparam name="T">Type.</typeparam>
+    /// <returns>Mapper or null.</returns>
+    public static OneColumnMapper<T>? TryCreate<T>() => 
Mappers.GetValueOrDefault(typeof(T)) as OneColumnMapper<T>;
+
+    private static T UnwrapNullable<T>(T? value, IMapperColumn column)
+        where T : struct
+    {
+        if (value.HasValue)
+        {
+            return value.Value;
+        }
+
+        var message = $"Can't map '{typeof(T)}' to column '{column.Name}' - 
column is nullable, but field is not.";
+
+        throw new IgniteClientException(ErrorGroups.Client.Configuration, 
message);
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs 
b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
index de4aba7e9f6..6529cefcfd5 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Table.cs
@@ -22,6 +22,7 @@ namespace Apache.Ignite.Internal.Table
     using System.Collections.Generic;
     using System.Diagnostics;
     using System.Diagnostics.CodeAnalysis;
+    using System.Runtime.CompilerServices;
     using System.Threading;
     using System.Threading.Tasks;
     using Buffers;
@@ -34,6 +35,7 @@ namespace Apache.Ignite.Internal.Table
     using Proto;
     using Proto.MsgPack;
     using Serialization;
+    using Serialization.Mappers;
     using Sql;
     using Transactions;
 
@@ -148,7 +150,26 @@ namespace Apache.Ignite.Internal.Table
         /// <inheritdoc/>
         [RequiresUnreferencedCode(ReflectionUtils.TrimWarning)]
         public IRecordView<T> GetRecordView<T>()
-            where T : notnull => GetRecordViewInternal<T>();
+            where T : notnull
+        {
+            var simpleMapper = OneColumnMappers.TryCreate<T>();
+
+            if (!RuntimeFeature.IsDynamicCodeSupported)
+            {
+                if (simpleMapper == null)
+                {
+                    throw new InvalidOperationException(
+                        "Dynamic code generation is not supported in the 
current environment. " +
+                        "Provide an explicit IMapper<T> implementation for 
type " + typeof(T).FullName);
+                }
+
+                return GetRecordView(simpleMapper);
+            }
+
+            return simpleMapper is not null
+                ? GetRecordView(simpleMapper)
+                : GetRecordViewInternal<T>();
+        }
 
         /// <inheritdoc/>
         public IRecordView<T> GetRecordView<T>(IMapper<T> mapper)
@@ -158,8 +179,27 @@ namespace Apache.Ignite.Internal.Table
         /// <inheritdoc/>
         [RequiresUnreferencedCode(ReflectionUtils.TrimWarning)]
         public IKeyValueView<TK, TV> GetKeyValueView<TK, TV>()
-            where TK : notnull =>
-            new KeyValueView<TK, TV>(GetRecordViewInternal<KvPair<TK, TV>>());
+            where TK : notnull
+        {
+            var simpleMapper = KeyValueMappers.TryCreate<TK, TV>();
+
+            if (!RuntimeFeature.IsDynamicCodeSupported)
+            {
+                if (simpleMapper == null)
+                {
+                    throw new InvalidOperationException(
+                        "Dynamic code generation is not supported in the 
current environment. " +
+                        "Provide an explicit IMapper<KeyValuePair<TK, TV>> 
implementation for types " +
+                        typeof(TK).FullName + " and " + typeof(TV).FullName);
+                }
+
+                return GetKeyValueView(simpleMapper);
+            }
+
+            return simpleMapper is not null
+                ? GetKeyValueView(simpleMapper)
+                : new KeyValueView<TK, TV>(GetRecordViewInternal<KvPair<TK, 
TV>>());
+        }
 
         /// <inheritdoc/>
         public IKeyValueView<TK, TV> GetKeyValueView<TK, 
TV>(IMapper<KeyValuePair<TK, TV>> mapper)


Reply via email to