Generischer DbDataReader zur Liste Kartierung

c# dbdatareader expression-trees linq-expressions reflection

Frage

Ich habe ein kleines Problem (mehr wie ein Ärgernis) mit meinen Eigentum verbindlichen Datenzugriffsklassen. Das Problem besteht darin, dass das Mapping fehlschlägt, wenn keine Spalte im Reader für die entsprechende Eigenschaft in der Klasse vorhanden ist.

Code

Hier ist die Mapper-Klasse:

// Map our datareader object to a strongly typed list
private static IList<T> Map<T>(DbDataReader dr) where T : new()
{
    try
    {
        // initialize our returnable list
        List<T> list = new List<T>();
        // fire up the lamda mapping
        var converter = new Converter<T>();
        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<T>);
    }
}

Konverterklasse:

/// <summary>
/// Converter class to convert returned Sql Records to strongly typed classes
/// </summary>
/// <typeparam name="T">Type of the object we'll convert too</typeparam>
internal class Converter<T> where T : new()
{
    // Concurrent Dictionay objects
    private static ConcurrentDictionary<Type, object> _convertActionMap = new ConcurrentDictionary<Type, object>();
    // Delegate action declaration
    private Action<IDataReader, T> _convertAction;

    // Build our mapping based on the properties in the class/type we've passed in to the class
    private static Action<IDataReader, T> GetMapFunc()
    {
        var exps = new List<Expression>();
        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<IDataReader, T>>(Expression.Block(exps), new[] { paramExp, targetExp }).Compile();
    }

    internal Converter()
    {
        // Fire off our mapping functionality
        _convertAction = (Action<IDataReader, T>)_convertActionMap.GetOrAdd(typeof(T), (t) => GetMapFunc());
    }

    internal T CreateItemFromRow(IDataReader dataReader)
    {
        T result = new T();
        _convertAction(dataReader, result);
        return result;
    }
}

Ausnahme

System.IndexOutOfRangeException {"Mileage"}

Stapelverfolgung

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

Frage

Wie kann ich es beheben, so dass es nicht fehlschlägt, wenn ich eine zusätzliche Eigenschaft habe, die der Leser möglicherweise nicht als Spalte hat und umgekehrt? Natürlich wäre das schnelle NULL As Mileage , einfach NULL As Mileage zu dieser Abfrage in Beispiel hinzuzufügen, dies ist jedoch keine Lösung für das Problem :)


Hier ist Map<T> mit Reflektion:

// Map our datareader object to a strongly typed list
private static IList<T> Map<T>(DbDataReader dr) where T : new()
{
    try
    {
        // initialize our returnable list
        List<T> list = new List<T>();
        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<T>);
    }
}

Hinweis: Diese Methode ist 63% langsamer als die Verwendung von Ausdrucksbäumen ...

Akzeptierte Antwort

Wie in den Kommentaren erwähnt, besteht das Problem darin, dass im Reader für die angegebene Eigenschaft keine Spalte vorhanden ist. Die Idee ist, zuerst nach den Spaltennamen des Lesers zu suchen und zu prüfen, ob eine passende Eigenschaft existiert. Aber wie bekommt man vorher die Liste der Spaltennamen?

  1. Eine Idee besteht darin, die Ausdrucksbäume selbst zu verwenden, um die Liste der Spaltennamen aus dem Lesegerät zu erstellen und sie mit Eigenschaften der Klasse zu vergleichen. Etwas wie das

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

    wäre das Äquivalent von

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

    Man kann mit dem endgültigen Ausdruck fortfahren, aber hier gibt es einen Haken, der jede weitere Anstrengung in dieser Richtung vergeblich macht. Der obige Ausdrucksbaum holt die Spaltennamen jedes Mal, wenn der letzte Delegierte aufgerufen wird, was in Ihrem Fall für jede Objekterstellung ist, was gegen den Geist Ihrer Anforderung ist.

  2. Ein anderer Ansatz besteht darin, der Konverterklasse eine vordefinierte Erkennung der Spaltennamen für einen bestimmten Typ zu geben, beispielsweise mittels Attributen ( siehe Beispiel ) oder durch Beibehaltung eines statischen Wörterbuchs ( Dictionary<Type, IEnumerable<string>> ). Obwohl es mehr Flexibilität bietet, ist die Umkehrung, dass Ihre Abfrage nicht immer alle Spaltennamen einer Tabelle enthalten muss, und jeder reader[notInTheQueryButOnlyInTheTableColumn] würde zu einer Ausnahme führen.

  3. Der beste Ansatz ist, wie ich sehe, die Spaltennamen aus dem Reader-Objekt zu holen, aber nur einmal. Ich würde das Ding neu schreiben wie:

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

    Jetzt stellt sich die Frage, warum man den Datenleser nicht direkt an den Konstruktor weitergibt. Das wäre besser.

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

    Nennen Sie es wie

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

Es gibt jedoch eine Reihe von Verbesserungen, die ich vorschlagen kann.

  1. Das generische new T() Sie in CreateItemFromRow ist langsamer, es verwendet Reflektion hinter den Kulissen . Sie können diesen Teil auch an Ausdrucksbäume delegieren, die schneller sein sollten

  2. GetProperty wird bei GetProperty call nicht zwischen Groß- und Kleinschreibung unterschieden, was bedeutet, dass Ihre Spaltennamen exakt mit dem Namen der Eigenschaft übereinstimmen müssen. Ich würde es Fallunempfindlich machen mit einem dieser Bindings.Flag .

  3. Ich bin mir überhaupt nicht sicher, warum Sie hier ein ConcurrentDictionary als Caching-Mechanismus verwenden. Ein statisches Feld in einer generischen Klasse <T> wird für jedes T eindeutig sein . Das generische Feld selbst kann als Cache dienen. Auch warum ist der Value Teil von ConcurrentDictionary des Typs object ?

  4. Wie ich bereits sagte, ist es nicht das Beste, einen Typ und die Spaltennamen stark zu verknüpfen (was Sie tun, indem Sie einen bestimmten Action Delegaten pro Typ zwischenspeichern ). Selbst für den gleichen Typ können Ihre Abfragen unterschiedlich sein und unterschiedliche Spalten auswählen. Es ist am besten, es dem Datenleser überlassen zu entscheiden.

  5. Verwenden Sie Expression.Convert anstelle von Expression.TypeAs für die Werttypkonvertierung aus dem object .

  6. Beachten Sie auch, dass reader.GetOrdinal viel schneller ist, Daten-Reader-Lookups durchzuführen.

Ich würde die ganze Sache neu schreiben wie:

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


Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow