Como se señaló en los comentarios, el problema es que no existe una columna en el lector para la propiedad especificada. La idea es recorrer primero los nombres de la columna del lector y verificar si existe una propiedad coincidente. Pero, ¿cómo se obtienen de antemano la lista de nombres de columnas?
Una idea es usar árboles de expresión en sí para construir la lista de nombres de columnas del lector y verificarlo con las propiedades de la clase. Algo como esto
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));
sería el equivalente de
List<string> columnNames = new List<string>(); for (int i = 0; i < reader.FieldCount; i++) { columnNames.Add(reader.GetName(i)); }
Uno puede continuar con la expresión final, pero aquí hay una captura que hace más esfuerzo a lo largo de esta línea inútil. El árbol de expresión anterior buscará los nombres de la columna cada vez que se llame el delegado final que en su caso es para cada creación de objetos, que está en contra del espíritu de su requisito.
Otro enfoque es dejar que la clase convertidor tenga una conciencia predefinida de los nombres de la columna para un tipo dado, mediante atributos (ver para un ejemplo) o manteniendo un diccionario estático como (
Dictionary<Type, IEnumerable<string>>
). Aunque da más flexibilidad, la otra cara es que su consulta no siempre debe incluir todos los nombres de columnas de una tabla, y cualquierareader[notInTheQueryButOnlyInTheTableColumn]
resultaría en una excepción.El mejor enfoque como veo es obtener los nombres de la columna del objeto del lector, pero solo una vez. Reescribiría la cosa 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; }
Ahora, eso plantea la pregunta ¿Por qué no pasar al lector de datos directamente al constructor? Eso estaría mejor.
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; }
Llámalo como
List<T> list = new List<T>(); var converter = new Converter<T>(dr); while (dr.Read()) { var obj = converter.CreateItemFromRow(); list.Add(obj); }
Sin embargo, hay una serie de mejoras que puedo sugerir.
El genérico
new T()
estas llamandoCreateItemFromRow
es más lento, Utiliza la reflexión detrás de escena. Puedes delegar esa parte a los árboles de expresión también que debería ser más rápidoEn este momento
GetProperty
La llamada no es insensible al caso, lo que significa que los nombres de su columna deberán coincidir exactamente con el nombre de la propiedad. Lo haría insensible al caso usando uno de esosBindings.Flag
.No estoy seguro de por qué estás usando un
ConcurrentDictionary
Como mecanismo de almacenamiento en caché aquí. Un campo estático en una clase genérica<T>
Será único para cadaT
. El campo genérico en sí puede actuar como caché. Además, ¿por qué es elValue
parte deConcurrentDictionary
de tipoobject
?Como dije antes, no es lo mejor para unir fuertemente un tipo y los nombres de la columna (que estás haciendo almacenando en caché un en particular
Action
delegado por escribe). Incluso para el mismo tipo, sus consultas pueden ser diferentes seleccionando diferentes conjuntos de columnas. Es mejor dejar que el lector de datos decida.Usar
Expression.Convert
en vez deExpression.TypeAs
Para la conversión de tipo de valor deobject
.También tenga en cuenta que Reader.getordinal es una forma mucho más rápida de realizar las búsquedas de lectores de datos.
Reescribiría todo 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);
}