DbDataReader genérico a la lista cartografía

c# dbdatareader expression-trees linq-expressions reflection

Pregunta

Estoy teniendo un pequeño problema (más como una molestia) con mis clases de acceso a datos vinculantes de propiedad. El problema es que la asignación falla cuando no existe una columna en el lector para la propiedad correspondiente en la clase.

Código

Aquí está la clase de mapeador:

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

Clase de convertidor:

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

Excepción

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

Pregunta

¿Cómo puedo solucionarlo para que no falle cuando tengo una propiedad adicional que el lector no puede tener como columna y viceversa? Por supuesto, la ayuda de banda rápida sería simplemente agregar NULL As Mileage a esta consulta en el ejemplo, sin embargo, esto no es una solución al problema :)


Aquí está el Map<T> usando la reflexión:

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

Nota: este método es un 63% más lento que usar árboles de expresiones ...

Respuesta aceptada

Como se señaló en los comentarios, el problema es que no existe una columna en el lector para la propiedad especificada. La idea es recorrer primero los nombres de las columnas del lector y verificar si existe una propiedad coincidente. Pero, ¿cómo se obtiene de antemano la lista de nombres de columna?

  1. Una idea es utilizar los propios árboles de expresión para construir la lista de nombres de columna del lector y compararla con las propiedades de la clase. Algo como esto

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

    sería el equivalente de

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

    Uno puede continuar con la expresión final, pero aquí hay un problema que hace que cualquier esfuerzo adicional sea en vano. El árbol de expresión anterior buscará los nombres de las columnas cada vez que se llame al delegado final, lo que en su caso es para cada creación de objeto, lo que va en contra del espíritu de su requerimiento.

  2. Otro enfoque es permitir que la clase convertidora tenga un conocimiento predefinido de los nombres de columna para un tipo dado, por medio de atributos ( vea un ejemplo ) o manteniendo un diccionario estático como ( Dictionary<Type, IEnumerable<string>> ). Aunque ofrece más flexibilidad, la otra cara es que su consulta no siempre debe incluir todos los nombres de columna de una tabla, y cualquier reader[notInTheQueryButOnlyInTheTableColumn] resultará en una excepción.

  3. El mejor enfoque que veo es recuperar los nombres de columna del objeto lector, pero solo una vez. Me gustaría volver a escribir la cosa como:

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

    Ahora que plantea la pregunta ¿por qué no pasar el lector de datos directamente al constructor? Eso estaría mejor.

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

    Llamalo como

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

Sin embargo, hay una serie de mejoras que puedo sugerir.

  1. El new T() genérico new T() que está llamando en CreateItemFromRow es más lento, usa el reflejo detrás de escena . Puede delegar esa parte en los árboles de expresiones, lo que debería ser más rápido

  2. En este momento, la llamada a GetProperty no distingue entre mayúsculas y minúsculas, lo que significa que los nombres de sus columnas deberán coincidir exactamente con el nombre de la propiedad. Lo haría insensible a mayúsculas y minúsculas utilizando uno de esos Bindings.Flag .

  3. No estoy seguro de por qué está utilizando un ConcurrentDictionary como mecanismo de almacenamiento en caché aquí. Un campo estático en una clase genérica <T> será único para cada T El campo genérico en sí puede actuar como caché. Además, ¿por qué la parte Value de ConcurrentDictionary de tipo object ?

  4. Como dije anteriormente, no es lo mejor vincular fuertemente un tipo y los nombres de columna (lo que está haciendo almacenando en caché un delegado de Action particular por tipo ). Incluso para el mismo tipo, sus consultas pueden ser diferentes seleccionando diferentes conjuntos de columnas. Es mejor dejar que el lector de datos decida.

  5. Use Expression.Convert lugar de Expression.TypeAs para la conversión del tipo de valor desde el object .

  6. También tenga en cuenta que reader.GetOrdinal es una forma mucho más rápida de realizar búsquedas de lectores de datos.

Reescribiría todo el asunto como:

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


Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow