DbDataReader generico per la mapping Elenco

Sto avendo un leggero problema (più come un fastidio) con le mie classi di accesso ai dati vincolanti della proprietà. Il problema è che la mapping fallisce quando non esiste alcuna colonna nel lettore per la proprietà corrispondente in class.

Codice

Ecco la class del mapper:

// 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 del convertitore:

 ///  /// 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; } } 

Eccezione

 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 

Domanda

Come posso aggiustarlo, in modo che non fallisca quando ho una proprietà extra che il lettore potrebbe non avere come colonna e viceversa? Ovviamente il cerotto rapido sarebbe semplicemente aggiungere NULL As Mileage a questa query, ad esempio, tuttavia, questa non è una soluzione al problema 🙂


Ecco la Map utilizzando la riflessione:

 // 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: questo metodo è del 63% più lento rispetto all’utilizzo di alberi di espressione …

Come notato nei commenti, il problema è che non esiste alcuna colonna nel lettore per la proprietà specificata. L’idea è di eseguire il ciclo in base ai nomi delle colonne del lettore e controllare se esiste una proprietà corrispondente. Ma come si ottiene in anticipo l’elenco dei nomi delle colonne?

  1. Un’idea è di usare gli alberi di espressione per build l’elenco dei nomi delle colonne dal lettore e controllarlo rispetto alle proprietà della class. Qualcosa come questo

     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)); 

    sarebbe l’equivalente di

     List columnNames = new List(); for (int i = 0; i < reader.FieldCount; i++) { columnNames.Add(reader.GetName(i)); } 

    Si può continuare con l'espressione finale, ma c'è un problema qui che rende inutile ogni ulteriore sforzo lungo questa linea. L'albero delle espressioni sopra riportato recupererà i nomi delle colonne ogni volta che viene chiamato il delegato finale, che nel tuo caso è per ogni creazione di un object, il che è contrario allo spirito della tua esigenza.

  2. Un altro approccio consiste nel consentire alla class del convertitore di avere una consapevolezza predefinita dei nomi delle colonne per un determinato tipo, mediante attributi ( vedere per un esempio ) o mantenendo un dizionario statico come ( Dictionary> ). Sebbene offra maggiore flessibilità, il rovescio della medaglia è che la tua query non deve sempre includere tutti i nomi di colonna di una tabella, e qualsiasi reader[notInTheQueryButOnlyInTheTableColumn] risulterebbe in un'eccezione.

  3. L'approccio migliore, come si vede, è quello di recuperare i nomi delle colonne dall'object lettore, ma solo una volta. Vorrei riscrivere la cosa come:

     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; } 

    Ora che sorge la domanda perché non passare il lettore di dati direttamente al costruttore? Sarebbe meglio.

     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; } 

    Chiamalo come

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

Ci sono una serie di miglioramenti che posso suggerire, però.

  1. La new T() generica che stai chiamando in CreateItemFromRow è più lenta, utilizza la riflessione dietro le quinte . È ansible debind anche quella parte agli alberi di espressione, che dovrebbe essere più veloce

  2. Al momento, la chiamata a GetProperty non fa distinzione tra maiuscole e minuscole, pertanto i nomi delle colonne dovranno corrispondere esattamente al nome della proprietà. Lo renderei case insensitive usando uno di questi Bindings.Flag .

  3. Non sono affatto sicuro del motivo per cui stai usando ConcurrentDictionary come meccanismo di caching qui. Un campo statico in una class generica sarà univoco per ogni T Il campo generico stesso può agire come cache. Inoltre, perché la parte Value di ConcurrentDictionary di tipo object ?

  4. Come ho detto prima, non è il migliore per bind fortemente un tipo e i nomi delle colonne (che si stanno facendo memorizzando nella cache un particolare delegato di Action per tipo ). Anche per lo stesso tipo le tue query possono essere diverse selezionando diversi set di colonne. È meglio lasciarlo al lettore di dati per decidere.

  5. Utilizza Expression.Convert anziché Expression.TypeAs per la conversione del tipo di valore da object .

  6. Inoltre, reader.GetOrdinal è un modo molto più rapido per eseguire ricerche di lettori di dati.

Vorrei riscrivere il tutto come:

 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); }