Ответ 1
Как отмечалось в комментариях, проблема в том, что для указанного свойства нет столбца в читателе. Идея состоит в том, чтобы сначала зациклировать имена столбцов читателя и проверить, существует ли свойство соответствия. Но как получить список имен столбцов заранее?
-
Одна идея состоит в том, чтобы сами использовать деревья выражений для создания списка имен столбцов из читателя и проверки его на свойства класса. Что-то вроде этого
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)); }
Можно продолжить с окончательным выражением, но здесь есть улов, делающий любые дальнейшие усилия вдоль этой линии бесполезными. Вышеупомянутое дерево выражений будет извлекать имена столбцов каждый раз, когда вызывается окончательный делегат, который в вашем случае предназначен для каждого создания объекта, что противоречит духу вашего требования.
-
Другой подход заключается в том, чтобы позволить классу преобразователя предустановленной осведомленности о именах столбцов для данного типа с помощью атрибутов (см. пример) или поддерживая статический словарь, подобный (
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
вызов не чувствителен к регистру, то есть ваши имена столбцов должны точно совпадать с именем свойства. Я бы сделал его нечувствительным к регистру, используя один из этихBindings.Flag
. -
Я не уверен, почему вы используете
ConcurrentDictionary
как механизм кэширования здесь. Статическое поле в родовом классе<T>
будет уникальным для каждогоT
. Основное поле может действовать как кеш. Также почемуValue
частьConcurrentDictionary
типаobject
? -
Как я уже говорил ранее, это не лучшее, чтобы сильно связать имена типов и столбцов (которые вы делаете, кэшируя один конкретный делегат
Action
для каждого типа). Даже для того же типа ваши запросы могут быть разными, выбрав другой набор столбцов. Лучше оставить его для чтения данных, чтобы решить. -
Используйте
Expression.Convert
вместоExpression.TypeAs
для преобразования типа значения изobject
. -
Также обратите внимание, что reader.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);
}