通用 ListHelper 类 - .NET 4.5





5.00/5 (5投票s)
本文将介绍如何通过动态创建 lambda 表达式,从 SqlDataReader 对象中创建特定类类型的列表,以实现列表的填充。
引言
这是我先前文章 《Generically Populate List of Objects from SqlDataReader》 的更新。本文发布的信息主要用于将通用列表填充器更新到 .NET Framework 4.5 版本,并增加根据类中给定属性对对象列表进行排序的功能。在大多数情况下,本文的文字将与之前的文章相同,但代码已修改和添加。话不多说,让我们开始吧。
在创建 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 上的一篇题为 《Reflection Optimization Techniques》 的文章获得了一些帮助,该文章为我指明了正确的方向。但不幸的是,它并没有提供我所需的所有内容,并且是针对 .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 GenericPopulator.ListHelper<Employee>().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)
{
Func<SqlDataReader, T> readRow = this.GetReader(reader);
var results = new List<T>();
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 表达式列表创建存储
我们将创建多个 lambda 表达式,并希望最终将它们组合在一起。创建一个 List
var statements = new List<Expression>();
步骤 3 - 获取 SqlDataReader 的索引器属性
我们需要知道 SqlDataReader 的“Item”属性称为什么。我们将把这个值存储在一个变量中,供以后使用。
var indexerProperty = typeof(SqlDataReader).GetProperty("Item", new[] { typeof(string) });
步骤 4 - 创建对象实例的表达式
我们的 lambda 函数创建一个指定类的实例,并接受一个 SqlDataReader 作为输入参数。我们需要为这些对象中的每一个创建一个 ParameterExpression——一个用于输出,一个用于输入。
var instanceParam = Expression.Variable(typeof(T));
var readerParam = Expression.Parameter(typeof(SqlDataReader));
步骤 5 - 创建类型的实例
现在我们可以创建一个实际生成指定数据类型实例的表达式。
BinaryExpression createInstance = Expression.Assign(instanceParam, Expression.New(typeof(T)));
statements.Add(createInstance);
步骤 6 - 获取属性列表及其数据库列名
我们需要获取数据模型类中的属性列表。使用 DatabaseProperty
属性(位于 Attributes 文件夹中),您可以指定在填充属性时使用数据库中的哪个列。例如,这允许将名为 EmployeeID 的属性与 Employee_ID
列中的数据填充。这是我在类的初始生成后在上一版本中添加的功能之一。这也是一些人询问过的内容,所以我在本版本文章中包含了这些信息。
var properties = typeof(T).GetProperties();
var columnNames = this.GetColumnNames(properties);
步骤 7 - 为每个属性创建条件表达式列表
我们使用 ConditionalExpressions
,因为我们需要确定如何设置实际的列值。属性的实际值将取决于几个因素。
首先,我们查看属性的数据类型。在我们的环境中,我们选择让“string”属性在数据为 null 时最初用空字符串填充。另外,如果属性的数据类型是可空值,我们将适当地将值设置为 NULL。
我们将循环遍历类的属性(由 T 定义)以确定需要多少 ConditionalExpression
元素以及它们的外观。循环的处理过程是:
- 循环遍历
Class<T>
中的每个属性 - 如果在数据集中找到该属性作为列
- 创建一个
MemberExpression
来设置属性 - 创建一个
IndexExpression
来从 SqlDataReader 获取相应的数据 - 创建一个
ConstantExpression
来表示DBNull.Value
- 创建一个
BinaryExpression
来确定表中的数据是否等于DBNull.Value
- 创建适当的
ConditionalExpression
来分配默认值或数据库中的值 - 将
ConditionalExpression
添加到表达式列表中
- 创建一个
foreach (var property in properties)
{
string columnName = columnNames[property.Name];
//string columnName = property.Name;
if (readerColumns.Contains(columnName))
{
// get the instance.Property
MemberExpression setProperty = Expression.Property(instanceParam, property);
// the database column name will be what is in the columnNames list -- defaults to the property name
IndexExpression readValue = Expression.MakeIndex(readerParam, indexerProperty, new[] { Expression.Constant(columnName) });
ConstantExpression nullValue = Expression.Constant(DBNull.Value, typeof(System.DBNull));
BinaryExpression valueNotNull = Expression.NotEqual(readValue, nullValue);
if (property.PropertyType.Name.ToLower().Equals("string"))
{
ConditionalExpression assignProperty = Expression.IfThenElse(valueNotNull, Expression.Assign(setProperty, Expression.Convert(readValue, property.PropertyType)), Expression.Assign(setProperty, Expression.Constant("", typeof(System.String))));
statements.Add(assignProperty);
}
else
{
ConditionalExpression assignProperty = Expression.IfThen(valueNotNull, Expression.Assign(setProperty, Expression.Convert(readValue, property.PropertyType)));
statements.Add(assignProperty);
}
}
}
var returnStatement = instanceParam;
statements.Add(returnStatement);
步骤 5 - 创建并编译 lambda 函数
函数的最后一部分将执行以下步骤:
- 创建 lambda 表达式的主体
- 编译 lambda 表达式
- 将函数委托返回给调用函数
var body = Expression.Block(instanceParam.Type, new[] { instanceParam }, statements.ToArray());
var lambda = Expression.Lambda<Func<SqlDataReader, T>>(body, readerParam);
resDelegate = lambda.Compile();
return (Func<SqlDataReader, T>)resDelegate;
关注点
未包含性能指标
虽然我没有在此包含任何性能指标,但我已经对各种大小的数据集进行了测试。我的性能测试结果大多数情况下都超出了预期——实际上,有时甚至优于 DataTable.Load()
选项。小数据集是性能“瓶颈”所在(大约 2 毫秒到 4-6 毫秒),因为反射的开销。中等到大型数据集通常与其他更“明确”的列表填充方法非常具有可比性。
自我实践
请理解,尽管该项目经过了一些研究和实验,但最终很有用。这种通用列表填充方法现在在我公司被定期使用。
下载包
在下载包中,您将找到此处所述功能的完整可用代码。
自定义属性
此版本允许数据模型属性上的自定义属性。这些自定义属性基本上是为了允许您拥有不同的属性名称和列名称(EmployeeID
属性从 Employee_ID
列填充)。