Mapeamento genérico de DbDataReader para lista

Eu estou tendo um pequeno problema (mais como um aborrecimento) com minhas classs de access de dados de vinculação de propriedade. O problema é que o mapeamento falha quando não existe nenhuma coluna no leitor para a propriedade correspondente na class.

Código

Aqui está a class do mapeador:

// Map our datareader object to a strongly typed list private static IList Map(DbDataReader dr) where T : new() { try { // initialize our returnable list List list = new List(); // fire up the lamda mapping var converter = new Converter(); while (dr.Read()) { // read in each row, and properly map it to our T object var obj = converter.CreateItemFromRow(dr); // add it to our list list.Add(obj); } // reutrn it return list; } catch (Exception ex) { return default(List); } } 

Classe do conversor:

 ///  /// Converter class to convert returned Sql Records to strongly typed classs ///  /// Type of the object we'll convert too internal class Converter where T : new() { // Concurrent Dictionay objects private static ConcurrentDictionary _convertActionMap = new ConcurrentDictionary(); // Delegate action declaration private Action _convertAction; // Build our mapping based on the properties in the class/type we've passed in to the class private static Action GetMapFunc() { var exps = new List(); var paramExp = Expression.Parameter(typeof(IDataReader), "o7thDR"); var targetExp = Expression.Parameter(typeof(T), "o7thTarget"); var getPropInfo = typeof(IDataRecord).GetProperty("Item", new[] { typeof(string) }); var _props = typeof(T).GetProperties(); foreach (var property in _props) { var getPropExp = Expression.MakeIndex(paramExp, getPropInfo, new[] { Expression.Constant(property.Name, typeof(string)) }); var castExp = Expression.TypeAs(getPropExp, property.PropertyType); var bindExp = Expression.Assign(Expression.Property(targetExp, property), castExp); exps.Add(bindExp); } // return our compiled mapping, this will ensure it is cached to use through our record looping return Expression.Lambda<Action>(Expression.Block(exps), new[] { paramExp, targetExp }).Compile(); } internal Converter() { // Fire off our mapping functionality _convertAction = (Action)_convertActionMap.GetOrAdd(typeof(T), (t) => GetMapFunc()); } internal T CreateItemFromRow(IDataReader dataReader) { T result = new T(); _convertAction(dataReader, result); return result; } } 

Exceção

 System.IndexOutOfRangeException {"Mileage"} 

Stacktrace

 at System.Data.ProviderBase.FieldNameLookup.GetOrdinal(String fieldName) at System.Data.SqlClient.SqlDataReader.GetOrdinal(String name) at System.Data.SqlClient.SqlDataReader.get_Item(String name) at lambda_method(Closure , IDataReader , Typing ) at o7th.Class.Library.Data.Converter`1.CreateItemFromRow(IDataReader dataReader) in d:\Backup Folder\Development\o7th Web Design\o7th.Class.Library.C-Sharp\o7th.Class.Library\Data Access Object\Converter.cs:line 50 at o7th.Class.Library.Data.Wrapper.Map[T](DbDataReader dr) in d:\Backup Folder\Development\o7th Web Design\o7th.Class.Library.C-Sharp\o7th.Class.Library\Data Access Object\Wrapper.cs:line 33 

Questão

Como posso consertá-lo, para que ele não falhe quando eu tiver uma propriedade extra que o leitor possa não ter como coluna e vice-versa? Claro que o rápido band-aid seria simplesmente adicionar NULL As Mileage a esta consulta no exemplo, no entanto, isso não é uma solução para o problema 🙂


Aqui está o Map usando a reflection:

 // Map our datareader object to a strongly typed list private static IList Map(DbDataReader dr) where T : new() { try { // initialize our returnable list List list = new List(); T item = new T(); PropertyInfo[] properties = (item.GetType()).GetProperties(); while (dr.Read()) { int fc = dr.FieldCount; for (int j = 0; j < fc; ++j) { var pn = properties[j].Name; var gn = dr.GetName(j); if (gn == pn) { properties[j].SetValue(item, dr[j], null); } } list.Add(item); } // return it return list; } catch (Exception ex) { // Catch an exception if any, an write it out to our logging mechanism, in addition to adding it our returnable message property _Msg += "Wrapper.Map Exception: " + ex.Message; ErrorReporting.WriteEm.WriteItem(ex, "o7th.Class.Library.Data.Wrapper.Map", _Msg); // make sure this method returns a default List return default(List); } } 

Nota: Este método é 63% mais lento do que usando trees de expressão …

Conforme observado nos comentários, o problema é que não existe coluna no leitor para a propriedade especificada. A ideia é fazer um loop pelos nomes das colunas do leitor primeiro e verificar se existe uma propriedade correspondente. Mas como obter a lista de nomes de colunas de antemão?

  1. Uma ideia é usar trees de expressão para construir a lista de nomes de coluna do leitor e verificar as propriedades da class. Algo assim

     var paramExp = Expression.Parameter(typeof(IDataRecord), "o7thDR"); var loopIncrementVariableExp = Expression.Parameter(typeof(int), "i"); var columnNamesExp = Expression.Parameter(typeof(List), "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 de

     List columnNames = new List(); 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, tornando inútil qualquer esforço adicional nessa linha. A tree de expressões acima irá buscar os nomes das colunas toda vez que o delegado final for chamado, o que no seu caso é para toda criação de objects, o que é contra o espírito de sua exigência.

  2. Outra abordagem é permitir que a class do conversor tenha um reconhecimento predefinido dos nomes de coluna para um determinado tipo, por meio de atributos ( veja um exemplo ) ou mantendo um dictionary estático como ( Dictionary> ). Embora dê mais flexibilidade, o outro lado é que sua consulta nem sempre precisa include todos os nomes de coluna de uma tabela, e qualquer reader[notInTheQueryButOnlyInTheTableColumn] resultaria em exceção.

  3. A melhor abordagem, como eu vejo, é buscar os nomes das colunas do object leitor, mas apenas uma vez. Eu rewriteei a coisa como:

     private static List columnNames; private static Action GetMapFunc() { var exps = new List(); 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>( 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)_convertActionMap.GetOrAdd( typeof(T), (t) => GetMapFunc()); } T result = new T(); _convertAction(dataReader, result); return result; } 

    Agora, isso levanta a questão: por que não passar o leitor de dados diretamente para o construtor? Isso seria melhor.

     private IDataReader dataReader; private Action GetMapFunc() { var exps = new List(); 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>( Expression.Block(exps), paramExp, targetExp).Compile(); } internal Converter(IDataReader dataReader) { this.dataReader = dataReader; _convertAction = (Action)_convertActionMap.GetOrAdd( typeof(T), (t) => GetMapFunc()); } internal T CreateItemFromRow() { T result = new T(); _convertAction(dataReader, result); return result; } 

    Chame como

     List list = new List(); var converter = new Converter(dr); while (dr.Read()) { var obj = converter.CreateItemFromRow(); list.Add(obj); } 

Há uma série de melhorias que posso sugerir, no entanto.

  1. O new T() genérico new T() você está chamando em CreateItemFromRow é mais lento, ele usa reflection nos bastidores . Você pode delegar essa parte para as trees de expressão, o que deve ser mais rápido

  2. No momento, a chamada GetProperty não diferencia maiúsculas de minúsculas, o que significa que os nomes das colunas terão que corresponder exatamente ao nome da propriedade. Eu faria caso insensível usando um desses Bindings.Flag .

  3. Eu não tenho certeza de tudo porque você está usando um ConcurrentDictionary como um mecanismo de cache aqui. Um campo estático em uma class genérica será exclusivo para cada T O campo genérico em si pode atuar como cache. Além disso, por que a parte Value de ConcurrentDictionary do tipo type é?

  4. Como eu disse anteriormente, não é melhor amarrar fortemente um tipo e os nomes das colunas (o que você está fazendo armazenando em cache um determinado delegado de Action por tipo ). Mesmo para o mesmo tipo, suas consultas podem ser diferentes, selecionando diferentes conjuntos de colunas. É melhor deixar para o leitor de dados decidir.

  5. Use Expression.Convert vez de Expression.TypeAs para conversão de tipo de valor do object .

  6. Observe também que o reader.GetOrdinal é uma maneira muito mais rápida de realizar pesquisas de leitores de dados.

Eu rewriteia a coisa toda como:

 readonly Func _converter; readonly IDataReader dataReader; private Func GetMapFunc() { var exps = new List(); 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>( Expression.Block(new[] { targetExp }, exps), paramExp).Compile(); } internal Converter(IDataReader dataReader) { this.dataReader = dataReader; _converter = GetMapFunc(); } internal T CreateItemFromRow() { return _converter(dataReader); }