Преобразувайте редове от четец на данни във въведени резултати

Използвам библиотека на трета страна, която връща четец на данни. Бих искал прост и възможно най-общ начин да го преобразувам в списък с обекти.
Например, да кажем, че имам клас „Служител“ с 2 свойства EmployeeId и Име, бих искал четеца на данни (който съдържа списък със служители), който да бъде преобразуван в 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
Тъй като връщането на доходност отлага изпълнението, актуализираната версия не работи за мен, защото докато се изпълни, DataReader вече е затворен. Все пак е добър пример. - person Korey; 03.04.2013
comment
@Korey Използвал съм този модел на няколко други места тук в Stack Overflow, където разликата е, че вместо да приема четец на данни като аргумент на функция, бих променил кода на мястото, където първо е създаден четецът на данни, за върнете 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 Възможно е да се напишат DB методи, които връщат 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 и е много по-лесно да се отделят и използват повторно както за DB, така и за код първо в сравнение с първия отговор с две функции - 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

Като магия

Аз лично МРАЗЯ да правя ръчно картографиране в конструкторите, също така не съм фен на правенето на собствено отражение. И така, ето още едно решение, предоставено с любезното съдействие на прекрасната (и доста повсеместна) Newtonsoft JSON lib.

Ще работи само ако имената на вашите свойства съвпадат точно с имената на колоните на четеца на данни, но работи добре за нас.

...предполага, че имате име на четец на данни "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