Преобразование строк из средства чтения данных в типизированные результаты

Я использую стороннюю библиотеку, которая возвращает средство чтения данных. Мне нужен простой и как можно более общий способ преобразовать его в список объектов.
Например, скажем, у меня есть класс «Сотрудник» с 2 свойствами EmployeeId и Name, мне нужен считыватель данных (который содержит список сотрудников), который нужно преобразовать в List ‹Employee>.
Думаю, у меня нет другого выбора, кроме как перебирать строки средства чтения данных и для каждой из них преобразовывать их в объект Employee, который я добавлю в Список. Есть ли лучшее решение? Я использую C # 3.5, и в идеале я хотел бы, чтобы он был как можно более универсальным, чтобы он работал с любыми классами (имена полей в DataReader совпадают с именами свойств различных объектов).


person Anthony    schedule 29.07.2009    source источник
comment
Блин, если бы я был рядом с компилятором прямо сейчас, я бы с удовольствием написал этот код! Завтра я займусь этим, если меня никто не снесет. +1 вопрос.   -  person Matt Howells    schedule 30.07.2009
comment
@MattHowells, вы все еще можете написать это, лично я хотел бы увидеть, есть ли это что-то другое.   -  person nawfal    schedule 11.02.2013
comment
возможный дубликат stackoverflow .com / questions / 1464883 /.   -  person nawfal    schedule 11.02.2013


Ответы (10)


Вам действительно нужен список, или IEnumerable подойдет?

Я знаю, что вы хотите, чтобы он был универсальным, но гораздо более распространенным шаблоном является наличие статического метода Factory для целевого типа объекта, который принимает datarow (или IDataRecord). Это выглядело бы примерно так:

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }

    public static Employee Create(IDataRecord record)
    {
        return new Employee
        {
           Id = record["id"],
           Name = record["name"]
        };
    }
}

.

public IEnumerable<Employee> GetEmployees()
{
    using (var reader = YourLibraryFunction())
    {
       while (reader.Read())
       {
           yield return Employee.Create(reader);
       }
    }
}

Затем, если вам действительно нужен список, а не IEnumerable, вы можете вызвать .ToList() для результатов. Я полагаю, вы также можете использовать дженерики + делегат, чтобы сделать код для этого шаблона более пригодным для повторного использования.

Обновление. Сегодня я снова увидел это, и мне захотелось написать общий код:

public IEnumerable<T> GetData<T>(IDataReader reader, Func<IDataRecord, T> BuildObject)
{
    try
    {
        while (reader.Read())
        {
            yield return BuildObject(reader);
        }
    }
    finally
    {
         reader.Dispose();
    }
}

//call it like this:
var result = GetData(YourLibraryFunction(), Employee.Create);
person Joel Coehoorn    schedule 29.07.2009
comment
Я определенно придерживаюсь того же мнения относительно возврата IEnumerable, а затем, возможно, вызова ToList. - person Noldorin; 30.07.2009
comment
Это даже проще, чем я думал тогда: во многих случаях универсальный метод может определить тип на основе переданного ему делегата. - person Joel Coehoorn; 18.07.2012
comment
Поскольку yield return откладывает выполнение, обновленная версия у меня не сработала, потому что к моменту ее выполнения DataReader уже закрыт. Тем не менее, это по-прежнему хороший пример. - person Korey; 03.04.2013
comment
@Korey Я использовал этот шаблон в нескольких других местах здесь, в Stack Overflow, где разница в том, что вместо того, чтобы принимать datareader в качестве аргумента функции, я бы изменил код в том месте, где datareader был впервые создан, чтобы вернуть IEnumerable вместо datareader или чего-то еще. - person Joel Coehoorn; 03.04.2013
comment
@JoelCoehoorn, в моем случае использования я бы, вероятно, немедленно вызвал бы ToList (), но это заставляет меня задуматься: я в основном приводил SqlDataReader несколько раз в IDataRecords, но тогда SqlDataReader Dispose()ed после того, как ToList () вызывается правильно? Действительны ли IDataRecords в моем списке ‹IDataRecord›? Возможно, они мешают правильному освобождению памяти? Они ставят бомбы замедленного действия, ожидая, когда придет сборщик мусора и сотрет их? Может, именно по этому поводу стоит задать отдельный вопрос. Спасибо. - person user1325179; 26.09.2014
comment
@ user1325179 Это причина, по которой BuildObject Func включен в список аргументов. Эта функция создает новый объект BusinessLayer из IDataRecord, который не зависит от носителя данных. Это не важно из-за ToList(), а не из-за того, что без него код дает тот же объект, который только видоизменяется с каждой итерацией. Память для объекта в списке будет по-прежнему доступна после удаления (удаление касается неуправляемых ресурсов, а не управляемой памяти), но в списке будет один и тот же объект для каждой записи. BuildObject() позаботится об этом. - person Joel Coehoorn; 26.09.2014
comment
@JoelCoehoorn, я прокомментировал использование ToList (), поскольку в конечном итоге он вызовет Dispose () до того, как у меня появится возможность использовать любой из IDataRecords. Не правда ли? Но если мы можем передать данные другому объекту, как в случае с BuildObject, данные должны быть в безопасности, верно? При передаче потребуется скопировать значения, а не только ссылки на SqlDataReader. Так что, если вместо использования BuildObject Func после вызова ExecuteReader () я вызвал reader.Cast ‹System.Data.IDataRecord› (). ToList ()? Достаточно ли этого, чтобы отделить данные от считывателя, который будет удален? - person user1325179; 26.09.2014
comment
@ user1325179 Нет, это правда. Функция BuildObject будет вызываться для каждого элемента по мере того, как записи перечисляются в списке, до удаления загрузчика данных. Даже если этого не произошло, dispose уничтожает только неуправляемые ресурсы. Память, используемая устройством чтения данных, все еще там, включая конечное состояние полей, используемых IDataRecord. - person Joel Coehoorn; 26.09.2014
comment
@JoelCoehoorn, спасибо за терпение. Я понимаю, что BuildObject будет вызываться до Dispose, но после вызова ToList () будут вызваны все команды yield, и оператор using завершится, удалив SqlDataReader. Но я думаю, что ключевым моментом является то, что данные SqlDataReader все еще доступны даже после того, как они были удалены. Я никогда не знал этого, и я просто подтвердил это в другом месте. Спасибо. Итак, вызов reader.Cast<System.Data.IDataRecord>().ToList() - хорошая идея, или есть лучший способ преобразовать данные SqlDataReader в List ‹IDataRecord›? - person user1325179; 26.09.2014
comment
@ user1325179 Ключевым моментом здесь является то, что это не должно не создавать Список ‹IDataRecord›. Функция BuildObject () требуется для копирования данных в новый объект. Если вы этого не сделаете, тогда да: это не удастся. Кроме того, где-то по пути был GetEnumerator() метод, который сделал это в значительной степени устаревшим. В большинстве случаев вы можете просто использовать это вместо этого. - person Joel Coehoorn; 26.09.2014
comment
Давайте продолжим это обсуждение в чате. - person user1325179; 26.09.2014
comment
Я не могу согласиться с тем, что это хорошее решение. Типы возврата - это обещания вызывающему. IEnumerable ‹T› означает: способ получить T по запросу. Я интерпретирую по запросу, чтобы иметь в виду любое время, которое мне нравится, не обязательно все сразу. Однако - если вы попытаетесь выполнить перечисление в свое удовольствие, особенно после закрытия соединения, вы столкнетесь с проблемой. - person Kyle Pena; 23.04.2015
comment
@KylePena Можно написать методы БД, которые возвращают IEnumerable и поддерживают соединение открытым до тех пор, пока Enumerable не будет удален. - person Joel Coehoorn; 23.04.2015
comment
@JoelCoehoorn, я думаю, Id = record [id], надо преобразовать в int из объекта ?? иначе было бы исключение !! - person Shekhar Pankaj; 17.07.2015
comment
Отлично, потрясающе ... блестящее обновление, и думаю об общей версии ... Вы заслуживаете множества положительных отзывов +1 - person ThePravinDeshmukh; 02.09.2015
comment
Я думаю, что вызов Dispose для объекта, который вам был предоставлен в качестве параметра, - плохая идея, поскольку он скрывает от вызывающего абонента, что он больше не может использовать объект, который он вам дал. - person Richardissimo; 23.02.2018

Вы можете создать такой метод расширения, как:

public static List<T> ReadList<T>(this IDataReader reader, 
                                  Func<IDataRecord, T> generator) {
     var list = new List<T>();
     while (reader.Read())
         list.Add(generator(reader));
     return list;
}

и используйте это как:

var employeeList = reader.ReadList(x => new Employee {
                                               Name = x.GetString(0),
                                               Age = x.GetInt32(1)
                                        });

Предложение Джоэла хорошее. Вы можете выбрать возврат IEnumerable<T>. Приведенный выше код легко преобразовать:

public static IEnumerable<T> GetEnumerator<T>(this IDataReader reader, 
                                              Func<IDataRecord, T> generator) {
     while (reader.Read())
         yield return generator(reader);
}

Если вы хотите автоматически сопоставить столбцы со свойствами, идея кода такая же. Вы можете просто заменить функцию generator в приведенном выше коде функцией, которая опрашивает typeof(T) и устанавливает свойства объекта с помощью отражения, считывая соответствующий столбец. Однако я лично предпочитаю определять фабричный метод (например, тот, который упоминается в ответе Джоэла) и передавать его делегат в эту функцию:

 var list = dataReader.GetEnumerator(Employee.Create).ToList();
person mmx    schedule 29.07.2009
comment
0 проголосовать против Очень хороший ответ. Linq такой классный. Разве высокомерно чувствовать себя хорошо, потому что ты наконец-то можешь читать такой код и ценить его? - person rp.; 30.07.2009
comment
0 голосов против неуместны! Я проголосовал за это. Скопируйте и вставьте меня сюда дьяволы. - person rp.; 30.07.2009
comment
Когда я копирую и вставляю это, я получаю «тип или имя пространства имен« T »не может быть найдено». Возможно, я упускаю что-то очевидное. Идеи? - person Anthony; 30.07.2009
comment
Почему генератор? Вопрос указывает, что свойства отображаются в столбцы. - person Matt Howells; 30.07.2009
comment
Энтони: Ой. Я упустил общий аргумент. Фиксированный. - person mmx; 30.07.2009
comment
Мэтт: Я не понимаю вашего утверждения. Вы имеете в виду какую-то функцию, которая автоматически отображает столбцы в свойствах путем отражения или чего-то в этом роде? - person mmx; 30.07.2009

Хотя я бы не рекомендовал это для производственного кода, но вы можете сделать это автоматически, используя отражение и дженерики:

public static class DataRecordHelper
{
    public static void CreateRecord<T>(IDataRecord record, T myClass)
    {
        PropertyInfo[] propertyInfos = typeof(T).GetProperties();

        for (int i = 0; i < record.FieldCount; i++)
        {
            foreach (PropertyInfo propertyInfo in propertyInfos)
            {
                if (propertyInfo.Name == record.GetName(i))
                {
                    propertyInfo.SetValue(myClass, Convert.ChangeType(record.GetValue(i), record.GetFieldType(i)), null);
                    break;
                }
            }
        }
    }
}

public class Employee
{
    public int Id { get; set; }
    public string LastName { get; set; }
    public DateTime? BirthDate { get; set; }

    public static IDataReader GetEmployeesReader()
    {
        SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["NorthwindConnectionString"].ConnectionString);

        conn.Open();
        using (SqlCommand cmd = new SqlCommand("SELECT EmployeeID As Id, LastName, BirthDate FROM Employees"))
        {
            cmd.Connection = conn;
            return cmd.ExecuteReader(CommandBehavior.CloseConnection);
        }
    }

    public static IEnumerable GetEmployees()
    {
        IDataReader rdr = GetEmployeesReader();
        while (rdr.Read())
        {
            Employee emp = new Employee();
            DataRecordHelper.CreateRecord<Employee>(rdr, emp);

            yield return emp;
        }
    }
}

Затем вы можете использовать CreateRecord<T>() для создания экземпляра любого класса из полей в средстве чтения данных.

<asp:GridView ID="GvEmps" runat="server" AutoGenerateColumns="true"></asp:GridView>

GvEmps.DataSource = Employee.GetEmployees();
GvEmps.DataBind();
person Dan Diplo    schedule 29.07.2009
comment
Что бы вы не порекомендовали для производства? - person Anthony; 30.07.2009
comment
Потому что это неправильно. Автоматическая установка свойств из средства чтения данных означает, что у вас меньше контроля над проверкой ошибок, а использование отражения обходится дорого. Я не считаю его надежным, хотя он должен работать. Однако, если вы серьезно относитесь к использованию такой техники, вам лучше всего будет искать подходящее решение для сопоставления ORM, такое как LinqToSql, Entity Framwork, nHibernate и т. Д. - person Dan Diplo; 30.07.2009
comment
@DanDiplo именно так! это то, что предпочитает делать большинство картографов, и только мы, программисты, знаем, насколько это хрупко. мы должны думать об объектах предметной области, и метапрограммирование должно быть последним, к чему прибегать! - person nawfal; 05.02.2013
comment
Тем не менее, используйте кеширование, чтобы улучшить производительность отражения, как показано здесь основная идея состоит в том, чтобы не использовать отражение внутри цикла .. - person nawfal; 05.02.2013
comment
Я не уверен, что согласен с предложенной хрупкостью отражения. Я с большим успехом использовал его для этого приложения во многих производственных средах. Отражение, конечно, сопряжено с большими накладными расходами, но кеширование результата ворчливой работы может устранить многие из этих накладных расходов. Что касается автоматической установки свойств, я здесь тоже не согласен. Предположим, вы используете дженерики и проходите через объект, вы можете использовать отражение для вызова определенного конструктора и т. Д. - person pim; 27.02.2016

Мы реализовали следующее решение и считаем, что оно работает очень хорошо. Это довольно просто и требует немного большего количества подключений, чем то, что сделал бы картограф. Однако иногда приятно иметь ручное управление, и, честно говоря, вы подключаетесь один раз, и все готово.

В двух словах: наши модели предметной области реализуют интерфейс, в котором есть метод, который принимает IDataReader и заполняет из него свойства модели. Затем мы используем Generics и Reflection, чтобы создать экземпляр модели и вызвать на нем метод Parse.

Мы рассматривали возможность использования конструктора и передачи ему IDataReader, но выполненные нами базовые проверки производительности, похоже, показали, что интерфейс был последовательно быстрее (хотя бы немного). Кроме того, маршрут интерфейса обеспечивает мгновенную обратную связь через ошибки компиляции.

Мне нравится то, что вы можете использовать private set для таких свойств, как Age в приведенном ниже примере, и устанавливать их прямо из базы данных.

public interface IDataReaderParser
{
    void Parse(IDataReader reader);
}

public class Foo : IDataReaderParser
{
    public string Name { get; set; }
    public int Age { get; private set; }

    public void Parse(IDataReader reader)
    {
        Name = reader["Name"] as string;
        Age = Convert.ToInt32(reader["Age"]);
    }
}

public class DataLoader
{
    public static IEnumerable<TEntity> GetRecords<TEntity>(string connectionStringName, string storedProcedureName, IEnumerable<SqlParameter> parameters = null)
                where TEntity : IDataReaderParser, new()
    {
        using (var sqlCommand = new SqlCommand(storedProcedureName, Connections.GetSqlConnection(connectionStringName)))
        {
            using (sqlCommand.Connection)
            {
                sqlCommand.CommandType = CommandType.StoredProcedure;
                AssignParameters(parameters, sqlCommand);
                sqlCommand.Connection.Open();

                using (var sqlDataReader = sqlCommand.ExecuteReader())
                {
                    while (sqlDataReader.Read())
                    {
                        //Create an instance and parse the reader to set the properties
                        var entity = new TEntity();
                        entity.Parse(sqlDataReader);
                        yield return entity;
                    }
                }
            }
        }
    }
}

Чтобы вызвать его, вы просто указываете параметр типа

IEnumerable<Foo> foos = DataLoader.GetRecords<Foo>(/* params */)
person Airn5475    schedule 13.09.2016
comment
Мне нравится решение с интерфейсом. Может быть, было бы неплохо использовать new () в качестве предложения where для T. Так что вам не нужно никакого отражения, такого как класс Activator. - person Sebi; 23.09.2016
comment
@Sebi, спасибо за отличное предложение! Как-то я забыл об этой способности с дженериками! Обратите внимание, что вам нужно добавить new() в качестве общего ограничения. Спасибо за то, что вы также выразили это любезно и без язв и отрицательных голосов! - person Airn5475; 26.09.2016
comment
@ Aim5475 Спасибо за ответ, но разве он не очень похож на этот? stackoverflow.com/a/1202973/15928 - person Anthony; 28.09.2016
comment
@Anthony Я думаю, что согласен с вами после того, как снова более внимательно посмотрел на «Обновление», так что никакого неуважения к этому решению. IMO, я чувствую, что мой немного чище, потому что мне не нужно передавать две функции методу GetRecords, только параметр типа. #moreThanOneWayToSkinACat - person Airn5475; 28.09.2016
comment
Это решение еще действует! Просто попробовал с .NET Core 3.1, и гораздо проще сначала разделить и повторно использовать как для БД, так и для кода по сравнению с первым ответом с двумя функциями - person Philip; 06.05.2020
comment
@ Airn5475 Можно ли интегрировать задачу async / await для одного и того же решения? Будет ли польза? - person Philip; 11.05.2020

ПРИМЕЧАНИЕ. Это код .NET Core.

Тупо производительный вариант, если вы не возражаете против внешней зависимости (замечательный пакет Fast Member nuget):

public static T ConvertToObject<T>(this SqlDataReader rd) where T : class, new()
{

    Type type = typeof(T);
    var accessor = TypeAccessor.Create(type);
    var members = accessor.GetMembers();
    var t = new T();

    for (int i = 0; i < rd.FieldCount; i++)
    {
        if (!rd.IsDBNull(i))
        {
            string fieldName = rd.GetName(i);

            if (members.Any(m => string.Equals(m.Name, fieldName, StringComparison.OrdinalIgnoreCase)))
            {
                accessor[t, fieldName] = rd.GetValue(i);
            }
        }
    }

    return t;
}

Использовать:

public IEnumerable<T> GetResults<T>(SqlDataReader dr) where T : class, new()
{
    while (dr.Read())
    {
        yield return dr.ConvertToObject<T>());
    }
}
person pim    schedule 19.06.2017

Самое простое решение:

var dt=new DataTable();
dt.Load(myDataReader);
list<DataRow> dr=dt.AsEnumerable().ToList();

Затем выберите их, чтобы сопоставить с любым типом.

person Mohsen    schedule 10.09.2017

Для .NET Core 2.0:

Вот метод расширения, который работает с .NET CORE 2.0 для выполнения RAW SQL и отображения результатов в СПИСОК произвольных типов:

ИСПОЛЬЗОВАНИЕ:

 var theViewModel = new List();
 string theQuery = @"SELECT * FROM dbo.Something";
 theViewModel = DataSQLHelper.ExecSQL(theQuery,_context);

 using Microsoft.EntityFrameworkCore;
 using System.Data;
 using System.Data.SqlClient;
 using System.Reflection;

public static List ExecSQL(string query, myDBcontext context)
 {
 using (context)
 {
 using (var command = context.Database.GetDbConnection().CreateCommand())
 {
 command.CommandText = query;
 command.CommandType = CommandType.Text;
 context.Database.OpenConnection();
                using (var result = command.ExecuteReader())
                {
                    List<T> list = new List<T>();
                    T obj = default(T);
                    while (result.Read())
                    {
                        obj = Activator.CreateInstance<T>();
                        foreach (PropertyInfo prop in obj.GetType().GetProperties())
                        {
                            if (!object.Equals(result[prop.Name], DBNull.Value))
                            {
                                prop.SetValue(obj, result[prop.Name], null);
                            }
                        }
                        list.Add(obj);
                    }
                    return list;

                }
            }
        }
    }
person Tony Bourdeaux    schedule 21.09.2017

Как Magic

Лично я НЕНАВИЖУ делать отображение вручную в конструкторах, я также не фанат создания собственного отражения. Итак, вот еще одно решение, любезно предоставленное замечательной (и довольно повсеместной) библиотекой Newtonsoft JSON.

Он будет работать только в том случае, если имена ваших свойств в точности совпадают с именами столбцов в средстве чтения данных, но у нас это сработало.

... предполагается, что у вас есть имя чтения данных "yourDataReader" ...

        var dt = new DataTable();
        dt.Load(yourDataReader);
        // creates a json array of objects
        string json = Newtonsoft.Json.JsonConvert.SerializeObject(dt);
        // this is what you're looking for right??
        List<YourEntityType> list = 
Newtonsoft.Json.JsonConvert
.DeserializeObject<List<YourEntityType>>(json);
person C.List    schedule 09.10.2019

Я нашел это решение.

var cmd = ctx.Connection.CreateCommand();

T result = DbDataReaderdHelper.Fill<T>(cmd)

public static class DbDataReaderdHelper
{
    public static List<T> Fill<T>(DbCommand dbCommand) where T : new()
    {
        List<T> result = new List<T>();
        var reader = dbCommand.ExecuteReader();

        if (reader.HasRows)
        {
            while (reader.Read())
            {
                Type type = typeof(T);
                T obj = (T)Activator.CreateInstance(type);
                PropertyInfo[] properties = type.GetProperties();

                foreach (PropertyInfo property in properties)
                {
                    var value = reader[property.Name];

                    try
                    {
                        if (value != null)
                        {
                            var convertedValue = TypeDescriptor.GetConverter(property.PropertyType).ConvertFromInvariantString(value.ToString());

                            property.SetValue(obj, convertedValue);
                        }
                    }
                    catch {}
                }
                result.Add(obj);
            }
        }

        reader.Close();

        return result;
    }
}
person ccassob    schedule 23.07.2020

Моя версия

Использование:

var Q = await Reader.GetTable<DbRoom>("SELECT id, name FROM rooms");

PgRoom - это

public class DbRoom
{
    [Column("id")]
    public int Id { get; set; }

    [Column("name")]
    public string Name { get; set; }
}

Reader.GetTable содержит:

            using (var R = await Q.ExecuteReaderAsync())
            {
                List<T> Result = new List<T>();

                Dictionary<int, PropertyInfo> Props = new Dictionary<int, PropertyInfo>();

                foreach (var p in typeof(T).GetProperties())
                {
                    for (int i = 0; i < R.FieldCount; i++)
                    {
                        if (p.GetCustomAttributes<ColumnAttribute>().FirstOrDefault(t => t.Name == R.GetName(i)) != null
                            && p.PropertyType == R.GetFieldType(i))
                        {
                            Props.Add(i, p);
                        }
                    }
                }

                while (await R.ReadAsync())
                {
                    T row = new T();

                    foreach (var kvp in Props)
                    {
                        kvp.Value.SetValue(row, R[kvp.Key]);
                    }

                    Result.Add(row);
                }

                return Result;
            }
person Алексей Телятников    schedule 28.08.2020