/* BSD 3-Clause License Copyright (c) 2017, B. Wofter All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ namespace BWofter.Converters.Data { using BWofter.Converters.EqualityComparers; using BWofter.Converters.Expressions; using BWofter.Converters.Extensions; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; using System.Linq; using System.Linq.Expressions; using System.Reflection; /// A static class used to convert data tables into entities. /// The entity type to convert to. public static class DataTableConverter where TEntity : class, new() { private static readonly Type type = typeof(TEntity); private static readonly ConcurrentDictionary, Func> converters = new ConcurrentDictionary, Func>(StringCollectionComparer.GetInstance()); /// Iterates over the values in the , mapping their fields /// to the entity type. /// The to map to the entity type. /// A yielded instance of the entity type. public static IEnumerable ToEntities(DataTable dataTable) { if (dataTable == null) throw new ArgumentNullException(nameof(dataTable)); return ToEntities(dataTable, new Dictionary(dataTable.Columns.Select(GetDataColumnKeyValuePair).ToDictionary())); } /// Iterates over the values in the , mapping their fields /// to the entity type. /// The to map to the entity type. /// The to map the values to properties. /// A yielded instance of the entity type. public static IEnumerable ToEntities(DataTable dataTable, IDictionary columnToMemberMap) { if (dataTable == null) throw new ArgumentNullException(nameof(dataTable)); if (columnToMemberMap == null) throw new ArgumentNullException(nameof(columnToMemberMap)); Func converter = GetConverter(columnToMemberMap); if (converter == null) throw new InvalidOperationException($"Unable to generate a converter for the data table. This could be due to a bug in the expression generator."); //Iterate over the data rows in the data table, yielding the results back to the caller. foreach (DataRow row in dataTable.Rows) { yield return converter(row); } } /// Iterates over the values in , mapping their fields /// to the entity type. /// The to map to the entity type. /// A yielded instance of the entity type. public static IEnumerable ToEntities(DataRowCollection dataRows) { if (dataRows == null) throw new ArgumentNullException(nameof(dataRows)); if (dataRows.Count == 0) throw new ArgumentException($"Parameter {nameof(dataRows)} should have at least 1 value, 0 given."); return ToEntities(dataRows, dataRows[0].Table.Columns.Select(GetDataColumnKeyValuePair).ToDictionary()); } /// Iterates over the values in , mapping their fields /// to the entity type. /// The to map to the entity type. /// The to map the values to properties. /// A yielded instance of the entity type. public static IEnumerable ToEntities(DataRowCollection dataRows, IDictionary columnToMemberMap) { if (dataRows == null) throw new ArgumentNullException(nameof(dataRows)); if (columnToMemberMap == null) throw new ArgumentNullException(nameof(columnToMemberMap)); Func converter = GetConverter(columnToMemberMap); if (converter == null) throw new InvalidOperationException($"Unable to generate a converter for the data table. This could be due to a bug in the expression generator."); //Iterate over the data rows in the data table, yielding the results back to the caller. foreach (DataRow row in dataRows) { yield return converter(row); } } /// Iterates over the values in , mapping their fields /// to the entity type. /// The of values to map to the entity type. /// A yielded instance of the entity type. public static IEnumerable ToEntities(IEnumerable dataRows) { DataRow first = dataRows?.FirstOrDefault(); if (dataRows == null) throw new ArgumentNullException(nameof(dataRows)); if (first == null) throw new ArgumentException($"Parameter {nameof(dataRows)} should have at least 1 value, 0 given."); return ToEntities(dataRows, first.Table.Columns.Select(GetDataColumnKeyValuePair).ToDictionary()); } /// Iterates over the values in , mapping their fields /// to the entity type. /// The of values to map to the entity type. /// The to map the values to properties. /// A yielded instance of the entity type. public static IEnumerable ToEntities(IEnumerable dataRows, IDictionary columnToMemberMap) { if (dataRows == null) throw new ArgumentNullException(nameof(dataRows)); if (columnToMemberMap == null) throw new ArgumentNullException(nameof(columnToMemberMap)); Func converter = GetConverter(columnToMemberMap); if (converter == null) throw new InvalidOperationException($"Unable to generate a converter for the data table. This could be due to a bug in the expression generator."); //Iterate over the data rows in the data table, yielding the results back to the caller. foreach (DataRow row in dataRows) { yield return converter(row); } } /// Iterates over the values in , mapping their fields /// to the entity type. /// The of values to map to the entity type. /// A yielded instance of the entity type. public static IEnumerable ToEntities(params DataRow[] dataRows) { if (dataRows == null) throw new ArgumentNullException(nameof(dataRows)); if (dataRows.Length == 0) throw new ArgumentException($"Parameter {nameof(dataRows)} should have at least 1 value, 0 given."); return ToEntities(dataRows, dataRows[0].Table.Columns.Select(GetDataColumnKeyValuePair).ToDictionary()); } /// Iterates over the values in , mapping their fields /// to the entity type. /// The to map the values to properties. /// The of values to map to the entity type. /// A yielded instance of the entity type. public static IEnumerable ToEntities(IDictionary columnToMemberMap, params DataRow[] dataRows) { if (dataRows == null) throw new ArgumentNullException(nameof(dataRows)); if (columnToMemberMap == null) throw new ArgumentNullException(nameof(columnToMemberMap)); Func converter = GetConverter(columnToMemberMap); if (converter == null) throw new InvalidOperationException($"Unable to generate a converter for the data table. This could be due to a bug in the expression generator."); //Iterate over the data rows in the data table, yielding the results back to the caller. foreach (DataRow row in dataRows) { yield return converter(row); } } //Creates a cached generator for the converter to use for all data tables with a matching set of data columns. Due to our assumption that data types are loose in //data tables, this will generate an extremely generic converter that doesn't statically use the data table's data typing. private static Func GetConverter(IDictionary columnToMemberMap) { List columnNames = columnToMemberMap.Select(k => k.Key.ColumnName).ToList(); if (!converters.TryGetValue(columnNames, out Func value)) { NewExpression instantiate = Expression.New(type); ParameterExpression dataRow = Expression.Parameter(typeof(DataRow), "dataRow"); //Declare a dictionary to prevent creating more than 1 instance of a temporary parser type. ConcurrentDictionary parameters = new ConcurrentDictionary(); List memberBindings = new List(); //Iterate over the column map and generate the expressions needed. foreach (KeyValuePair columnToMember in columnToMemberMap) { if (TryGetMemberInfo(columnToMember.Value, columnToMember.Key.Table.CaseSensitive, out MemberInfo memberInfo)) { //Get a member expression. This is used for type resolution. Type memberType = Expression.MakeMemberAccess(instantiate, memberInfo).Type, realType = Nullable.GetUnderlyingType(memberType) ?? memberType; //Get the data column expression used to access the data row field. DataColumnExpression dataColumn = DataExpression.DataColumn(DataExpression.DataTable(dataRow), columnToMember.Key.ColumnName); //Get the is null expression used to determine if the data field is null. DataFieldIsNullExpression callDataFieldIsNull = DataExpression.DataFieldIsNull(dataRow, dataColumn); ConditionalExpression dataFieldIsNull = Expression.Condition(callDataFieldIsNull, Expression.Default(memberType), Expression.Default(memberType)); //Get the is assignable from expression used to determine if the data column can be converted to the member type. IsAssignableFromExpression callIsAssignableFrom = DataExpression.IsAssignableFrom(DataExpression.DataType(dataColumn), memberType); //Get the is assignable from true result. If member type is string, then call the trim method on the string. Expression isAssignableFromTrue = Expression.Convert(DataExpression.DataField(dataRow, dataColumn), memberType); if (typeof(string).IsAssignableFrom(memberType)) { isAssignableFromTrue = Expression.Call(isAssignableFromTrue, "Trim", Type.EmptyTypes); } //Get the conditional expression used to process the data column type conversion. ConditionalExpression isAssignableFrom = Expression.Condition(callIsAssignableFrom, isAssignableFromTrue, Expression.Default(memberType)); if (realType.GetMethods().Any(m => m.Name == "TryParse")) { //Get the is string parameter expression used for try parsing. ParameterExpression stringLocal = parameters.GetOrAdd(typeof(string), Expression.Variable(typeof(string))), outLocal = parameters.GetOrAdd(realType, Expression.Variable(realType)); //Get the type is assign and try parse expressions used for try parsing. TypeIsAssignExpression typeIsAssign = DataExpression.TypeIsAssign( DataExpression.DataField(dataRow, dataColumn), stringLocal); TryParseExpression callTryParse = DataExpression.TryParse(stringLocal, outLocal); //Get the conditional expression used to process the try parse conversion. ConditionalExpression tryParse = Expression.Condition( Expression.AndAlso(typeIsAssign, callTryParse), Expression.Convert(outLocal, memberType), Expression.Default(memberType)); //Update is assignable from with the try parse method. isAssignableFrom = isAssignableFrom.Update(isAssignableFrom.Test, isAssignableFrom.IfTrue, tryParse); } if (typeof(IConvertible).IsAssignableFrom(realType)) { //Get the change type expression. ChangeTypeExpression callChangeType = DataExpression.ChangeType( DataExpression.DataField(dataRow, dataColumn), realType); //Get the conversion expression for the change type. If member type is string, then call the trim method on the string. Expression changeType = Expression.Convert(callChangeType, memberType); if (typeof(string).IsAssignableFrom(memberType)) { changeType = Expression.Call(changeType, "Trim", Type.EmptyTypes); } //Determine if try parse is set. If so, update it and is assignable from. Otherwise, add change type to is assignable from. if (isAssignableFrom.IfFalse is ConditionalExpression tryParse) { tryParse = tryParse.Update(tryParse.Test, tryParse.IfTrue, changeType); isAssignableFrom = isAssignableFrom.Update(isAssignableFrom.Test, isAssignableFrom.IfTrue, tryParse); } else { isAssignableFrom = isAssignableFrom.Update(isAssignableFrom.Test, isAssignableFrom.IfTrue, changeType); } } dataFieldIsNull = dataFieldIsNull.Update(dataFieldIsNull.Test, dataFieldIsNull.IfTrue, isAssignableFrom); //Silently ignore conversion errors with the try catch expression. This is mostly for testing purposes and might be removed //in the future. memberBindings.Add(Expression.Bind(memberInfo, Expression.TryCatch(dataFieldIsNull, Expression.Catch(typeof(Exception), Expression.Default(memberType))))); } } //Get the converter lambda expression, creating the initialization block with its parameters. Expression> converter = Expression.Lambda>( Expression.Block(parameters.Values, Expression.MemberInit(instantiate, memberBindings)), dataRow); //Use expression reducer to reduce the conversion lambda expression, then compile and return the delegate. converter = (Expression>)new ExpressionReducer().Visit(converter); value = converters.GetOrAdd(new HashSet(columnNames), converter.Compile()); } return value; } //Attempts to get a member info object from the entity type the converter is converting to, returning true if one is found. private static bool TryGetMemberInfo(string memberName, bool caseSensitive, out MemberInfo memberInfo) { BindingFlags flags = BindingFlags.Instance | BindingFlags.Public; //Determine if the case sensitivite bool is false and add the ignore case flag to the binding search. if (!caseSensitive) { flags |= BindingFlags.IgnoreCase; } //Determine if a property exists with the name member name and has the appropriate flags and set the out variable to this property info. if (type.GetProperty(memberName, flags) is PropertyInfo propertyInfo) { memberInfo = propertyInfo; } //Determine if a field exists with the name member name and has the appropriate flags and set the out variable to this field info. else if (type.GetField(memberName, flags) is FieldInfo fieldInfo) { memberInfo = fieldInfo; } //If no property or field exists in the type with the name given, assume that there is no valid target and set the out variable to null. else { memberInfo = null; } return memberInfo != null; } //Creates a key value pair for the data column/column's name. This is used whenever no dictionary is provided to seed the converter's data column => property map. private static KeyValuePair GetDataColumnKeyValuePair(DataColumn dataColumn) => new KeyValuePair(dataColumn, dataColumn.ColumnName); } }