Skip to content

Instantly share code, notes, and snippets.

@evan-choi
Last active August 5, 2021 00:44
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save evan-choi/e69846e9c797114684e6daa9253ed466 to your computer and use it in GitHub Desktop.
Save evan-choi/e69846e9c797114684e6daa9253ed466 to your computer and use it in GitHub Desktop.
ObjectSelector is a simple class written for navigating nested object properties.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
/*
* Syntax:
* MemberAccessor | IndexAccessor ('.' MemberAccessor | IndexAccessor)*
*
* MemberAccessor:
* Field | Property
*
* IndexAccessor:
* '[' (System.Int32 | System.Index | System.Range) ']'
*/
/*
* Example)
*
* Property
* Property.NestedProperty
* Property[5].Field
* Property[^1].Field
* Property[1..^1].Field
*/
public sealed class ObjectSelector
{
private delegate object Selector(object obj);
public string Path { get; }
private MethodInfo ArrayGetLength => _arrayGetLength ??= typeof(Array).GetMethod(nameof(Array.GetLength));
private MethodInfo IndexGetOffset => _indexGetOffset ??= typeof(Index).GetMethod(nameof(Index.GetOffset));
private MethodInfo RuntimeHelpersGetSubArray => _runtimeHelpersGetSubArray ??= typeof(RuntimeHelpers).GetMethod(nameof(RuntimeHelpers.GetSubArray));
private MethodInfo _arrayGetLength;
private MethodInfo _indexGetOffset;
private MethodInfo _runtimeHelpersGetSubArray;
private readonly IList<IAccessor> _accessors;
private readonly ConcurrentDictionary<Type, SelectorInfo> _cache = new();
public ObjectSelector(string path)
{
Path = path;
_accessors = Parse(path);
}
public object Select(object obj)
{
var type = obj.GetType();
if (!_cache.TryGetValue(type, out var info))
{
info = Compile(type);
_cache[type] = info;
}
return info.Selector(obj);
}
public bool TryGetReturnType(Type type, out Type returnType)
{
try
{
if (!_cache.TryGetValue(type, out var info))
{
info = Compile(type);
_cache[type] = info;
}
returnType = info.Type;
return true;
}
catch
{
returnType = default;
return false;
}
}
private SelectorInfo Compile(Type type)
{
var paramExpr = Expression.Parameter(typeof(object));
Expression bodyExpr = Expression.TypeAs(paramExpr, type);
foreach (var accessor in _accessors)
{
Expression accessorExpr;
switch (accessor)
{
case MemberAccessor memberAccessor:
accessorExpr = CreateMemberExpression(bodyExpr, memberAccessor);
break;
case IndexAccessor indexAccessor:
accessorExpr = CreateIndexerExpression(bodyExpr, indexAccessor);
break;
default:
throw new NotSupportedException(accessor.GetType().Name);
}
bodyExpr = accessorExpr;
}
var returnType = bodyExpr.Type;
if (bodyExpr.Type != typeof(object))
bodyExpr = Expression.Convert(bodyExpr, typeof(object));
Expression<Selector> lambdaExpr = Expression.Lambda<Selector>(bodyExpr, paramExpr);
return new SelectorInfo(lambdaExpr.Compile(), returnType);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Expression CreateMemberExpression(Expression target, MemberAccessor accessor)
{
return Expression.PropertyOrField(target, accessor.Name);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Expression CreateIndexerExpression(Expression target, IndexAccessor accessor)
{
if (target.Type.IsArray)
return CreateArrayIndexerExpression(target, accessor);
return CreateItemIndexerExpression(target, accessor);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Expression CreateArrayIndexerExpression(Expression target, IndexAccessor accessor)
{
if (accessor.IndexParameters.Length == 1 && accessor.IndexParameters[0] is Range range)
{
if (!target.Type.IsSZArray)
throw ThrowCanNotResolveIndexer(target.Type, accessor);
var getSubArray = RuntimeHelpersGetSubArray.MakeGenericMethod(target.Type.GetElementType()!);
return Expression.Call(getSubArray, target, Expression.Constant(range));
}
return Expression.ArrayAccess(target, accessor.IndexParameters.Select(CreateParamExpressions));
Expression CreateParamExpressions(object param, int dimension)
{
switch (param)
{
case Index index:
{
if (index.IsFromEnd)
{
return Expression.Call(
Expression.Constant(index),
IndexGetOffset,
Expression.Call(target, ArrayGetLength, Expression.Constant(dimension))
);
}
return Expression.Constant(index.Value);
}
case int element:
return Expression.Constant(element);
default:
throw ThrowCanNotResolveIndexer(target.Type, accessor);
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Expression CreateItemIndexerExpression(Expression target, IndexAccessor accessor)
{
IEnumerable<PropertyInfo> itemMethods = target.Type.GetProperties()
.Where(p => p.Name == "Item");
foreach (var itemProperty in itemMethods)
{
ParameterInfo[] paramInfos = itemProperty.GetIndexParameters();
if (paramInfos.Length != accessor.IndexParameters.Length)
continue;
var paramExprs = new Expression[paramInfos.Length];
var matched = true;
for (int i = 0; i < paramExprs.Length; i++)
{
var param = accessor.IndexParameters[i];
var paramType = paramInfos[i].ParameterType;
ref var paramExpr = ref paramExprs[i];
if (paramType == typeof(Index))
{
switch (param)
{
case int element:
paramExpr = Expression.Constant(new Index(element));
break;
case Index index:
paramExpr = Expression.Constant(index);
break;
}
}
else if (paramType == typeof(int))
{
switch (param)
{
case int element:
paramExpr = Expression.Constant(element);
break;
case Index index:
if (!index.IsFromEnd)
{
paramExpr = Expression.Constant(index.Value);
}
else if (TryGetSizeProperty(target.Type, out var sizePropertyInfo))
{
paramExpr = Expression.Call(
Expression.Constant(index),
IndexGetOffset,
Expression.Property(target, sizePropertyInfo)
);
}
break;
}
}
else if (paramType == typeof(Range) && param is Range range)
{
paramExpr = Expression.Constant(range);
}
if (paramExpr == null)
{
matched = false;
break;
}
}
if (matched)
return Expression.Property(target, itemProperty, paramExprs);
}
throw ThrowCanNotResolveIndexer(target.Type, accessor);
}
private Exception ThrowCanNotResolveIndexer(Type type, IndexAccessor accessor)
{
IEnumerable<string> paramNames = accessor.IndexParameters
.Select(x => x.GetType().FullName);
throw new Exception($"Can not resolve indexer '{type.FullName}[{string.Join(", ", paramNames)}]'");
}
private bool TryGetSizeProperty(Type type, out PropertyInfo sizePropertyInfo)
{
sizePropertyInfo =
type.GetProperty("Length", typeof(int)) ??
type.GetProperty("Count", typeof(int));
return sizePropertyInfo != null;
}
private static IList<IAccessor> Parse(ReadOnlySpan<char> path)
{
var accessors = new List<IAccessor>();
while ((path = path.TrimStart()).Length > 0)
{
var indexer = path[0] == '[';
if (accessors.Count == 0)
{
// '.Prop'
// ^
if (path[0] == '.')
throw new FormatException(path.ToString());
}
else
{
// '[0]Prop'
// ^
if (!indexer && accessors.Count > 0)
{
if (path[0] != '.')
throw new FormatException(path.ToString());
path = path[1..];
}
}
var next = indexer ? path.IndexOf(']') : path.IndexOfAny('.', '[');
ReadOnlySpan<char> token;
if (next == -1)
{
token = path;
path = ReadOnlySpan<char>.Empty;
}
else
{
var offset = indexer ? 1 : 0;
token = path[offset..next];
path = path[(next + offset)..];
}
token = token.Trim();
if (token.IsEmpty)
throw new FormatException(path.ToString());
if (indexer)
{
if (!TryParseIndexParameters(token, out object[] indexParameters))
throw new FormatException(token.ToString());
accessors.Add(new IndexAccessor(indexParameters));
}
else
{
accessors.Add(new MemberAccessor(token.ToString()));
}
}
return accessors;
}
private static bool TryParseIndexParameters(ReadOnlySpan<char> token, out object[] indexParameters)
{
var result = new List<object>();
indexParameters = default;
while (!token.IsEmpty)
{
var next = token.IndexOf(',');
ReadOnlySpan<char> value;
if (next == -1)
{
value = token.Trim();
token = ReadOnlySpan<char>.Empty;
}
else
{
value = token[..next].Trim();
token = token[(next + 1)..];
}
var rangeMark = value.IndexOf("..");
if (rangeMark == -1)
{
if (!TryParseIndex(value, out var index))
return false;
result.Add(index.IsFromEnd ? index : index.Value);
continue;
}
ReadOnlySpan<char> left = value[..rangeMark].Trim();
ReadOnlySpan<char> right = value[(rangeMark + 2)..].Trim();
Index leftIndex;
Index rightIndex;
if (left.IsEmpty)
{
leftIndex = Index.Start;
}
else if (!TryParseIndex(left, out leftIndex))
{
return false;
}
if (right.IsEmpty)
{
rightIndex = Index.End;
}
else if (!TryParseIndex(right, out rightIndex))
{
return false;
}
result.Add(new Range(leftIndex, rightIndex));
}
indexParameters = result.ToArray();
return true;
}
private static bool TryParseIndex(ReadOnlySpan<char> value, out Index index)
{
bool fromEnd = value[0] == '^';
if (fromEnd)
value = value[1..];
if (!int.TryParse(value, out var num))
{
index = default;
return false;
}
index = new Index(num, fromEnd);
return true;
}
private interface IAccessor
{
}
private readonly struct MemberAccessor : IAccessor
{
public string Name { get; }
public MemberAccessor(string name)
{
Name = name;
}
}
private readonly struct IndexAccessor : IAccessor
{
public object[] IndexParameters { get; }
public IndexAccessor(object[] indexParameters)
{
IndexParameters = indexParameters;
}
}
private readonly struct SelectorInfo
{
public Selector Selector { get; }
public Type Type { get; }
public SelectorInfo(Selector selector, Type type)
{
Selector = selector;
Type = type;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment