Come notato nei commenti, il problema è che non esiste una colonna nel lettore per la proprietà specificata. L'idea è quella di fare il giro dei nomi delle colonne del lettore e verificare se esiste la proprietà corrispondente. Ma come si ottiene prima l'elenco dei nomi delle colonne?
Un'idea è quella di usare gli alberi di espressione stessa per creare l'elenco dei nomi delle colonne dal lettore e verificarlo rispetto alle proprietà della classe. Qualcosa come questo
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));
sarebbe l'equivalente di
List<string> columnNames = new List<string>(); for (int i = 0; i < reader.FieldCount; i++) { columnNames.Add(reader.GetName(i)); }
Si può continuare con l'espressione finale, ma c'è una presa qui che fa ulteriori sforzi lungo questa linea inutile. L'albero di espressione di cui sopra riprenderà i nomi delle colonne ogni volta che il delegato finale viene chiamato che nel tuo caso è per ogni creazione di oggetti, che è contro lo spirito del tuo requisito.
Un altro approccio è lasciare che la classe del convertitore abbia una consapevolezza predefinita dei nomi delle colonne per un determinato tipo, mediante attributi (vedere un esempio) o mantenendo un dizionario statico come (
Dictionary<Type, IEnumerable<string>>
). Sebbene fornisca una maggiore flessibilità, il rovescio della medaglia è che la tua query non deve sempre includere tutti i nomi di colonne di una tabella e qualsiasi altrareader[notInTheQueryButOnlyInTheTableColumn]
comporterebbe un'eccezione.L'approccio migliore come vedo io è prendere i nomi delle colonne dall'oggetto Reader, ma solo una volta. Vorrei riscrivere la cosa come:
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; }
Ora questo pone la domanda perché non passare il lettore di dati direttamente al costruttore? Sarebbe meglio.
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; }
Chiamalo come
List<T> list = new List<T>(); var converter = new Converter<T>(dr); while (dr.Read()) { var obj = converter.CreateItemFromRow(); list.Add(obj); }
Tuttavia, ci sono una serie di miglioramenti che posso suggerire.
Il generico
new T()
Stai chiamandoCreateItemFromRow
è più lento, Usa il riflesso dietro le quinte. Puoi delegare anche quella parte per espressione degli alberi che dovrebbe essere più veloceProprio adesso
GetProperty
La chiamata non è insensibile al caso, il che significa che i nomi delle colonne dovranno corrispondere esattamente al nome della proprietà. Lo renderei insensibile al caso usando uno di quelliBindings.Flag
.Non sono affatto sicuro del perché stai usando un
ConcurrentDictionary
Come meccanismo di memorizzazione nella cache qui. Un campo statico in una classe generica<T>
sarà unico per ogniT
. Il campo generico stesso può fungere da cache. Anche perché è ilValue
parte diConcurrentDictionary
di tipoobject
?Come ho detto prima, non è il migliore per legare fortemente un tipo e i nomi delle colonne (cosa che stai facendo cachinando un particolare
Action
delegato per genere). Anche per lo stesso tipo, le tue query possono essere diverse selezionando diverse set di colonne. È meglio lasciarlo al lettore di dati per decidere.Uso
Expression.Convert
invece diExpression.TypeAs
per la conversione del tipo di valore daobject
.Nota anche che Reader.GetOrdinal è un modo molto più veloce per eseguire le ricerche del lettore di dati.
Riscriverei tutto come:
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);
}