DynamicObjectBase.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.Dynamic;
  5. using System.Linq;
  6. using System.Reflection;
  7. namespace ShwasherSys.ReflectionMagic
  8. {
  9. public abstract class DynamicObjectBase : DynamicObject
  10. {
  11. #if NET45
  12. private static readonly Type[] _emptyTypes = new Type[0];
  13. #endif
  14. // We need to virtualize this so we use a different cache for instance and static props
  15. protected abstract IDictionary<Type, IDictionary<string, IProperty>> PropertiesOnType { get; }
  16. protected abstract Type TargetType { get; }
  17. protected abstract object Instance { get; }
  18. protected abstract BindingFlags BindingFlags { get; }
  19. public abstract object RealObject { get; }
  20. public override bool TryGetMember(GetMemberBinder binder, out object result)
  21. {
  22. if (binder == null)
  23. throw new ArgumentNullException(nameof(binder));
  24. IProperty prop = GetProperty(binder.Name);
  25. // Get the property value
  26. result = prop.GetValue(Instance, index: null);
  27. // Wrap the sub object if necessary. This allows nested anonymous objects to work.
  28. result = result.AsDynamic();
  29. return true;
  30. }
  31. public override bool TrySetMember(SetMemberBinder binder, object value)
  32. {
  33. if (binder == null)
  34. throw new ArgumentNullException(nameof(binder));
  35. IProperty prop = GetProperty(binder.Name);
  36. // Set the property value. Make sure to unwrap it first if it's one of our dynamic objects
  37. prop.SetValue(Instance, Unwrap(value), index: null);
  38. return true;
  39. }
  40. public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
  41. {
  42. if (binder == null)
  43. throw new ArgumentNullException(nameof(binder));
  44. IProperty prop = GetIndexProperty();
  45. result = prop.GetValue(Instance, indexes);
  46. // Wrap the sub object if necessary. This allows nested anonymous objects to work.
  47. result = result.AsDynamic();
  48. return true;
  49. }
  50. public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
  51. {
  52. if (binder == null)
  53. throw new ArgumentNullException(nameof(binder));
  54. IProperty prop = GetIndexProperty();
  55. prop.SetValue(Instance, Unwrap(value), indexes);
  56. return true;
  57. }
  58. public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
  59. {
  60. if (binder == null)
  61. throw new ArgumentNullException(nameof(binder));
  62. if (args == null)
  63. throw new ArgumentNullException(nameof(args));
  64. for (int i = 0; i < args.Length; i++)
  65. {
  66. args[i] = Unwrap(args[i]);
  67. }
  68. var typeArgs = GetGenericMethodArguments(binder);
  69. result = InvokeMethodOnType(TargetType, Instance, binder.Name, args, typeArgs);
  70. // Wrap the sub object if necessary. This allows nested anonymous objects to work.
  71. result = result.AsDynamic();
  72. return true;
  73. }
  74. public override bool TryConvert(ConvertBinder binder, out object result)
  75. {
  76. if (binder == null)
  77. throw new ArgumentNullException(nameof(binder));
  78. result = binder.Type.GetTypeInfo().IsInstanceOfType(RealObject) ? RealObject : Convert.ChangeType(RealObject, binder.Type);
  79. return true;
  80. }
  81. public override string ToString()
  82. {
  83. Debug.Assert(Instance != null);
  84. return Instance.ToString();
  85. }
  86. private IProperty GetIndexProperty()
  87. {
  88. // The index property is always named "Item" in C#
  89. return GetProperty("Item");
  90. }
  91. private IProperty GetProperty(string propertyName)
  92. {
  93. // Get the list of properties and fields for this type
  94. IDictionary<string, IProperty> typeProperties = GetTypeProperties(TargetType);
  95. // Look for the one we want
  96. if (typeProperties.TryGetValue(propertyName, out IProperty property))
  97. return property;
  98. // The property doesn't exist
  99. // Get a list of supported properties and fields and show them as part of the exception message
  100. // For fields, skip the auto property backing fields (which name start with <)
  101. var propNames = typeProperties.Keys.Where(name => name[0] != '<').OrderBy(name => name);
  102. throw new MissingMemberException(
  103. $"The property {propertyName} doesn\'t exist on type {TargetType}. Supported properties are: {string.Join(", ", propNames)}");
  104. }
  105. private IDictionary<string, IProperty> GetTypeProperties(Type type)
  106. {
  107. // First, check if we already have it cached
  108. if (PropertiesOnType.TryGetValue(type, out IDictionary<string, IProperty> typeProperties))
  109. return typeProperties;
  110. // Not cached, so we need to build it
  111. typeProperties = new Dictionary<string, IProperty>();
  112. // First, recurse on the base class to add its fields
  113. if (type.GetTypeInfo().BaseType != null)
  114. {
  115. foreach (IProperty prop in GetTypeProperties(type.GetTypeInfo().BaseType).Values)
  116. {
  117. typeProperties[prop.Name] = prop;
  118. }
  119. }
  120. // Then, add all the properties from the current type
  121. foreach (PropertyInfo prop in type.GetTypeInfo().GetProperties(BindingFlags))
  122. {
  123. if (prop.DeclaringType == type)
  124. {
  125. typeProperties[prop.Name] = new Property(prop);
  126. }
  127. }
  128. // Finally, add all the fields from the current type
  129. foreach (FieldInfo field in type.GetTypeInfo().GetFields(BindingFlags))
  130. {
  131. if (field.DeclaringType == type)
  132. {
  133. typeProperties[field.Name] = new Field(field);
  134. }
  135. }
  136. // Cache it for next time
  137. PropertiesOnType[type] = typeProperties;
  138. return typeProperties;
  139. }
  140. private static bool ParametersCompatible(MethodInfo method, object[] passedArguments)
  141. {
  142. Debug.Assert(method != null);
  143. Debug.Assert(passedArguments != null);
  144. var parametersOnMethod = method.GetParameters();
  145. if (parametersOnMethod.Length != passedArguments.Length)
  146. return false;
  147. for (int i = 0; i < parametersOnMethod.Length; ++i)
  148. {
  149. var parameterType = parametersOnMethod[i].ParameterType.GetTypeInfo();
  150. ref var argument = ref passedArguments[i];
  151. if (argument == null && parameterType.IsValueType)
  152. {
  153. // Value types can not be null.
  154. return false;
  155. }
  156. if (!parameterType.IsInstanceOfType(argument))
  157. {
  158. // Parameters should be instance of the parameter type.
  159. if (parameterType.IsByRef)
  160. {
  161. var typePassedByRef = parameterType.GetElementType().GetTypeInfo();
  162. Debug.Assert(typePassedByRef != null);
  163. if (typePassedByRef.IsValueType && argument == null)
  164. {
  165. return false;
  166. }
  167. if (argument != null)
  168. {
  169. var argumentType = argument.GetType().GetTypeInfo();
  170. var argumentByRefType = argumentType.MakeByRefType().GetTypeInfo();
  171. if (parameterType != argumentByRefType)
  172. {
  173. try
  174. {
  175. argument = Convert.ChangeType(argument, typePassedByRef.AsType());
  176. }
  177. catch (InvalidCastException)
  178. {
  179. return false;
  180. }
  181. }
  182. }
  183. }
  184. else if (argument == null)
  185. {
  186. continue;
  187. }
  188. else
  189. {
  190. return false;
  191. }
  192. }
  193. }
  194. return true;
  195. }
  196. private static object InvokeMethodOnType(Type type, object target, string name, object[] args, Type[] typeArgs)
  197. {
  198. Debug.Assert(type != null);
  199. Debug.Assert(args != null);
  200. Debug.Assert(typeArgs != null);
  201. const BindingFlags allMethods =
  202. BindingFlags.Public | BindingFlags.NonPublic
  203. | BindingFlags.Instance | BindingFlags.Static;
  204. MethodInfo method = null;
  205. Type currentType = type;
  206. while (method == null && currentType != null)
  207. {
  208. var methods = currentType.GetTypeInfo().GetMethods(allMethods);
  209. MethodInfo candidate;
  210. for (int i = 0; i < methods.Length; ++i)
  211. {
  212. candidate = methods[i];
  213. if (candidate.Name == name)
  214. {
  215. // Check if the method is called as a generic method.
  216. if (typeArgs.Length > 0 && candidate.ContainsGenericParameters)
  217. {
  218. var candidateTypeArgs = candidate.GetGenericArguments();
  219. if (candidateTypeArgs.Length == typeArgs.Length)
  220. {
  221. candidate = candidate.MakeGenericMethod(typeArgs);
  222. }
  223. }
  224. if (ParametersCompatible(candidate, args))
  225. {
  226. method = candidate;
  227. break;
  228. }
  229. }
  230. }
  231. if (method == null)
  232. {
  233. // Move up in the type hierarchy.
  234. currentType = currentType.GetTypeInfo().BaseType;
  235. }
  236. }
  237. if (method == null)
  238. {
  239. throw new MissingMethodException($"Method with name '{name}' not found on type '{type.FullName}'.");
  240. }
  241. return method.Invoke(target, args);
  242. }
  243. private static object Unwrap(object o)
  244. {
  245. // If it's a wrap object, unwrap it and return the real thing
  246. if (o is DynamicObjectBase wrappedObj)
  247. return wrappedObj.RealObject;
  248. // Otherwise, return it unchanged
  249. return o;
  250. }
  251. private static Type[] GetGenericMethodArguments(InvokeMemberBinder binder)
  252. {
  253. var csharpInvokeMemberBinderType = binder
  254. .GetType().GetTypeInfo()
  255. .GetInterface("Microsoft.CSharp.RuntimeBinder.ICSharpInvokeOrInvokeMemberBinder")
  256. .GetTypeInfo();
  257. var typeArgsList = (IList<Type>)csharpInvokeMemberBinderType.GetProperty("TypeArguments").GetValue(binder, null);
  258. Type[] typeArgs;
  259. if (typeArgsList.Count == 0)
  260. {
  261. #if NET45
  262. typeArgs = _emptyTypes;
  263. #else
  264. typeArgs = Array.Empty<Type>();
  265. #endif
  266. }
  267. else
  268. {
  269. typeArgs = typeArgsList.ToArray();
  270. }
  271. return typeArgs;
  272. }
  273. }
  274. }