65.9K
CodeProject 正在变化。 阅读更多。
Home

通用 ListHelper 类 - .NET 4.5

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2015 年 7 月 15 日

CPOL

8分钟阅读

viewsIcon

27135

downloadIcon

530

本文将介绍如何通过动态创建 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 类

动态列表填充的使用方法非常简单。基本需要三个步骤:

  1. 创建一个代表列表中单个项目的数据模型
  2. 执行一个查询,查询结果将填充您的数据模型的 List<T>
  3. 实例化 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 列填充)。

© . All rights reserved.