Skip to content

Instantly share code, notes, and snippets.

@JLChnToZ
Last active May 28, 2023 08:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JLChnToZ/f97fa95fa3bbce70a789afa897858337 to your computer and use it in GitHub Desktop.
Save JLChnToZ/f97fa95fa3bbce70a789afa897858337 to your computer and use it in GitHub Desktop.
The dynamic access pass to any methods, properties and fields of any types and instances in C#.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace JLChnToZ.CommonUtils.Dynamic {
using static LimitlessUtilities;
/// <summary>
/// The dynamic access pass to any methods, properties and fields of any types and instances, whatever it is public or private, static or non-static.
/// </summary>
/// <remarks>
/// You can use the static methods from this class as a starting point.
/// To initiate the dynamic instance with specified types, you can use <see cref="Type"/> object, type name strings or other wrapped dynamic objects.
/// </remarks>
/// <example><code><![CDATA[
/// // To construct an instance and call a method
/// var newString = Limitless.Construct(typeof(StringBuilder), "Hello, ").Append("World!").ToString();
/// // To call a static generic method, you may use .of(...) to specify generic type arguments.
/// Limitless.Static("UnityEngine.Object, UnityEngine").FindObjectOfType<RigidBody>();
/// Limitless.Static("UnityEngine.Object, UnityEngine").FindObjectOfType.of(typeof(Rigidbody))();
/// // To wrap an existing object.
/// var obj = new MyObject();
/// var objWrapped = Limitless.Wrap(obj);
/// // To re-wrap an object narrowed to a type, useful for accessing hidden interface members
/// var narrowedType = Lmitless.Wrap(obj, typeof(IMyInterface));
/// var narrowedType2 = Lmitless.Wrap(objWrapped, typeof(IMyInterface));
/// // To wrap a method to a delegate
/// Func<double, double> absWrapped = Limitless.Static(typeof(Math)).Abs;
/// ]]></code></example>
public class Limitless : DynamicObject, IEnumerable {
protected readonly internal object target;
protected readonly internal Type type;
protected readonly internal TypeInfo typeInfo;
/// <summary>Get access to all static members of a type.</summary>
/// <param name="type">The type to access.</param>
/// <returns>The dynamic object with access to all static members of the type.</returns>
public static dynamic Static(string typeName) => Static(Type.GetType(typeName, true));
/// <summary>Get access to all static members of a type.</summary>
/// <param name="type">The type to access.</param>
/// <returns>The dynamic object with access to all static members of the type.</returns>
public static dynamic Static(Type type) {
if (type == null) throw new ArgumentNullException(nameof(type));
return new Limitless(null, type);
}
/// <summary>Get access to all static members of a type.</summary>
/// <param name="source">The dynamic object of the type to access.</param>
/// <returns>The dynamic object with access to all static members of the type.</returns>
public static dynamic Static(Limitless source) => Static(source.type);
/// <summary>Wrap an object to dynamic object.</summary>
/// <param name="obj">The object to wrap.</param>
/// <param name="type">Optional base/interface type to wrap.</param>
/// <returns>The dynamic object with access to all members of it.</returns>
public static dynamic Wrap(object obj, Type type = null) {
if (obj == null) return null;
if (obj is DynamicObject dynamicObj) return dynamicObj;
if (type == null) return InternalWrap(obj);
obj = InternalUnwrap(obj);
var objType = obj.GetType();
if (!type.IsAssignableFrom(objType) && !objType.IsAssignableFrom(type))
throw new ArgumentException("Type mismatch", nameof(type));
return new Limitless(obj, type);
}
/// <summary>Wrap an object to dynamic object.</summary>
/// <param name="obj">The object to wrap.</param>
/// <param name="typeName">The name of base/interface type to wrap.</param>
/// <returns>The dynamic object with access to all members of it.</returns>
public static dynamic Wrap(object obj, string typeName) => Wrap(obj, Type.GetType(typeName, true));
/// <summary>Construct an object with specified type and arguments.</summary>
/// <param name="type">The type of object to construct.</param>
/// <param name="args">The arguments to construct the object.</param>
/// <returns>The dynamic instance with access to all members of it.</returns>
public static dynamic Construct(Type type, params object[] args) {
if (type == null) throw new ArgumentNullException(nameof(type));
if (TypeInfo.Get(type).TryConstruct(args, out var result)) return result;
throw new MissingMethodException("No constructor matched");
}
/// <summary>Construct an object with specified type and arguments.</summary>
/// <param name="typeName">The name of type of object to construct.</param>
/// <param name="args">The arguments to construct the object.</param>
/// <returns>The dynamic instance with access to all members of it.</returns>
public static dynamic Construct(string typeName, params object[] args) => Construct(Type.GetType(typeName, true), args);
/// <summary>Construct an object with specified type and arguments.</summary>
/// <param name="source">The dynamic object of the type to construct.</param>
/// <param name="args">The arguments to construct the object.</param>
/// <returns>The dynamic instance with access to all members of it.</returns>
public static dynamic Construct(Limitless source, params object[] args) => Construct(source.type, args);
protected internal Limitless(object target = null, Type type = null) {
this.target = target;
this.type = type ?? target?.GetType() ?? typeof(object);
typeInfo = TypeInfo.Get(this.type);
}
public override bool TryGetMember(GetMemberBinder binder, out object result) {
if (typeInfo.TryGetValue(target, binder.Name, out result))
return true;
if (typeInfo.TryGetMethods(binder.Name, out var methods)) {
result = new LimitlessInvokable(target, methods);
return true;
}
if (typeInfo.TryGetSubType(binder.Name, out var subType)) {
result = Static(subType);
return true;
}
return typeInfo.TryGetValue(target, new [] { binder.Name }, out result);
}
public override bool TrySetMember(SetMemberBinder binder, object value) =>
typeInfo.TrySetValue(target, binder.Name, value) ||
typeInfo.TrySetValue(target, new [] { binder.Name }, value);
public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) =>
typeInfo.TryGetValue(target, indexes, out result);
public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value) =>
typeInfo.TrySetValue(target, indexes, value);
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) =>
typeInfo.TryInvoke(target, binder.Name, args, out result, binder.GetGenericTypeArguments());
public override bool TryInvoke(InvokeBinder binder, object[] args, out object result) {
if (target is Delegate del) {
InternalUnwrap(args);
result = InternalWrap(del.DynamicInvoke(args));
InternalWrap(args, args);
return true;
}
result = null;
return false;
}
public override bool TryBinaryOperation(BinaryOperationBinder binder, object arg, out object result) =>
typeInfo.TryInvoke(target, $"op_{binder.Operation}", new[] { arg }, out result);
public override bool TryUnaryOperation(UnaryOperationBinder binder, out object result) =>
typeInfo.TryInvoke(target, $"op_{binder.Operation}", null, out result);
public override bool TryConvert(ConvertBinder binder, out object result) {
if (typeInfo.TryCast(target, binder.Type, out result))
return true;
try {
if (target is IConvertible convertible) {
result = convertible.ToType(binder.Type, null);
return true;
}
} catch (Exception) { }
result = null;
return false;
}
// Supports await ... syntax
public virtual LimitlessAwaiter GetAwaiter() =>
new LimitlessAwaiter(typeInfo.TryInvoke(target, nameof(GetAwaiter), null, out var awaiter) ? InternalUnwrap(awaiter) : target);
// Supports foreach (... in ...) { ... } syntax
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
protected virtual IEnumerator GetEnumerator() {
if (target is IEnumerable enumerable) return new LimitlessEnumerator(enumerable.GetEnumerator());
throw new InvalidOperationException("The object is not enumerable.");
}
public new Type GetType() => target?.GetType() ?? type;
public override string ToString() => target?.ToString() ?? type.ToString();
public override int GetHashCode() => target?.GetHashCode() ?? 0;
}
/// <summary>Special dynamic object that can be used to enumerate.</summary>
/// <remarks>This should not be directly used. Use <see cref="Limitless"/> instead.</remarks>
public class LimitlessEnumerator : Limitless, IEnumerator {
internal LimitlessEnumerator(IEnumerator target) : base(target, null) { }
public dynamic Current => InternalWrap(((IEnumerator)target).Current);
public bool MoveNext() => ((IEnumerator)target).MoveNext();
public void Reset() => ((IEnumerator)target).Reset();
protected override IEnumerator GetEnumerator() => this;
}
/// <summary>Special dynamic object that can be used to await for async methods.</summary>
/// <remarks>This should not be directly used. Use <see cref="Limitless"/> instead.</remarks>
public class LimitlessAwaiter : Limitless, INotifyCompletion {
internal LimitlessAwaiter(object target) : base(target, null) { }
void INotifyCompletion.OnCompleted(Action continuation) {
if (target is INotifyCompletion notifyCompletion)
notifyCompletion.OnCompleted(continuation);
else
continuation();
}
public dynamic GetResult() {
if (target is INotifyCompletion && typeInfo.TryInvoke(target, nameof(GetResult), emptyArgs, out var result))
return result;
return InternalWrap(target);
}
public override LimitlessAwaiter GetAwaiter() => this;
}
/// <summary>Special dynamic object that can be used to invoke methods with same name but different signatures.</summary>
/// <remarks>This should not be directly used. Use <see cref="Limitless"/> instead.</remarks>
public class LimitlessInvokable : DynamicObject {
readonly MethodInfo[] methodInfos;
readonly object target;
internal LimitlessInvokable(object target, MethodInfo[] methodInfos) {
if (methodInfos == null || methodInfos.Length == 0)
throw new ArgumentNullException(nameof(methodInfos));
this.target = target;
this.methodInfos = methodInfos;
}
public override bool TryInvoke(InvokeBinder binder, object[] args, out object result) =>
TryInvoke(args, out result, binder.GetGenericTypeArguments());
bool TryInvoke(object[] args, out object result, IList<Type> genericTypes = null) {
var safeArgs = args;
if (TryGetMatchingMethod(methodInfos, ref safeArgs, out var methodInfo, genericTypes)) {
result = InternalWrap(methodInfo.Invoke(target, safeArgs));
InternalWrap(safeArgs, args);
return true;
}
result = null;
return false;
}
/// <summary>Invokes the method with the given arguments.</summary>
public dynamic Invoke(params object[] args) {
if (TryInvoke(args, out var result)) return result;
throw new InvalidOperationException("No matching method found.");
}
public override bool TryConvert(ConvertBinder binder, out object result) =>
TryCreateDelegate(binder.Type, out result);
/// <summary>Creates a delegate with the given type.</summary>
public dynamic CreateDelegate(Type delegateType) {
if (TryCreateDelegate(delegateType, out var result)) return result;
throw new InvalidOperationException("No matching method found.");
}
bool TryCreateDelegate(Type delegateType, out object result) {
if (delegateType.IsSubclassOf(typeof(Delegate))) {
var invokeMethod = delegateType.GetMethod("Invoke");
if (invokeMethod != null) {
var expectedParameters = invokeMethod.GetParameters();
var expectedReturnType = invokeMethod.ReturnType;
foreach (var methodInfo in methodInfos) {
if (methodInfo.ContainsGenericParameters ||
methodInfo.ReturnType != expectedReturnType) continue;
var parameters = methodInfo.GetParameters();
if (parameters.Length != expectedParameters.Length) continue;
for (int i = 0; i < parameters.Length; i++)
if (parameters[i].ParameterType != expectedParameters[i].ParameterType)
goto NoMatches;
result = InternalWrap(target == null ?
Delegate.CreateDelegate(delegateType, methodInfo, false) :
Delegate.CreateDelegate(delegateType, target, methodInfo, false));
if (result != null) return true;
NoMatches:;
}
}
}
result = null;
return false;
}
/// <summary>Fulfills generic parameters of the methods.</summary>
public dynamic Of(params Type[] types) {
if (types == null) throw new ArgumentNullException(nameof(types));
var filteredMethods = new List<MethodInfo>();
foreach (var methodInfo in methodInfos) {
var genericParams = methodInfo.GetGenericArguments();
if (genericParams.Length != types.Length) continue;
try {
filteredMethods.Add(types.Length > 0 ? methodInfo.MakeGenericMethod(types) : methodInfo);
} catch {} // Try next if it fails
}
return new LimitlessInvokable(target, filteredMethods.ToArray());
}
public dynamic of(params object[] types) {
if (types == null) throw new ArgumentNullException(nameof(types));
var convertedTypes = new Type[types.Length];
for (int i = 0; i < types.Length; i++) {
var typeLike = types[i];
if (typeLike is Limitless limitless) {
if (limitless.target is Type type) {
convertedTypes[i] = type;
continue;
}
if (limitless.target == null) {
convertedTypes[i] = limitless.type;
continue;
}
}
convertedTypes[i] = (Type)typeLike;
}
return Of(convertedTypes);
}
}
internal static class LimitlessUtilities {
public const BindingFlags BASE_FLAGS = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;
public const BindingFlags STATIC_FLAGS = BASE_FLAGS | BindingFlags.Static;
public const BindingFlags INSTANCE_FLAGS = BASE_FLAGS | BindingFlags.Instance;
public const BindingFlags DEFAULT_FLAGS = STATIC_FLAGS | INSTANCE_FLAGS;
public static object InternalUnwrap(object obj) => obj is Limitless limitObj ? limitObj.target : obj;
public static readonly object[] emptyArgs = new object[0];
static bool isUsingCSharpBinder = true;
static Type invokeBinderType;
static PropertyInfo getTypeArgsProperty;
public static void InternalUnwrap(object[] objs) {
for (int i = 0; i < objs.Length; i++) objs[i] = InternalUnwrap(objs[i]);
}
public static MethodMatchLevel UnwrapParamsAndCheck(ParameterInfo[] paramInfos, ref object[] input, bool shouldCloneArray = false) {
if (input == null) input = emptyArgs;
if (paramInfos.Length > input.Length) {
var newInput = new object[paramInfos.Length];
Array.Copy(input, newInput, input.Length);
input = newInput;
shouldCloneArray = true;
}
var currentMatchLevel = MethodMatchLevel.Exact;
for (int i = 0; i < paramInfos.Length; i++) {
var inputObj = input[i];
var matchLevel = UnwrapParamAndCheck(paramInfos[i].ParameterType, ref inputObj);
if (matchLevel == MethodMatchLevel.NotMatch) return MethodMatchLevel.NotMatch;
input[i] = inputObj;
if (matchLevel < currentMatchLevel) currentMatchLevel = matchLevel;
}
if (currentMatchLevel != MethodMatchLevel.Exact && !shouldCloneArray) {
var newInput = new object[paramInfos.Length];
Array.Copy(input, newInput, input.Length);
input = newInput;
}
return currentMatchLevel;
}
public static MethodMatchLevel UnwrapParamAndCheck(Type type, ref object input) {
if (input == null) return type.IsValueType ? MethodMatchLevel.NotMatch : MethodMatchLevel.Implicit;
if (input is Limitless limitObj) input = limitObj.target;
var inputType = input.GetType();
return type == inputType ? MethodMatchLevel.Exact :
type.IsAssignableFrom(input.GetType()) ? MethodMatchLevel.Implicit :
MethodMatchLevel.NotMatch;
}
public static object InternalWrap(object obj) =>
obj == null || obj is DynamicObject || Type.GetTypeCode(obj.GetType()) != TypeCode.Object ? obj : new Limitless(obj);
public static void InternalWrap(object[] sourceObj, object[] destObj) {
if (sourceObj == null || destObj == null) return;
if (sourceObj != destObj) Array.Copy(sourceObj, 0, destObj, 0, Math.Min(sourceObj.Length, destObj.Length));
for (int i = 0; i < destObj.Length; i++) destObj[i] = InternalWrap(destObj[i]);
}
public static bool TryGetMatchingMethod<T>(T[] methodInfos, ref object[] args, out T bestMatches, IList<Type> genericTypes = null) where T : MethodBase {
(T method, object[] safeArgs)? fallback = null;
foreach (var method in methodInfos) {
var m = method;
var safeArgs = args;
var genericArgs = method.GetGenericArguments();
if (genericArgs.Length > 0) {
if (genericTypes == null || method.MemberType != MemberTypes.Method || genericArgs.Length != genericTypes.Count) continue;
try {
var typeArgsArray = new Type[genericTypes.Count];
genericTypes.CopyTo(typeArgsArray, 0);
m = (method as MethodInfo).MakeGenericMethod(typeArgsArray) as T;
} catch {
continue;
}
} else if (genericTypes != null && genericTypes.Count > 0) continue;
switch (UnwrapParamsAndCheck(m.GetParameters(), ref safeArgs)) {
case MethodMatchLevel.Exact:
fallback = (m, safeArgs);
goto skip;
case MethodMatchLevel.Implicit:
if (fallback == null) fallback = (m, safeArgs);
break;
}
}
skip: if (fallback.HasValue) {
bestMatches = fallback.Value.method;
args = fallback.Value.safeArgs;
return true;
}
bestMatches = null;
return false;
}
// https://stackoverflow.com/a/5493142, We can get generic type arguments, but needs some reflection.
public static IList<Type> GetGenericTypeArguments(this DynamicMetaObjectBinder binder) {
try {
if (!isUsingCSharpBinder || binder == null) return null;
// ICSharpInvokeOrInvokeMemberBinder is internal
// We can eat our own dog food here but it would potentially cause recursion.
if (invokeBinderType == null) {
invokeBinderType = Type.GetType("Microsoft.CSharp.RuntimeBinder.ICSharpInvokeOrInvokeMemberBinder", false);
if (invokeBinderType == null) {
isUsingCSharpBinder = false;
return null; // not using C# binder
}
}
if (!binder.GetType().IsAssignableFrom(invokeBinderType)) return null;
if (getTypeArgsProperty == null) getTypeArgsProperty = invokeBinderType.GetProperty("TypeArguments");
return getTypeArgsProperty.GetValue(binder) as IList<Type>;
} catch {
return null;
}
}
}
/// <summary>Internal struct that holds type members information.</summary>
public readonly struct TypeInfo {
static readonly Dictionary<Type, TypeInfo> cache = new Dictionary<Type, TypeInfo>();
readonly ConstructorInfo[] constructors;
readonly PropertyInfo[] indexers;
readonly Dictionary<string, MethodInfo[]> methods;
readonly Dictionary<string, PropertyInfo> properties;
readonly Dictionary<string, FieldInfo> fields;
readonly Dictionary<Type, MethodInfo> castOperators;
readonly Dictionary<string, Type> subTypes;
public static TypeInfo Get(Type type) {
if (!cache.TryGetValue(type, out var typeInfo))
cache[type] = typeInfo = new TypeInfo(type);
return typeInfo;
}
TypeInfo(Type type) {
if (type == null) throw new ArgumentNullException(nameof(type));
methods = new Dictionary<string, MethodInfo[]>();
properties = new Dictionary<string, PropertyInfo>();
fields = new Dictionary<string, FieldInfo>();
castOperators = new Dictionary<Type, MethodInfo>();
subTypes = new Dictionary<string, Type>();
var tempMethods = new Dictionary<string, List<MethodInfo>>();
foreach (var m in type.GetMethods(DEFAULT_FLAGS)) {
var methodName = m.Name;
if (m.ContainsGenericParameters) {
int methodCountIndex = methodName.LastIndexOf('`');
if (methodCountIndex >= 0) methodName = methodName.Substring(0, methodCountIndex);
}
if (!tempMethods.TryGetValue(methodName, out var list))
tempMethods[methodName] = list = new List<MethodInfo>();
list.Add(m);
switch (methodName) {
case "op_Implicit": case "op_Explicit": {
var parameters = m.GetParameters();
var returnType = m.ReturnType;
if (parameters.Length == 1 && returnType != type && returnType != typeof(void))
castOperators[parameters[0].ParameterType] = m;
break;
}
}
}
foreach (var kv in tempMethods) methods[kv.Key] = kv.Value.ToArray();
var tempIndexers = new List<PropertyInfo>();
foreach (var p in type.GetProperties(DEFAULT_FLAGS)) {
properties[p.Name] = p;
if (p.GetIndexParameters().Length > 0) tempIndexers.Add(p);
}
indexers = tempIndexers.ToArray();
foreach (var f in type.GetFields(DEFAULT_FLAGS)) fields[f.Name] = f;
constructors = type.GetConstructors(INSTANCE_FLAGS);
foreach (var t in type.GetNestedTypes(DEFAULT_FLAGS)) subTypes[t.Name] = t;
}
internal bool TryGetValue(object instance, string key, out object value) {
if (properties.TryGetValue(key, out var property)) {
value = InternalWrap(property.GetValue(instance));
return true;
}
if (fields.TryGetValue(key, out var field)) {
value = InternalWrap(field.GetValue(instance));
return true;
}
value = null;
return false;
}
internal bool TryGetValue(object instance, object[] indexes, out object value) {
(PropertyInfo indexer, object[] safeIndexes)? fallback = null;
foreach (var indexer in indexers) {
var safeIndexes = indexes;
switch (UnwrapParamsAndCheck(indexer.GetIndexParameters(), ref safeIndexes)) {
case MethodMatchLevel.Exact:
fallback = (indexer, safeIndexes);
goto skip;
case MethodMatchLevel.Implicit:
if (fallback == null) fallback = (indexer, safeIndexes);
break;
}
}
skip: if (fallback.HasValue) {
value = InternalWrap(fallback.Value.indexer.GetValue(instance, fallback.Value.safeIndexes));
return true;
}
value = null;
return false;
}
internal bool TrySetValue(object instance, string key, object value) {
if (properties.TryGetValue(key, out var property)) {
property.SetValue(InternalUnwrap(instance), value);
return true;
}
if (fields.TryGetValue(key, out var field)) {
field.SetValue(InternalUnwrap(instance), value);
return true;
}
return false;
}
internal bool TrySetValue(object instance, object[] indexes, object value) {
(PropertyInfo indexer, object[] safeIndexes)? fallback = null;
foreach (var indexer in indexers) {
var safeIndexes = indexes;
switch (UnwrapParamsAndCheck(indexer.GetIndexParameters(), ref safeIndexes)) {
case MethodMatchLevel.Exact:
fallback = (indexer, safeIndexes);
goto skip;
case MethodMatchLevel.Implicit:
if (fallback == null) fallback = (indexer, safeIndexes);
break;
}
}
skip: if (fallback.HasValue) {
fallback.Value.indexer.SetValue(InternalUnwrap(instance), value, fallback.Value.safeIndexes);
return true;
}
return false;
}
internal bool TryGetMethods(string methodName, out MethodInfo[] method) => methods.TryGetValue(methodName, out method);
internal bool TryInvoke(object instance, string methodName, object[] args, out object result, IList<Type> genericTypes = null) {
var safeArgs = args;
if (methods.TryGetValue(methodName, out var methodArrays) &&
TryGetMatchingMethod(methodArrays, ref safeArgs, out var resultMethod, genericTypes)) {
result = InternalWrap(resultMethod.Invoke(InternalUnwrap(instance), safeArgs));
InternalWrap(safeArgs, args);
return true;
}
result = null;
return false;
}
internal bool TryCast(object instance, Type type, out object result) {
// Note: Return results after casting must not be wrapped.
instance = InternalUnwrap(instance);
if (instance == null) {
result = null;
return !type.IsValueType; // Only reference types can be null
}
if (type.IsAssignableFrom(instance.GetType())) { // No need to cast if the type is already assignable
result = instance;
return true;
}
if (castOperators.TryGetValue(type, out var method)) {
result = method.Invoke(instance, emptyArgs);
return true;
}
result = null;
return false;
}
internal bool TryConstruct(object[] args, out object result) {
if (TryGetMatchingMethod(constructors, ref args, out var resultConstructor)) {
result = InternalWrap(resultConstructor.Invoke(args));
return true;
}
result = null;
return false;
}
internal bool TryGetSubType(string name, out Type type) => subTypes.TryGetValue(name, out type);
}
internal enum MethodMatchLevel {
NotMatch,
Implicit,
Exact,
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment