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)