Conforme observado nos comentários, o problema é que não existe coluna no leitor para a propriedade especificada. A idéia é fazer um loop pelos nomes das colunas do leitor primeiro e verifique se existe a propriedade correspondente. Mas como se obtém a lista de nomes de colunas com antecedência?
Uma idéia é usar as próprias árvores de expressão para construir a lista de nomes de colunas do leitor e verificá -la nas propriedades da classe. Algo assim
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));
seria o equivalente a
List<string> columnNames = new List<string>(); for (int i = 0; i < reader.FieldCount; i++) { columnNames.Add(reader.GetName(i)); }
Pode -se continuar com a expressão final, mas há um problema aqui fazendo mais esforço nessa linha inútil. A árvore de expressão acima estará buscando os nomes das colunas toda vez que o delegado final é chamado, que no seu caso é para todas as criações de objetos, o que é contra o espírito de sua exigência.
Outra abordagem é permitir que a classe Converter tenha uma consciência predefinida dos nomes das colunas para um determinado tipo, por meio de atributos (Veja por um exemplo) ou mantendo um dicionário estático como (
Dictionary<Type, IEnumerable<string>>
). Embora dê mais flexibilidade, o lado do flip é que sua consulta nem sempre precisa incluir todos os nomes de colunas de uma tabela e qualquerreader[notInTheQueryButOnlyInTheTableColumn]
resultaria em exceção.A melhor abordagem que vejo é buscar os nomes das colunas do objeto do leitor, mas apenas uma vez. Eu reescreveria o que é como:
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; }
Agora isso levanta a pergunta, por que não passar o leitor de dados diretamente para o construtor? Isso seria melhor.
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; }
Chame como
List<T> list = new List<T>(); var converter = new Converter<T>(dr); while (dr.Read()) { var obj = converter.CreateItemFromRow(); list.Add(obj); }
No entanto, existem várias melhorias que posso sugerir.
O genérico
new T()
você está ligandoCreateItemFromRow
é mais lento, usa reflexão nos bastidores. Você pode delegar essa parte para a expressão de árvores também que deve ser mais rápidoAgora mesmo
GetProperty
A chamada não é insensível ao caso, o que significa que seus nomes de colunas terão que corresponder exatamente ao nome da propriedade. Eu faria isso insensível ao caso usando um daquelesBindings.Flag
.Não tenho certeza por que você está usando um
ConcurrentDictionary
como um mecanismo de cache aqui. Um campo estático em uma classe genérica<T>
será único para cadaT
. O próprio campo genérico pode atuar como cache. Também por que é oValue
parte deConcurrentDictionary
do tipoobject
?Como eu disse anteriormente, não é o melhor amarrar fortemente um tipo e os nomes das colunas (o que você está fazendo em cache em um particular
Action
delegado por modelo). Mesmo para o mesmo tipo, suas consultas podem ser diferentes selecionando diferentes conjuntos de colunas. É melhor deixar o Data Reader decidir.Usar
Expression.Convert
ao invés deExpression.TypeAs
Para conversão de tipo de valor deobject
.Observe também isso leitor.getordinal é uma maneira muito mais rápida de executar as pesquisas do leitor de dados.
Eu reescreveria a coisa toda como:
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);
}