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>