blob: f31927bee4340758cd320b95c5d1ee59953b81e2 [file] [log] [blame]
/*
* 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.ComponentModel.DataAnnotations.Schema;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
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, ColumnInfo>> FieldsByColumnNameCache = new();
/// <summary>
/// Gets column names for all fields in the specified type.
/// </summary>
/// <param name="type">Type.</param>
/// <returns>Columns.</returns>
public static ICollection<ColumnInfo> GetColumns(this Type type) => (ICollection<ColumnInfo>)GetFieldsByColumnName(type).Values;
/// <summary>
/// Gets a pair of types for <see cref="KeyValuePair{TKey,TValue}"/>.
/// </summary>
/// <param name="type">Type.</param>
/// <returns>Resulting pair, or null when specified type is not <see cref="KeyValuePair{TKey,TValue}"/>.</returns>
public static (Type KeyType, Type ValType)? GetKeyValuePairTypes(this Type type)
{
if (!type.IsKeyValuePair())
{
return null;
}
var types = type.GetGenericArguments();
return (types[0], types[1]);
}
/// <summary>
/// Gets a value indicating whether the type is <see cref="KeyValuePair{TKey,TValue}"/>.
/// </summary>
/// <param name="type">Type.</param>
/// <returns>Whether the provided type is a <see cref="KeyValuePair{TKey,TValue}"/>.</returns>
public static bool IsKeyValuePair(this Type? type) =>
type is { IsGenericType: true } && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>);
/// <summary>
/// Gets the underlying enum type, if applicable. Otherwise, returns the type itself.
/// </summary>
/// <param name="type">Type to unwrap.</param>
/// <returns>Underlying type when enum; type itself otherwise.</returns>
public static Type UnwrapEnum(this Type type) => type.IsEnum ? Enum.GetUnderlyingType(type) : type;
/// <summary>
/// Gets a map of fields by column name. Ignores case, handles <see cref="ColumnAttribute"/> and <see cref="NotMappedAttribute"/>.
/// </summary>
/// <param name="type">Type to get the map for.</param>
/// <returns>Map.</returns>
public static IReadOnlyDictionary<string, ColumnInfo> GetFieldsByColumnName(this Type type)
{
// ReSharper disable once HeapView.CanAvoidClosure, HeapView.ClosureAllocation, HeapView.DelegateAllocation (false positive)
return FieldsByColumnNameCache.GetOrAdd(type, static t => RetrieveFieldsByColumnName(t));
static IReadOnlyDictionary<string, ColumnInfo> RetrieveFieldsByColumnName(Type type)
{
var res = new Dictionary<string, ColumnInfo>(StringComparer.OrdinalIgnoreCase);
foreach (var field in type.GetAllFields())
{
var columnInfo = field.GetColumnInfo();
if (columnInfo == null)
{
continue;
}
if (res.TryGetValue(columnInfo.Name, out var existingColInfo))
{
throw new ArgumentException(
$"Column '{columnInfo.Name}' maps to more than one field of type {type}: {field} and {existingColInfo.Field}");
}
res.Add(columnInfo.Name, columnInfo);
}
return res;
}
}
/// <summary>
/// Gets all fields from the type, including non-public and inherited.
/// </summary>
/// <param name="type">Type.</param>
/// <returns>Fields.</returns>
private static IEnumerable<FieldInfo> GetAllFields(this Type type)
{
if (type.IsPrimitive)
{
yield break;
}
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 cleaned up member name without compiler-generated prefixes and suffixes.
/// </summary>
/// <param name="memberInfo">Member.</param>
/// <returns>Clean name.</returns>
private 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>
private static ColumnInfo? GetColumnInfo(this FieldInfo fieldInfo)
{
if (fieldInfo.GetCustomAttribute<NotMappedAttribute>() != null)
{
return null;
}
if (fieldInfo.GetCustomAttribute<ColumnAttribute>() is { Name: { } columnAttributeName })
{
return new(columnAttributeName, fieldInfo, null, HasColumnNameAttribute: true);
}
var cleanName = fieldInfo.GetCleanName();
if (fieldInfo.IsDefined(typeof(CompilerGeneratedAttribute), inherit: true) &&
fieldInfo.DeclaringType?.GetProperty(cleanName) is { } property)
{
if (property.GetCustomAttribute<NotMappedAttribute>() != null)
{
return null;
}
if (property.GetCustomAttribute<ColumnAttribute>() is { Name: { } columnAttributeName2 })
{
// This is a compiler-generated backing field for an automatic property - get the attribute from the property.
return new(columnAttributeName2, fieldInfo, property, HasColumnNameAttribute: true);
}
return new(cleanName, fieldInfo, property, HasColumnNameAttribute: false);
}
return new(cleanName, fieldInfo, null, HasColumnNameAttribute: false);
}
/// <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>
private 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>
private static MethodInfo GetMethodInfo<T>(Expression<Func<T>> expression) => ((MethodCallExpression)expression.Body).Method;
/// <summary>
/// Column info.
/// </summary>
/// <param name="Name">Column name.</param>
/// <param name="Field">Corresponding field.</param>
/// <param name="Property">Corresponding property (when <see cref="Field"/> is a backing field of a auto property).</param>
/// <param name="HasColumnNameAttribute">Whether corresponding field or property has <see cref="ColumnAttribute"/>
/// with <see cref="ColumnAttribute.Name"/> set.</param>
internal record ColumnInfo(string Name, FieldInfo Field, PropertyInfo? Property, bool HasColumnNameAttribute);
}
}