泛型集合到 DataTable 的映射器






4.91/5 (24投票s)
一个高性能扩展,用于从泛型集合创建 DataTable。
引言
您知道的,有时您有一个非常大的泛型列表,但是您需要的是 DataTable,因为旧的遗留应用程序需要它,或者您想将其批量复制到数据库。
您可能会认为没问题。 Linq 命名空间中有一个 AsDataTable 函数。
是的,但是它只适用于 Linq to Dataset。
那么,除了每次需要时都硬编码之外,还有什么其他解决方案吗?
嗯,您可以使用反射,但问题是性能很差。
所以,因为我找不到有人创建了这个功能,所以我不得不自己做。
使用代码
只有一个公共方法,一个名为 AsDataTable 的扩展方法,它接受一个 IEnumerable
作为参数,并且使用起来很简单,例如 MyGenericList.AsDataTable()
/// <summary>
/// Creates a DataTable from an IEnumerable
/// </summary>
/// <typeparam name="TSource">The Generic type of the Collection</typeparam>
/// <param name="Collection"></param>
/// <returns>DataTable</returns>
public static DataTable AsDataTable<TSource>(this IEnumerable<TSource> Collection)
{
DataTable dt = DataTableCreator<TSource>.GetDataTable();
Func<TSource, object[]> Map = DataRowMapperCache<TSource>.GetDataRowMapper(dt);
foreach (TSource item in Collection)
{
dt.Rows.Add(Map(item));
}
return dt;
}
''' <summary>
''' Creates a DataTable from an IEnumerable
''' </summary>
''' <typeparam name="TSource">The Generic type of the Collection</typeparam>
''' <param name="Collection"></param>
''' <returns>DataTable</returns>
<Extension> _
Public Function AsDataTable(Of TSource)(Collection As IEnumerable(Of TSource)) As DataTable
Dim dt As DataTable = DataTableCreator(Of TSource).GetDataTable
Dim Map As Func(Of TSource, Object()) = DataRowMapperCache(Of TSource).GetDataRowMapper(dt)
For Each item As TSource In Collection
dt.Rows.Add(Map(item))
Next
Return dt
End Function
这部分非常简单,它从缓存中获取一个新的 DataTable,并获取一个 Delegate,它充当实例和 ObjectArray 之间的映射器。
然后,该委托用于集合中的每个项目,以创建一个添加到 DataTable 的 RowCollection 的 ObjectArray。
添加数组而不是 DataRow 的原因是 DataRow 没有公共构造函数。 这样做更简单。
创建 DataTable
DataTable 的构造是通过反射完成的,因为它被缓存,所以每个 Item Class 只做一次,并且不会多次影响性能
/// <summary>
/// Creates a DataTable with the same fields as the Generic Type argument
/// </summary>
/// <typeparam name="TSource">The Generic type</typeparam>
/// <returns>DataTable</returns>
static internal DataTable CreateDataTable<TSource>()
{
DataTable dt = new DataTable();
foreach (FieldInfo SourceMember in typeof(TSource).GetFields(BindingFlags.Instance | BindingFlags.Public))
{
dt.AddTableColumn(SourceMember, SourceMember.FieldType);
}
foreach (PropertyInfo SourceMember in typeof(TSource).GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if (SourceMember.CanRead)
{
dt.AddTableColumn(SourceMember, SourceMember.PropertyType);
}
}
return dt;
}
''' <summary>
''' Creates a DataTable with the same fields as the Generic Type argument
''' </summary>
''' <typeparam name="TSource">The Generic type</typeparam>
''' <returns>DataTable</returns>
Friend Function CreateDataTable(Of TSource)() As DataTable
Dim dt As New DataTable()
For Each SourceMember As FieldInfo In GetType(TSource).GetFields(BindingFlags.Instance Or BindingFlags.[Public])
dt.AddTableColumn(SourceMember, SourceMember.FieldType)
Next
For Each SourceMember As PropertyInfo In GetType(TSource).GetProperties(BindingFlags.Instance Or BindingFlags.[Public])
If SourceMember.CanRead Then
dt.AddTableColumn(SourceMember, SourceMember.PropertyType)
End If
Next
Return dt
End Function
此方法的作用是循环遍历 TSource 类的所有公共实例成员,并检查它们是字段还是可读属性。
如果 Member 的类型是 DataColumn 支持的类型,它将被添加,使用名称、类型以及是否允许 DbNull。
/// <summary>
/// Adds a Column to a DataTable
/// </summary>
public static void AddTableColumn(this DataTable dt, MemberInfo SourceMember, Type MemberType)
{
if (MemberType.IsAllowedType())
{
DataColumn dc;
string FieldName = GetFieldNameAttribute(SourceMember);
if (string.IsNullOrWhiteSpace(FieldName))
{
FieldName = SourceMember.Name;
}
if (Nullable.GetUnderlyingType(MemberType) == null)
{
dc = new DataColumn(FieldName, MemberType);
dc.AllowDBNull = !MemberType.IsValueType;
}
else
{
dc = new DataColumn(FieldName, Nullable.GetUnderlyingType(MemberType));
dc.AllowDBNull = true;
}
dt.Columns.Add(dc);
}
}
''' <summary>
''' Adds a Column to a DataTable
''' </summary>
<Extension> _
Private Sub AddTableColumn(dt As DataTable, SourceMember As MemberInfo, MemberType As Type)
If MemberType.IsAllowedType Then
Dim dc As DataColumn
Dim FieldName As String = GetFieldNameAttribute(SourceMember)
If String.IsNullOrWhiteSpace(FieldName) Then
FieldName = SourceMember.Name
End If
If Nullable.GetUnderlyingType(MemberType) Is Nothing Then
dc = New DataColumn(FieldName, MemberType)
dc.AllowDBNull = Not MemberType.IsValueType
Else
dc = New DataColumn(FieldName, Nullable.GetUnderlyingType(MemberType))
dc.AllowDBNull = True
End If
dt.Columns.Add(dc)
End If
End Sub
如果您希望 DataColumn 具有与 Member 不同的名称,您可以向其添加 FieldNameAttribute
[FieldName("Some odd fieldname")]
public string Name { get; set; }
<FieldName("Some odd fieldname")>
Property Name As String
FieldNameAttribute 显然优先于 TargetMembers 名称
创建 DataRowMapper
mapper 使用表达式树和反射创建。
它是通过循环遍历 DataTable 中的 DataColumn,并通过名称或 FieldnameAttribute 将它们与 Instanceclass 匹配来完成的
/// <summary>
/// Creates a delegate that maps an instance of TSource to an ItemArray of the supplied DataTable
/// </summary>
/// <typeparam name="TSource">The Generic Type to map from</typeparam>
/// <param name="dt">The DataTable to map to</param>
/// <returns>Func(Of TSource, Object())</returns>
static internal Func<TSource, object[]> CreateDataRowMapper<TSource>(DataTable dt)
{
Type SourceType = typeof(TSource);
ParameterExpression SourceInstanceExpression = Expression.Parameter(SourceType, "SourceInstance");
List<Expression> Values = new List<Expression>();
foreach (DataColumn col in dt.Columns)
{
foreach (FieldInfo SourceMember in SourceType.GetFields(BindingFlags.Instance | BindingFlags.Public))
{
if (MemberMatchesName(SourceMember, col.ColumnName))
{
Values.Add(GetSourceValueExpression(SourceInstanceExpression, SourceMember));
break;
}
}
foreach (PropertyInfo SourceMember in SourceType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if (SourceMember.CanRead && MemberMatchesName(SourceMember, col.ColumnName))
{
Values.Add(GetSourceValueExpression(SourceInstanceExpression, SourceMember));
break;
}
}
}
NewArrayExpression body = Expression.NewArrayInit(Type.GetType("System.Object"), Values);
return Expression.Lambda<Func<TSource, object[]>>(body, SourceInstanceExpression).Compile();
}
''' <summary>
''' Creates a delegate that maps an instance of TSource to an ItemArray of the supplied DataTable
''' </summary>
''' <typeparam name="TSource">The Generic Type to map from</typeparam>
''' <param name="dt">The DataTable to map to</param>
''' <returns>Func(Of TSource, Object())</returns>
Friend Function CreateDataRowMapper(Of TSource)(dt As DataTable) As Func(Of TSource, Object())
Dim SourceType As Type = GetType(TSource)
Dim SourceInstanceExpression As ParameterExpression = Expression.Parameter(SourceType, "SourceInstance")
Dim Values As New List(Of Expression)
For Each col As DataColumn In dt.Columns
For Each SourceMember As FieldInfo In SourceType.GetFields(BindingFlags.Instance Or BindingFlags.[Public])
If MemberMatchesName(SourceMember, col.ColumnName) Then
Values.Add(GetSourceValueExpression(SourceInstanceExpression, SourceMember))
Exit For
End If
Next
For Each SourceMember As PropertyInfo In SourceType.GetProperties(BindingFlags.Instance Or BindingFlags.[Public])
If SourceMember.CanRead AndAlso MemberMatchesName(SourceMember, col.ColumnName) Then
Values.Add(GetSourceValueExpression(SourceInstanceExpression, SourceMember))
Exit For
End If
Next
Next
Dim body As NewArrayExpression = Expression.NewArrayInit(Type.[GetType]("System.Object"), Values)
Return Expression.Lambda(Of Func(Of TSource, Object()))(body, SourceInstanceExpression).Compile()
End Function
当我们有匹配项时,我们创建一个 MemberExpression,表示 Field 或 Property 中的值,我们将其添加到用于创建 NewArrayInitExpression 的数组中。
/// <summary>
/// Creates an Expression representing the value of the SourceMember
/// </summary>
/// <param name="SourceInstanceExpression"></param>
/// <param name="SourceMember"></param>
/// <returns></returns>
private static Expression GetSourceValueExpression(ParameterExpression SourceInstanceExpression, MemberInfo SourceMember)
{
MemberExpression MemberExpression = Expression.PropertyOrField(SourceInstanceExpression, SourceMember.Name);
Expression SourceValueExpression;
if (Nullable.GetUnderlyingType(SourceMember.ReflectedType) == null)
{
SourceValueExpression = Expression.Convert(MemberExpression, typeof(object));
}
else
{
SourceValueExpression = Expression.Condition(
Expression.Property(Expression.Constant(SourceInstanceExpression), "HasValue"),
MemberExpression,
Expression.Constant(DBNull.Value),
typeof(object));
}
return SourceValueExpression;
}
''' <summary>
''' Creates an Expression representing the value of the SourceMember
''' </summary>
''' <param name="SourceInstanceExpression"></param>
''' <param name="SourceMember"></param>
''' <returns></returns>
Private Function GetSourceValueExpression(SourceInstanceExpression As ParameterExpression, SourceMember As MemberInfo) As Expression
Dim MemberExpression As MemberExpression = Expression.PropertyOrField(SourceInstanceExpression, SourceMember.Name)
Dim SourceValueExpression As Expression
If Nullable.GetUnderlyingType(SourceMember.ReflectedType) Is Nothing Then
SourceValueExpression = Expression.Convert(MemberExpression, GetType(Object))
Else
SourceValueExpression = Expression.Condition(
Expression.Property(Expression.Constant(SourceInstanceExpression), "HasValue"),
MemberExpression,
Expression.Constant(DBNull.Value),
GetType(Object)
)
End If
Return SourceValueExpression
End Function
然后,此表达式被编译为委托。
DataTable 和 MapperDelegate 被缓存以提高性能
关注点
MemberExpression 的创建可以与 DataTable 的创建同时进行,但我反对这样做,以便更容易进行未来的增强
历史
- 2015 年 3 月 25 日:v1.0 首次发布
- 2015 年 4 月 28 日:v1.1 一些重构和类型检查