PageRenderTime 51ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/Sources/DataAccess/StrongTypeBinder.cs

https://github.com/tpwalke2/DataTable
C# | 239 lines | 162 code | 30 blank | 47 comment | 30 complexity | edde1d441a19529c93001a854307fa23 MD5 | raw file
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Reflection;
  6. using System.ComponentModel;
  7. using System.Diagnostics;
  8. using System.Linq.Expressions;
  9. namespace DataAccess
  10. {
  11. // Binder for creating a strong type from a Row
  12. internal class StrongTypeBinder
  13. {
  14. // Map from type property names to column names.
  15. public static Type[] GetTypes(Type target, DataTable table)
  16. {
  17. string[] columnNamesNormalized = table.ColumnNames.ToArray();
  18. Type[] types = new Type[columnNamesNormalized.Length];
  19. columnNamesNormalized = Array.ConvertAll(columnNamesNormalized, Normalize);
  20. PropertyInfo[] targetProperties = target.GetProperties(BindingFlags.Instance | BindingFlags.Public);
  21. foreach (PropertyInfo p in targetProperties)
  22. {
  23. int index = LookupRowIndex(columnNamesNormalized, p.Name);
  24. if (index >= 0)
  25. {
  26. types[index] = p.PropertyType;
  27. }
  28. }
  29. return types;
  30. }
  31. // Create a strongly-typed custom parse method for the object.
  32. // This can frontload all type analysis and generate a dedicated method that avoids Reflection.
  33. public static Func<Row, T> BuildMethod<T>(IEnumerable<string> columnNames)
  34. {
  35. ParameterExpression param = Expression.Parameter(typeof(Row), "row");
  36. Type target = typeof(T);
  37. string[] columnNamesNormalized = columnNames.ToArray();
  38. columnNamesNormalized = Array.ConvertAll(columnNamesNormalized, Normalize);
  39. PropertyInfo[] targetProperties = target.GetProperties(BindingFlags.Instance | BindingFlags.Public);
  40. if ((targetProperties.Length == 0) && (columnNamesNormalized.Length == 1))
  41. {
  42. // If it's just a single column and we have no properties (such as for int, guid, double, etc)
  43. // Just parse row.Values[0]
  44. int index = 0;
  45. MethodInfo miLookup = ((Func<Row, int, string>)LookupExpression).Method;
  46. var lookupExpr = Expression.Call(miLookup, param, Expression.Constant(index)); // (row,int) --> string
  47. var parseResultExpr = GetParseExpression(target, lookupExpr); // string --> T
  48. return Expression.Lambda<Func<Row, T>>(parseResultExpr, param).Compile();
  49. }
  50. // Produce a delegate like:
  51. // T Parse(Row row){
  52. // newObj = new T();
  53. // newObj.Prop1 = Parse(row.Values[i1]);
  54. // newObj.Prop2 = Parse(row.Values[12]);
  55. // ...
  56. // return newObj
  57. // }
  58. //
  59. // i1,i2, are computed at build time doing name matching.
  60. // Parse() function is determined at build time based on property type.
  61. List<Expression> statements = new List<Expression>();
  62. var newObj = Expression.Variable(target, "target");
  63. statements.Add(Expression.Assign(newObj, Expression.New(target)));
  64. foreach (PropertyInfo p in targetProperties)
  65. {
  66. if (p.CanWrite)
  67. {
  68. int index = LookupRowIndex(columnNamesNormalized, p.Name);
  69. if (index == -1)
  70. {
  71. // Ignore properties where no matching column, leave as default value.
  72. // Except set strings to string.Empty instead of null.
  73. if (p.PropertyType == typeof(string))
  74. {
  75. var setExpr = Expression.Call(newObj, p.GetSetMethod(), Expression.Constant(string.Empty));
  76. statements.Add(setExpr);
  77. }
  78. }
  79. else
  80. {
  81. MethodInfo miLookup = ((Func<Row, int, string>)LookupExpression).Method;
  82. var lookupExpr = Expression.Call(miLookup, param, Expression.Constant(index));
  83. var parseResultExpr = GetParseExpression(p.PropertyType, lookupExpr);
  84. var setExpr = Expression.Call(newObj, p.GetSetMethod(), parseResultExpr);
  85. statements.Add(setExpr);
  86. }
  87. }
  88. }
  89. statements.Add(newObj); // return result
  90. Expression body = Expression.Block(new[] { newObj }, statements);
  91. Func<Row, T> lambda =
  92. Expression.Lambda<Func<Row, T>>(
  93. body, param).Compile();
  94. return lambda;
  95. }
  96. // Find the column index for the given name.
  97. // This is done once when creating the delegate, so it can have heavier string pattern matching logic.
  98. // This lets runtime just do an index lookup.
  99. // columnNames have already been normalized.
  100. static int LookupRowIndex(string[] columnNamesNormalized, string columnNameLookup)
  101. {
  102. columnNameLookup = Normalize(columnNameLookup);
  103. int i = 0;
  104. foreach(string x in columnNamesNormalized)
  105. {
  106. if (string.Compare(x, columnNameLookup, ignoreCase: true) == 0)
  107. {
  108. return i;
  109. }
  110. i++;
  111. }
  112. return -1;
  113. }
  114. // Normalize the column name. Convert to upper case, alphanumeric.
  115. static string Normalize(string name)
  116. {
  117. StringBuilder sb = new StringBuilder();
  118. sb.Append('_'); // avoids first char issues.
  119. foreach (var ch in name)
  120. {
  121. if (ch >= 'a' && ch <= 'z')
  122. {
  123. sb.Append((char) (ch - 'a' + 'A'));
  124. }
  125. else if (ch >= 'A' && ch <= 'Z')
  126. {
  127. sb.Append(ch);
  128. }
  129. else if (ch >= '0' && ch <= '9')
  130. {
  131. sb.Append(ch);
  132. }
  133. // ignore all other chars
  134. }
  135. return sb.ToString();
  136. }
  137. // runtime helper to find the expression.
  138. // index should be valid. Delegate builder already had a chance to analyze column names at compile time
  139. // and so avoid this method if the column doesn't exist.
  140. static string LookupExpression(Row row, int index)
  141. {
  142. return row.Values[index];
  143. }
  144. // Get an Expression tree which will parse the string (provided by value), and return a result of type Type, eg:
  145. // Func: string --> Type
  146. // This can do static analysis on type to return an efficient parse function.
  147. // This avoids a runtime search on type.
  148. static Expression GetParseExpression(Type type, Expression value)
  149. {
  150. // Input parameter is a string, which we'll parse.
  151. Debug.Assert(value.Type == typeof(string));
  152. // If it's a string, just return directly.
  153. if (type == typeof(string))
  154. {
  155. return value;
  156. }
  157. if (type == typeof(double))
  158. {
  159. MethodInfo parseDoubleMethod = ((Func<string, double>)ToDouble).Method;
  160. return Expression.Call(parseDoubleMethod, value);
  161. }
  162. // If it has a TryParse function, call that. That's much faster than a Type converter
  163. MethodInfo tryParseMethod = type.GetMethod("TryParse", new[] { typeof(string), type.MakeByRefType() });
  164. if (tryParseMethod != null)
  165. {
  166. // can't pass a property as an out parameter, so we need a temporary local.
  167. // compile as:
  168. // { T temp;
  169. // TryParse(value, out temp);
  170. // return temp
  171. // }
  172. var temp = Expression.Variable(type);
  173. return Expression.Block(new[] { temp }, // define Local
  174. Expression.Call(tryParseMethod, value, temp),
  175. temp); // return temp
  176. }
  177. {
  178. // Type converter lookup is slow and can be hoisted in the closure and done statically.
  179. var converter = TypeDescriptor.GetConverter(type);
  180. var converterExpr = Expression.Constant(converter); // hoisted
  181. // compile:
  182. // { return (T) converter.ConvertFrom(value); }
  183. var convertMethod = ((Func<object, object>)converter.ConvertFrom).Method;
  184. var exprCall = Expression.Call(converterExpr, convertMethod, value);
  185. return Expression.Convert(exprCall, type);
  186. }
  187. }
  188. // Parse a double, handle percents.
  189. // Return NaN on failure.
  190. private static double ToDouble(string s)
  191. {
  192. double result;
  193. if (double.TryParse(s, out result))
  194. {
  195. return result;
  196. }
  197. // Handle percents. 100% --> 1
  198. if (s.EndsWith("%"))
  199. {
  200. string s2 = s.Substring(0, s.Length - 1);
  201. if (double.TryParse(s2, out result))
  202. {
  203. return result / 100.0;
  204. }
  205. }
  206. return double.NaN;
  207. }
  208. }
  209. }