Comme indiqué dans les commentaires, le problème est qu'il n'existe pas de colonne dans le lecteur pour la propriété spécifiée. L'idée est d'abord de boucler par les noms de colonne du lecteur et de vérifier si la propriété correspondante existe. Mais comment obtenir la liste des noms de colonne au préalable?
Une idée consiste à utiliser les arbres d'expression lui-même pour construire la liste des noms de colonnes du lecteur et le vérifier par rapport aux propriétés de la classe. Quelque chose comme ça
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));
serait l'équivalent de
List<string> columnNames = new List<string>(); for (int i = 0; i < reader.FieldCount; i++) { columnNames.Add(reader.GetName(i)); }
On peut continuer l'expression finale, mais il y a une prise ici qui fait d'autres efforts dans cette ligne futile. L'arbre d'expression ci-dessus récupérera les noms de colonne chaque fois que le délégué final est appelé dans votre cas pour chaque création d'objets, qui est contre l'esprit de votre exigence.
Une autre approche consiste à permettre à la classe de convertisseur d'avoir une conscience prédéfinie des noms de colonne pour un type donné, au moyen d'attributs (Voir pour un exemple) ou en maintenant un dictionnaire statique comme (
Dictionary<Type, IEnumerable<string>>
). Bien que cela donne plus de flexibilité, le revers de la médaille est que votre requête n'a pas toujours besoin d'inclure tous les noms de colonne d'une table et toutreader[notInTheQueryButOnlyInTheTableColumn]
entraînerait une exception.La meilleure approche telle que je vois est de récupérer les noms de colonne de l'objet lecteur, mais une seule fois. Je réécrit la chose comme:
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; }
Maintenant, cela soulève la question pourquoi ne pas passer le lecteur de données directement au constructeur? Ça serait mieux.
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; }
Appelez ça comme
List<T> list = new List<T>(); var converter = new Converter<T>(dr); while (dr.Read()) { var obj = converter.CreateItemFromRow(); list.Add(obj); }
Il y a cependant un certain nombre d'améliorations que je peux suggérer.
Le générique
new T()
Vous appelezCreateItemFromRow
est plus lent, il utilise la réflexion dans les coulisses. Vous pouvez également déléguer cette partie aux arbres d'expression qui devrait être plus rapideÀ l'heure actuelle
GetProperty
L'appel n'est pas insensible au cas, ce qui signifie que vos noms de colonne devront correspondre exactement au nom de la propriété. Je ferais un dossier insensible en utilisant l'un de cesBindings.Flag
.Je ne suis pas du tout sûr pourquoi vous utilisez un
ConcurrentDictionary
En tant que mécanisme de mise en cache ici. Un champ statique dans une classe générique<T>
sera unique pour chaqueT
. Le champ générique lui-même peut agir comme un cache. Aussi pourquoi leValue
partie deConcurrentDictionary
de typeobject
?Comme je l'ai dit plus tôt, ce n'est pas le meilleur à lier fortement un type et les noms de colonne (que vous faites en mettant en cache un particulier
Action
délégué par taper). Même pour le même type, vos requêtes peuvent être différentes en sélectionnant un ensemble différent de colonnes. Il est préférable de le laisser à Data Reader pour décider.Utilisation
Expression.Convert
à la place deExpression.TypeAs
pour la conversion de type de valeur deobject
.Notez également que Reader.getOndinal est un moyen beaucoup plus rapide d'effectuer des recherches de lecture de données.
Je réécrit le tout comme:
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);
}