Как отмечалось в комментариях, проблема заключается в том, что в программе чтения не существует столбца для указанного свойства.Идея состоит в том, чтобы сначала выполнить цикл по именам столбцов reader и проверить, существует ли соответствующее свойство.Но как заранее получить список названий столбцов?
Одна из идей состоит в том, чтобы использовать сами деревья выражений для создания списка имен столбцов из программы чтения и сверки его со свойствами класса.Что - то вроде этого
var paramExp = Expression.Parameter(typeof(IDataRecord), "o7thDR"); var loopIncrementVariableExp = Expression.Parameter(typeof(int), "i"); var columnNamesExp = Expression.Parameter(typeof(List<string>), "columnNames"); var columnCountExp = Expression.Property(paramExp, "FieldCount"); var getColumnNameExp = Expression.Call(paramExp, "GetName", Type.EmptyTypes, Expression.PostIncrementAssign(loopIncrementVariableExp)); var addToListExp = Expression.Call(columnNamesExp, "Add", Type.EmptyTypes, getColumnNameExp); var labelExp = Expression.Label(columnNamesExp.Type); var getColumnNamesExp = Expression.Block( new[] { loopIncrementVariableExp, columnNamesExp }, Expression.Assign(columnNamesExp, Expression.New(columnNamesExp.Type)), Expression.Loop( Expression.IfThenElse( Expression.LessThan(loopIncrementVariableExp, columnCountExp), addToListExp, Expression.Break(labelExp, columnNamesExp)), labelExp));
было бы эквивалентно
List<string> columnNames = new List<string>(); for (int i = 0; i < reader.FieldCount; i++) { columnNames.Add(reader.GetName(i)); }
Можно продолжить с последним выражением, но здесь есть загвоздка, делающая любые дальнейшие усилия в этом направлении бесполезными.Приведенное выше дерево выражений будет извлекать имена столбцов каждый раз при вызове конечного делегата, который в вашем случае используется для каждого создания объекта, что противоречит духу вашего требования.
Другой подход заключается в том, чтобы позволить классу converter заранее знать имена столбцов для данного типа с помощью атрибутов (смотрите пример) или путем поддержания статического словаря, подобного (
Dictionary<Type, IEnumerable<string>>
).Хотя это дает большую гибкость, оборотной стороной является то, что ваш запрос не всегда должен включать имена всех столбцов таблицы и любыеreader[notInTheQueryButOnlyInTheTableColumn]
это привело бы к исключению.Лучший подход, на мой взгляд, заключается в извлечении имен столбцов из объекта reader, но только один раз.Я бы переписал эту вещь следующим образом:
private static List<string> columnNames; private static Action<IDataReader, T> GetMapFunc() { var exps = new List<Expression>(); var paramExp = Expression.Parameter(typeof(IDataRecord), "o7thDR"); var targetExp = Expression.Parameter(typeof(T), "o7thTarget"); var getPropInfo = typeof(IDataRecord).GetProperty("Item", new[] { typeof(string) }); foreach (var columnName in columnNames) { var property = typeof(T).GetProperty(columnName); if (property == null) continue; // use 'columnName' instead of 'property.Name' to speed up reader lookups //in case of certain readers. var columnNameExp = Expression.Constant(columnName); var getPropExp = Expression.MakeIndex( paramExp, getPropInfo, new[] { columnNameExp }); var castExp = Expression.TypeAs(getPropExp, property.PropertyType); var bindExp = Expression.Assign( Expression.Property(targetExp, property), castExp); exps.Add(bindExp); } return Expression.Lambda<Action<IDataReader, T>>( Expression.Block(exps), paramExp, targetExp).Compile(); } internal T CreateItemFromRow(IDataReader dataReader) { if (columnNames == null) { columnNames = Enumerable.Range(0, dataReader.FieldCount) .Select(x => dataReader.GetName(x)) .ToList(); _convertAction = (Action<IDataReader, T>)_convertActionMap.GetOrAdd( typeof(T), (t) => GetMapFunc()); } T result = new T(); _convertAction(dataReader, result); return result; }
Теперь возникает вопрос, почему бы не передать средство чтения данных непосредственно конструктору?Так было бы лучше.
private IDataReader dataReader; private Action<IDataReader, T> GetMapFunc() { var exps = new List<Expression>(); var paramExp = Expression.Parameter(typeof(IDataRecord), "o7thDR"); var targetExp = Expression.Parameter(typeof(T), "o7thTarget"); var getPropInfo = typeof(IDataRecord).GetProperty("Item", new[] { typeof(string) }); var columnNames = Enumerable.Range(0, dataReader.FieldCount) .Select(x => dataReader.GetName(x)); foreach (var columnName in columnNames) { var property = typeof(T).GetProperty(columnName); if (property == null) continue; // use 'columnName' instead of 'property.Name' to speed up reader lookups //in case of certain readers. var columnNameExp = Expression.Constant(columnName); var getPropExp = Expression.MakeIndex( paramExp, getPropInfo, new[] { columnNameExp }); var castExp = Expression.TypeAs(getPropExp, property.PropertyType); var bindExp = Expression.Assign( Expression.Property(targetExp, property), castExp); exps.Add(bindExp); } return Expression.Lambda<Action<IDataReader, T>>( Expression.Block(exps), paramExp, targetExp).Compile(); } internal Converter(IDataReader dataReader) { this.dataReader = dataReader; _convertAction = (Action<IDataReader, T>)_convertActionMap.GetOrAdd( typeof(T), (t) => GetMapFunc()); } internal T CreateItemFromRow() { T result = new T(); _convertAction(dataReader, result); return result; }
Назовите это так
List<T> list = new List<T>(); var converter = new Converter<T>(dr); while (dr.Read()) { var obj = converter.CreateItemFromRow(); list.Add(obj); }
Однако есть ряд улучшений, которые я могу предложить.
Общий
new T()
ты звонишь по телефонуCreateItemFromRow
медленнее, он использует отражение за кулисами.Вы также можете делегировать эту часть деревьям выражений что должно быть быстрееПрямо сейчас
GetProperty
call не чувствителен к регистру, что означает, что имена ваших столбцов должны точно совпадать с именем свойства.Я бы сделал это без учета регистра, используя один из нихBindings.Flag
.Я совсем не уверен, почему вы используете
ConcurrentDictionary
в качестве механизма кэширования здесь. Статическое поле в универсальном классе<T>
будет уникальным для каждогоT
.Само универсальное поле может выступать в качестве кэша.Кроме того, почемуValue
частьConcurrentDictionary
типаobject
?Как я уже говорил ранее, не стоит сильно привязывать тип и имена столбцов (что вы делаете, кэшируя один конкретный
Action
делегировать на Тип).Даже для одного и того же типа ваши запросы могут отличаться при выборе разного набора столбцов.Лучше всего оставить это на усмотрение читателя данных.Использование
Expression.Convert
вместо того, чтобыExpression.TypeAs
для преобразования типа значения изobject
.Также обратите внимание, что считыватель.GetOrdinal - это гораздо более быстрый способ выполнения поиска в программе чтения данных.
Я бы переписал все это заново следующим образом:
readonly Func<IDataReader, T> _converter;
readonly IDataReader dataReader;
private Func<IDataReader, T> GetMapFunc()
{
var exps = new List<Expression>();
var paramExp = Expression.Parameter(typeof(IDataRecord), "o7thDR");
var targetExp = Expression.Variable(typeof(T));
exps.Add(Expression.Assign(targetExp, Expression.New(targetExp.Type)));
//does int based lookup
var indexerInfo = typeof(IDataRecord).GetProperty("Item", new[] { typeof(int) });
var columnNames = Enumerable.Range(0, dataReader.FieldCount)
.Select(i => new { i, name = dataReader.GetName(i) });
foreach (var column in columnNames)
{
var property = targetExp.Type.GetProperty(
column.name,
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
if (property == null)
continue;
var columnNameExp = Expression.Constant(column.i);
var propertyExp = Expression.MakeIndex(
paramExp, indexerInfo, new[] { columnNameExp });
var convertExp = Expression.Convert(propertyExp, property.PropertyType);
var bindExp = Expression.Assign(
Expression.Property(targetExp, property), convertExp);
exps.Add(bindExp);
}
exps.Add(targetExp);
return Expression.Lambda<Func<IDataReader, T>>(
Expression.Block(new[] { targetExp }, exps), paramExp).Compile();
}
internal Converter(IDataReader dataReader)
{
this.dataReader = dataReader;
_converter = GetMapFunc();
}
internal T CreateItemFromRow()
{
return _converter(dataReader);
}