You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by pt...@apache.org on 2022/10/31 12:35:17 UTC
[ignite-3] branch main updated: IGNITE-16356 .NET: Add mapped column name customization with ColumnAttribute (#1277)
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 14b5464ed8 IGNITE-16356 .NET: Add mapped column name customization with ColumnAttribute (#1277)
14b5464ed8 is described below
commit 14b5464ed85d8eb86c90641371569c8fbcdc919c
Author: Pavel Tupitsyn <pt...@apache.org>
AuthorDate: Mon Oct 31 15:35:12 2022 +0300
IGNITE-16356 .NET: Add mapped column name customization with ColumnAttribute (#1277)
Support custom column names with a standard `System.ComponentModel.DataAnnotations.Schema.ColumnAttribute` when mapping user-defined types to Ignite columns in `RecordView<T>` and `KeyValueView<T>`.
---
.../Table/RecordViewCustomMappingTest.cs | 147 +++++++++++++++++++++
.../Table/Serialization/ObjectSerializerHandler.cs | 14 +-
.../Table/Serialization/ReflectionUtils.cs | 64 ++++++++-
3 files changed, 211 insertions(+), 14 deletions(-)
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewCustomMappingTest.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewCustomMappingTest.cs
new file mode 100644
index 0000000000..4f15100ac5
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewCustomMappingTest.cs
@@ -0,0 +1,147 @@
+/*
+ * 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 NotAccessedPositionalProperty.Local
+// ReSharper disable UnusedMember.Local
+// ReSharper disable UnusedAutoPropertyAccessor.Local
+// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local
+namespace Apache.Ignite.Tests.Table;
+
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Threading.Tasks;
+using Ignite.Table;
+using NUnit.Framework;
+
+/// <summary>
+/// Tests custom user type mapping behavior in <see cref="IRecordView{T}"/>.
+/// </summary>
+public class RecordViewCustomMappingTest : IgniteTestsBase
+{
+ private const long Key = 1;
+
+ private const string Val = "val1";
+
+ [SetUp]
+ public async Task SetUp()
+ {
+ await Table.RecordBinaryView.UpsertAsync(null, GetTuple(Key, Val));
+ }
+
+ [Test]
+ public async Task TestFieldMapping()
+ {
+ var res = await Table.GetRecordView<FieldMapping>().GetAsync(null, new FieldMapping(Key));
+ Assert.AreEqual(Val, res.Value.Name);
+ }
+
+ [Test]
+ public async Task TestRecordPropertyMapping()
+ {
+ var res = await Table.GetRecordView<PropertyMapping>().GetAsync(null, new PropertyMapping(Key));
+ Assert.AreEqual(Val, res.Value.Name);
+ }
+
+ [Test]
+ public async Task TestClassPropertyMapping()
+ {
+ var res = await Table.GetRecordView<ClassPropertyMapping>().GetAsync(null, new ClassPropertyMapping { Id = Key });
+ Assert.AreEqual(Val, res.Value.Name);
+ }
+
+ [Test]
+ public async Task TestStructFieldMapping()
+ {
+ var res = await Table.GetRecordView<StructFieldMapping>().GetAsync(null, new StructFieldMapping(Key));
+ Assert.AreEqual(Val, res.Value.Name);
+ }
+
+ [Test]
+ public async Task TestStructPropertyMapping()
+ {
+ var res = await Table.GetRecordView<StructPropertyMapping>().GetAsync(null, new StructPropertyMapping(Key));
+ Assert.AreEqual(Val, res.Value.Name);
+ }
+
+ [Test]
+ public void TestComputedPropertyMappingThrowsException()
+ {
+ var ex = Assert.ThrowsAsync<IgniteClientException>(async () =>
+ await Table.GetRecordView<ComputedPropertyMapping>().GetAsync(null, new ComputedPropertyMapping { Id = Key }));
+
+ Assert.AreEqual(ErrorGroups.Client.Configuration, ex!.Code);
+
+ Assert.AreEqual(
+ "Can't map 'Apache.Ignite.Tests.Table.RecordViewCustomMappingTest+ComputedPropertyMapping' to columns" +
+ " 'Int64 KEY, String VAL'. Matching fields not found.",
+ ex.Message);
+ }
+
+ [Test]
+ public void TestDuplicateColumnNameMappingThrowsException()
+ {
+ var ex = Assert.ThrowsAsync<IgniteClientException>(async () =>
+ await Table.GetRecordView<FieldMappingDuplicate>().GetAsync(null, new FieldMappingDuplicate(Key)));
+
+ Assert.AreEqual(ErrorGroups.Client.Configuration, ex!.Code);
+
+ Assert.AreEqual(
+ "Column 'Val' maps to more than one field of type " +
+ "Apache.Ignite.Tests.Table.RecordViewCustomMappingTest+FieldMappingDuplicate: " +
+ "System.String <Name2>k__BackingField and " +
+ "System.String <Name>k__BackingField",
+ ex.Message);
+ }
+
+ private record struct StructFieldMapping([field: Column("Key")] long Id, [field: Column("Val")] string? Name = null);
+
+ private record struct StructPropertyMapping([property: Column("KEY")] long Id, [property: Column("VAL")] string? Name = null);
+
+ private record FieldMapping([field: Column("Key")] long Id, [field: Column("Val")] string? Name = null);
+
+ private record PropertyMapping([property: Column("Key")] long Id, [property: Column("Val")] string? Name = null);
+
+ private class ClassPropertyMapping
+ {
+ [Column("key")]
+ public long Id { get; set; }
+
+ [Column("VAL")]
+ public string Name { get; set; } = null!;
+ }
+
+ // ReSharper disable MemberHidesStaticFromOuterClass
+ private record ComputedPropertyMapping
+ {
+ public long Id { get; set; }
+
+ public string? Name { get; set; }
+
+ public long Key
+ {
+ get => Id;
+ set => Id = value;
+ }
+
+ public string? Val
+ {
+ get => Name;
+ set => Name = value;
+ }
+ }
+
+ private record FieldMappingDuplicate(long Key, [field: Column("Val")] string? Name = null, [field: Column("Val")] string? Name2 = null);
+}
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 8969bce8de..a0cfe9450d 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
@@ -143,7 +143,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
for (var index = 0; index < count; index++)
{
var col = columns[index];
- var fieldInfo = type.GetFieldIgnoreCase(col.Name);
+ var fieldInfo = type.GetFieldByColumnName(col.Name);
if (fieldInfo == null)
{
@@ -211,7 +211,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
}
else
{
- fieldInfo = (col.IsKey ? keyType : valType).GetFieldIgnoreCase(col.Name);
+ fieldInfo = (col.IsKey ? keyType : valType).GetFieldByColumnName(col.Name);
}
if (fieldInfo == null)
@@ -296,7 +296,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
for (var i = 0; i < count; i++)
{
var col = columns[i];
- var fieldInfo = type.GetFieldIgnoreCase(col.Name);
+ var fieldInfo = type.GetFieldByColumnName(col.Name);
EmitFieldRead(fieldInfo, il, col, i, local);
}
@@ -345,7 +345,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
local = col.IsKey ? keyLocal : valLocal;
fieldInfo = local == null
? null
- : (col.IsKey ? keyType : valType).GetFieldIgnoreCase(col.Name);
+ : (col.IsKey ? keyType : valType).GetFieldByColumnName(col.Name);
}
EmitFieldRead(fieldInfo, il, col, i, local);
@@ -403,7 +403,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
for (var i = 0; i < columns.Count; i++)
{
var col = columns[i];
- var fieldInfo = type.GetFieldIgnoreCase(col.Name);
+ var fieldInfo = type.GetFieldByColumnName(col.Name);
if (col.IsKey)
{
@@ -456,7 +456,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
for (var i = schema.KeyColumnCount; i < columns.Count; i++)
{
var col = columns[i];
- var fieldInfo = valType.GetFieldIgnoreCase(col.Name);
+ var fieldInfo = valType.GetFieldByColumnName(col.Name);
EmitFieldRead(fieldInfo, il, col, i - schema.KeyColumnCount, valLocal);
}
@@ -612,7 +612,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
var keyValTypes = type.GetGenericArguments();
- return (keyValTypes[0], keyValTypes[1], type.GetFieldIgnoreCase("Key")!, type.GetFieldIgnoreCase("Val")!);
+ return (keyValTypes[0], keyValTypes[1], type.GetFieldByColumnName("Key")!, type.GetFieldByColumnName("Val")!);
}
}
}
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ReflectionUtils.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ReflectionUtils.cs
index d976239fc1..9a10fbfa94 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ReflectionUtils.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ReflectionUtils.cs
@@ -20,9 +20,10 @@ namespace Apache.Ignite.Internal.Table.Serialization
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
- using System.Linq;
+ using System.ComponentModel.DataAnnotations.Schema;
using System.Linq.Expressions;
using System.Reflection;
+ using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
/// <summary>
@@ -41,7 +42,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
/// </summary>
public static readonly MethodInfo GetTypeFromHandleMethod = GetMethodInfo(() => Type.GetTypeFromHandle(default));
- private static readonly ConcurrentDictionary<Type, IReadOnlyDictionary<string, FieldInfo>> FieldsByNameCache = new();
+ private static readonly ConcurrentDictionary<Type, IReadOnlyDictionary<string, FieldInfo>> FieldsByColumnNameCache = new();
/// <summary>
/// Gets all fields from the type, including non-public and inherited.
@@ -50,6 +51,11 @@ namespace Apache.Ignite.Internal.Table.Serialization
/// <returns>Fields.</returns>
public static IEnumerable<FieldInfo> GetAllFields(this Type type)
{
+ if (type.IsPrimitive)
+ {
+ yield break;
+ }
+
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
@@ -67,20 +73,38 @@ namespace Apache.Ignite.Internal.Table.Serialization
}
/// <summary>
- /// Gets the field by name ignoring case.
+ /// Gets the field by column name. Ignores case, handles <see cref="ColumnAttribute"/>.
/// </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)
+ public static FieldInfo? GetFieldByColumnName(this Type type, string name)
{
// ReSharper disable once HeapView.CanAvoidClosure, ConvertClosureToMethodGroup
- return FieldsByNameCache.GetOrAdd(type, t => GetFieldsByName(t)).TryGetValue(name, out var fieldInfo)
+ return FieldsByColumnNameCache.GetOrAdd(type, t => GetFieldsByColumnName(t)).TryGetValue(name, out var fieldInfo)
? fieldInfo
: null;
- static IReadOnlyDictionary<string, FieldInfo> GetFieldsByName(Type type) =>
- type.GetAllFields().ToDictionary(f => f.GetCleanName(), StringComparer.OrdinalIgnoreCase);
+ static IReadOnlyDictionary<string, FieldInfo> GetFieldsByColumnName(Type type)
+ {
+ var res = new Dictionary<string, FieldInfo>(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var field in type.GetAllFields())
+ {
+ var columnName = field.GetColumnName();
+
+ if (res.TryGetValue(columnName, out var existingField))
+ {
+ throw new IgniteClientException(
+ ErrorGroups.Client.Configuration,
+ $"Column '{columnName}' maps to more than one field of type {type}: {field} and {existingField}");
+ }
+
+ res.Add(columnName, field);
+ }
+
+ return res;
+ }
}
/// <summary>
@@ -90,6 +114,32 @@ namespace Apache.Ignite.Internal.Table.Serialization
/// <returns>Clean name.</returns>
public static string GetCleanName(this MemberInfo memberInfo) => CleanFieldName(memberInfo.Name);
+ /// <summary>
+ /// Gets column name for the specified field: uses <see cref="ColumnAttribute"/> when available,
+ /// falls back to cleaned up field name otherwise.
+ /// </summary>
+ /// <param name="fieldInfo">Member.</param>
+ /// <returns>Clean name.</returns>
+ public static string GetColumnName(this FieldInfo fieldInfo)
+ {
+ if (fieldInfo.GetCustomAttribute<ColumnAttribute>() is { Name: { } columnAttributeName })
+ {
+ return columnAttributeName;
+ }
+
+ var cleanName = fieldInfo.GetCleanName();
+
+ if (fieldInfo.IsDefined(typeof(CompilerGeneratedAttribute), inherit: true) &&
+ fieldInfo.DeclaringType?.GetProperty(cleanName) is { } property &&
+ property.GetCustomAttribute<ColumnAttribute>() is { Name: { } columnAttributeName2 })
+ {
+ // This is a compiler-generated backing field for an automatic property - get the attribute from the property.
+ return columnAttributeName2;
+ }
+
+ return cleanName;
+ }
+
/// <summary>
/// Cleans the field name and removes compiler-generated prefixes and suffixes.
/// </summary>