Last active
August 5, 2021 00:44
-
-
Save evan-choi/e69846e9c797114684e6daa9253ed466 to your computer and use it in GitHub Desktop.
ObjectSelector is a simple class written for navigating nested object properties.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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