Използвам библиотека на трета страна, която връща четец на данни. Бих искал прост и възможно най-общ начин да го преобразувам в списък с обекти.
Например, да кажем, че имам клас „Служител“ с 2 свойства EmployeeId и Име, бих искал четеца на данни (който съдържа списък със служители), който да бъде преобразуван в List‹ Employee>.
Предполагам, че нямам друг избор, освен да итерирам редовете на четеца на данни и за всеки от тях да ги конвертирам в обект Employee, който ще добавя към списък. Някакво по-добро решение? Използвам C# 3.5 и в идеалния случай бих искал да е възможно най-генеричен, така че да работи с всякакви класове (имената на полетата в DataReader съвпадат с имената на свойствата на различните обекти).
Преобразувайте редове от четец на данни във въведени резултати
Отговори (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);
IEnumerable
и след това може би да се обадя на ToList
.
- person Noldorin; 30.07.2009
Dispose()
ed след извикване на ToList() нали? Все още валидни ли са IDataRecords в моя списък‹IDataRecord›? Възможно ли е да пречат на правилното освобождаване на паметта? Дали те тиктакат бомби със закъснител и чакат боклукчия да дойде и да ги изтрие? Може би трябва да задам отделен въпрос точно за това. Благодаря.
- person user1325179; 26.09.2014
ToList()
, колкото защото без него кодът дава резултат на същия обект, който просто мутира с всяка итерация. Паметта за обекта в списък все още ще бъде налична след изхвърляне (изхвърлянето е за неуправлявани ресурси, а не за управлявана памет), но списъкът ще има един и същ обект за всеки запис. BuildObject()
се грижи за това.
- person Joel Coehoorn; 26.09.2014
yield
команди ще бъдат извикани и изразът using ще приключи, изхвърляйки SqlDataReader. Но предполагам, че ключовият момент е, че данните на SqlDataReader все още са налични дори след като са били изхвърлени. Никога не знаех това и току-що го потвърдих другаде. Благодаря ти. Така че извикването reader.Cast<System.Data.IDataRecord>().ToList()
добра идея ли е или има по-добър начин за конвертиране на данните от SqlDataReader в List‹IDataRecord›?
- person user1325179; 26.09.2014
GetEnumerator()
метод, който прави това доста остаряло. Много време можете просто да използвате това вместо това.
- person Joel Coehoorn; 26.09.2014
Можете да създадете метод за разширение като:
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();
Въпреки че не бих препоръчал това за производствен код, но можете да направите това автоматично, като използвате отражение и генерични:
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();
Приложихме следното решение и смятаме, че работи доста добре. Това е доста просто и изисква малко повече окабеляване от това, което картографът би направил. Въпреки това, понякога е хубаво да имате ръчно управление и честно казано, свързвате веднъж и сте готови.
Накратко: Нашите модели на домейн имплементират интерфейс, който има метод, който приема 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 */)
new()
като общо ограничение. Благодаря ви, че също го изразихте любезно и без заяждане и отрицателни гласове!
- person Airn5475; 26.09.2016
ЗАБЕЛЕЖКА: Това е код на .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>());
}
}
Най-простото решение:
var dt=new DataTable();
dt.Load(myDataReader);
list<DataRow> dr=dt.AsEnumerable().ToList();
След това ги изберете, за да ги съпоставите към всеки тип.
За .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;
}
}
}
}
Като магия
Аз лично МРАЗЯ да правя ръчно картографиране в конструкторите, също така не съм фен на правенето на собствено отражение. И така, ето още едно решение, предоставено с любезното съдействие на прекрасната (и доста повсеместна) 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);
Намерих това решение.
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;
}
}
Моята версия
Употреба:
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;
}