データ リーダーを返すサード パーティのライブラリを使用しています。それをオブジェクトのリストに変換するための簡単な方法とできるだけ一般的な方法が必要です。
たとえば、EmployeeId と Name の 2 つのプロパティを持つ 'Employee' クラスがあるとします。データ リーダー (従業員のリストを含む) を List< Employee> に変換したいと考えています。
データ リーダーの行を繰り返し処理し、それぞれをリストに追加する Employee オブジェクトに変換する以外に選択肢はないと思います。より良い解決策はありますか?私は C# 3.5 を使用していますが、理想的には、どのクラスでも動作するように可能な限りジェネリックにしたいと考えています (DataReader のフィールド名は、さまざまなオブジェクトのプロパティ名と一致します)。
11 に答える
本当にリストが必要ですか、それとも IEnumerable で十分でしょうか?
ジェネリックにする必要があることは承知していますが、より一般的なパターンは、データ行 (または IDataRecord) を受け入れるターゲット オブジェクト型に静的 Factory メソッドを設定することです。それは次のようになります。
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);
次のような拡張メソッドを構築できます。
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)
、一致した列を読み取ることにより、リフレクションを使用してオブジェクトのプロパティを調べて設定する関数に置き換えることができます。ただし、個人的には、ファクトリ メソッド (Joel の回答で言及されているものなど) を定義し、そのデリゲートをこの関数に渡すことを好みます。
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
が、行った基本的なパフォーマンス チェックでは、インターフェイスが一貫して高速であることが示唆されたようです (少しだけではありますが)。また、インターフェイス ルートは、コンパイル エラーを介して即座にフィードバックを提供します。
私が気に入っていることの 1 つは、以下の例の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 */)
魔法のように
私は個人的にコンストラクターで手動マッピングを行うのが嫌いです。また、自分でリフレクションを行うのも好きではありません。そこで、素晴らしい (そしてかなりユビキタスな) Newtonsoft JSON lib の好意による別のソリューションを次に示します。
プロパティ名が datareader 列名と正確に一致する場合にのみ機能しますが、私たちにとってはうまく機能しました。
...データリーダー名が「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);