以通用方式从 SqlDataReader 填充对象列表






4.89/5 (28投票s)
本文将介绍如何通过动态创建 lambda 表达式来将 SqlDataReader 对象中的数据填充到特定类类型的列表中。
引言
在创建 Web 应用程序时,有一个非常常见的做法是创建一个“列表”页面,向用户显示对象列表。用户将点击列表中某个项的链接,以打开一个“详细信息”页面。这是大多数人都经常编写的标准做法。我们面临的挑战是动态创建这些对象列表来填充我们的表格。
我们都曾不止一次地写过以下类型的代码
List<Models.Employee> result = new List<Models.Employee>();
while (reader.Read())
{
Models.Employee item = new Models.Employee()
{
EmployeeID = (int)reader["EmployeeID"],
FirstName = reader["FirstName"].ToString(),
LastName = reader["LastName"].ToString(),
Title = reader["Title"].ToString(),
BirthDate = (reader["BirthDate"] == DBNull.Value ? (DateTime?)null : (DateTime?)reader["BirthDate"]),
HireDate = (reader["HireDate"] == DBNull.Value ? (DateTime?)null : (DateTime?)reader["HireDate"]),
City = reader["City"].ToString()
};
result.Add(item);
}
背景
虽然上述代码和结果没有问题,但我必须承认我有点懒,不想为项目中的每个类都编写相同的代码。这就是我想要一个“通用”列表填充器的原因。我想创建一个类,它可以在不影响性能和可伸缩性的前提下,填充任何类型的数据列表。因此,这个类的要求是:
- 使用 C# 中的模型来实现通用('<T>')功能
- 性能应与使用 DataTable.Load 函数填充 DataTable 对象相似
- 列表项的填充应为类型安全
- 列表项的填充应允许设置默认值
- 列表项的填充应允许“可空”数据元素
在我研究的过程中,我发现 CodeProject 上的一篇题为《反射优化技术》的文章为我提供了帮助,它指明了正确的方向。但不幸的是,它没有提供我所需的所有内容,并且是针对 .NET Framework 4.0 的(我的公司目前的目标是 3.5 版本)。因此,我利用这些信息继续进行实验,并深入研究 MSDN,直到获得了最终结果:GenericPopulator<T>
在本文中,我将使用 Northwind SQL Server 示例数据库中的 Employee 表。除了创建数据库之外,所有代码都在下载包中提供。
使用 GenericPopulator 类
动态列表填充的使用非常简单。基本上需要三个步骤:
- 创建一个代表列表中单个项的数据模型
- 执行一个查询,查询结果将用于填充您的数据模型的
List<T>
- 实例化
GenericPopulator
类并调用CreateList()
函数
这是我的 Employee 数据模型。正如您所见,这个数据模型没有什么特别之处。它允许测试各种数据类型,以及可空数据类型。
[Serializable]
public class Employee
{
public int EmployeeID { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public string Title { get; set; }
public DateTime? BirthDate { get; set; }
public DateTime? HireDate { get; set; }
public string City { get; set; }
}
基于此数据模型执行查询并填充列表非常简单。
var query = "select * from Employees order by LastName, FirstName";
var cmd = new SqlCommand(query, conn);
using (var reader = cmd.ExecuteReader())
{
var employeeList = new Classes.GenericPopulator().CreateList(reader);
rptEmployees.DataSource = employeeList;
rptEmployees.DataBind();
reader.Close();
}
GenericPopulator 类详解
为了动态填充一个类,您必须使用反射来确定该类中的各种属性。一个使用反射来填充列表的简单函数可能如下所示:
public class ReflectionPopulator<T>
{
public virtual List<T> CreateList(SqlDataReader reader)
{
var results = new List<T>();
var properties = typeof(T).GetProperties();
while (reader.Read())
{
var item = Activator.CreateInstance<T>();
foreach (var property in typeof(T).GetProperties())
{
if (!reader.IsDBNull(reader.GetOrdinal(property.Name)))
{
Type convertTo = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
property.SetValue(item, Convert.ChangeType(reader[property.Name], convertTo), null);
}
}
results.Add(item);
}
return results;
}
}
不幸的是,使用反射动态填充类数据可能会非常耗时——特别是如果您要填充一个“宽”类(具有许多属性的类)。通过使用反射动态构建一个 Lambda 函数,我们可以显着提高性能,该函数将用于高效地将数据模型与数据库中的数据进行填充。生成的 CreateList()
函数出现在以下代码片段中。
public virtual List<T> CreateList(SqlDataReader reader)
{
var results = new List<T>();
Func<SqlDataReader, T> readRow = this.GetReader(reader);
while (reader.Read())
results.Add(readRow(reader));
return results;
}
正如您所见,我们调用了一个 GetReader 函数,它将返回一个 Lambda 函数,该函数接受一个 SqlDataReader 作为输入参数,并返回一个 Type <T>
的实例。在 GetReader
函数中将使用反射来构建生成的函数。生成的 Lambda 能够高效地填充即使是具有大量属性的类,从而显着提高性能,优于上面所示的纯反射方法。
GetReader 和动态 Lambda 表达式
最终,GetReader
函数需要创建一个 lambda 函数来执行本文第一个代码片段所示的任务。为此,我们将使用反射、SqlDataReader 本身以及动态 lambda 表达式(System.Linq.Expressions
)来构建一个如下所示的 lambda:
reader => new Employee() {
EmployeeID = IIF((System.DBNull.Value != reader.GetValue(0)), Convert(reader.GetValue(0)), Convert(0)),
LastName = IIF((System.DBNull.Value != reader.GetValue(1)), Convert(reader.GetValue(1)), Convert("")),
FirstName = IIF((System.DBNull.Value != reader.GetValue(2)), Convert(reader.GetValue(2)), Convert("")),
Title = IIF((System.DBNull.Value != reader.GetValue(3)), Convert(reader.GetValue(3)), Convert("")),
BirthDate = IIF((System.DBNull.Value != reader.GetValue(5)), Convert(reader.GetValue(5)), Convert(null)),
HireDate = IIF((System.DBNull.Value != reader.GetValue(6)), Convert(reader.GetValue(6)), Convert(null)),
City = IIF((System.DBNull.Value != reader.GetValue(8)), Convert(reader.GetValue(8)), Convert(""))
}
我们的 GetReader
函数的签名如下:
private Func<SqlDataReader, T> GetReader(SqlDataReader reader)
步骤 1 - 获取列列表
函数的第一步是获取 SqlDataReader
输入参数中的列列表。我们的类可能存在一些属性,而这些属性在查询中并不是数据元素。获取数据集中的列列表将使我们能够快速跳过类中不在 reader 中的任何属性。
List<string> readerColumns = new List<string>();
for (int index = 0; index < reader.FieldCount; index++)
readerColumns.Add(reader.GetName(index));
步骤 2 - 设置输入参数表达式
我们的 lambda 函数接受一个 SqlDataReader
输入参数。我们需要创建一个 ParameterExpression
来引用我们的 SqlDataReader
。通过反射,我们还可以获取 SqlDataReader
类中的 GetValue
方法的 MethodInfo
。稍后在创建类属性与数据集中值之间的绑定时将使用此信息。
var readerParam = Expression.Parameter(typeof(SqlDataReader), "reader");
var readerGetValue = typeof(SqlDataReader).GetMethod("GetValue");
步骤 3 - 设置 DBNull 检查
此代码部分有助于我们满足将属性设置为其默认值的要求。在创建绑定时,我们将属性值设置为数据集中的数据,或者设置为属性的默认值,具体取决于数据集值是否为 null。这也帮助我们确保数据集中 null 值不会导致错误。
我们使用反射来获取 DBNull.Value
字段的 FieldInfo
。然后,我们创建一个 FieldExpression 来引用我们输入 SqlDataReader 参数上的该字段。
var dbNullValue = typeof(System.DBNull).GetField("Value");
var dbNullExp = Expression.Field(Expression.Parameter(typeof(System.DBNull), "System.DBNull"), dbNullValue);
步骤 4 - 为每个属性创建 MemberBinding 表达式列表
MemberBinding
是一种 Linq.Expression
类型,它用于将成员属性设置为某个值。我们将遍历类(由 T 定义)的属性,以确定需要多少个 MemberBinding 元素以及它们的外观。循环的处理流程如下:
- 循环遍历
Class<T>
中的每个属性 - 如果在数据集中找到该属性作为列
- 确定数据集中该列的索引
- 创建一个 Call 表达式,它引用数据集中相应的列
- 创建一个 NotEqual 表达式来封装将属性设置为数据集列值的过程
- 创建一个表达式,用于在测试为 True 时将属性设置为数据集中列的值
- 创建一个表达式,用于在测试为 False 时将属性设置为默认值
- 创建实际的
MemberBinding
表达式并将其添加到集合中
List<MemberBinding> memberBindings = new List<MemberBinding>();
foreach (var prop in typeof(T).GetProperties())
{
if (readerColumns.Contains(prop.Name))
{
// determine the default value of the property
object defaultValue = null;
if (prop.PropertyType.IsValueType)
defaultValue = Activator.CreateInstance(prop.PropertyType);
else if (prop.PropertyType.Name.ToLower().Equals("string"))
defaultValue = string.Empty;
// build the Call expression to retrieve the data value from the reader
var indexExpression = Expression.Constant(reader.GetOrdinal(prop.Name));
var getValueExp = Expression.Call(readerParam, readerGetValue, new Expression[] { indexExpression });
// create the conditional expression to make sure the reader value != DBNull.Value
var testExp = Expression.NotEqual(dbNullExp, getValueExp);
var ifTrue = Expression.Convert(getValueExp, prop.PropertyType);
var ifFalse = Expression.Convert(Expression.Constant(defaultValue), prop.PropertyType);
// create the actual Bind expression to bind the value from the reader to the property value
MemberInfo mi = typeof(T).GetMember(prop.Name)[0];
MemberBinding mb = Expression.Bind(mi, Expression.Condition(testExp, ifTrue, ifFalse));
memberBindings.Add(mb);
}
}
步骤 5 - 创建和编译 Lambda 函数
函数的最后一部分将执行以下步骤:
- 创建一个 New 表达式,用于创建类的实例
- 创建一个
MemberInit
表达式——将新实例创建与我们在属性循环中创建的MemberBindings
集合结合起来。这两行代码生成了实际所需的 lambda 表达式 - 创建并编译所需的 Lambda 函数
- 将函数委托返回给调用函数
var newItem = Expression.New(typeof(T));
var memberInit = Expression.MemberInit(newItem, memberBindings);
var lambda = Expression.Lambda<Func<SqlDataReader, T>>(memberInit, new ParameterExpression[] { readerParam });
resDelegate = lambda.Compile();
return (Func<SqlDataReader, T>)resDelegate;
关注点
未包含性能指标
虽然我没有在此包含任何性能指标,但我已经对各种大小的数据集进行了测试。我的性能测试结果在大多数情况下都超出了预期——事实上,有时甚至优于 DataTable.Load()
选项。在小型数据集上可以看到性能的“下降”(从大约 2 毫秒到 4-6 毫秒),这是由于反射造成的。中大型数据集的性能通常与其他更“明确”的列表填充方法相当。
自我实践
请理解,尽管此项目进行了一些研究和实验,但最终结果是有益的。这种通用列表填充方法现在在我们公司被定期使用。
下载包
在下载包中,您将找到本文所述功能的完整可用代码。在 Classes 文件夹中,您会找到三个不同的类,它们都可以用于填充 List<Employee>
。每个类代表本文中介绍的一种方法(直接访问、反射、动态 Lambda)。因此,您可以查看各种方法并自己进行一些性能测试。
自定义属性
虽然此代码没有使用此选项,但我们使用的功能版本允许对数据模型属性使用自定义属性。这些自定义属性基本上是为了允许我们拥有不同的属性名和列名(EmployeeID
属性由 Employee_ID
列填充)。CodeProject(点击此处为一个示例)和其他地方的文章将有助于实现这种“升级”到该功能,并且绝对值得拥有。我没有在此包含该功能,因为我想专注于动态 Lambda 操作。