DataReader 的属性映射扩展






4.91/5 (46投票s)
DataReader 的属性映射扩展。
引言
和大家一样,我生性懒惰,总是想尽可能少地工作,而且我经常需要从数据库中生成大量的专业报告,所以我想通过使用一个自动映射器来节省所有那些乏味的属性映射工作。市面上有很多映射器,但我想要一个简单的、占用空间小且性能极高的映射器。
另一个设计目标是我希望它能与任何 IDataReader
一起工作。
因此,这个映射器不仅可以与 DBReader
(如 SQLDataReader
或 OracleDataReader
)一起使用,还可以与 DataTableReader
、StorageStream DataReader
或 Sebastien Lorion 的 CSVReader[^] 一起使用。
这意味着您可以使用实现 IDataReader 的任何数据读取器将数据映射到 POCO。
Using the Code
public
方法只是 IDataReader
的简单扩展方法,因此使用映射器非常容易。
只需像这样使用:Reader.AsEnumerable<MyClass>
或者如果您想一次性获取一个泛型列表或 LinkedList:Reader.ToList<MyClass>()
字典也一样:Reader.ToDictionary<MyClass => MyClass.Key, MyClass>()
如果您的数据需要从 string
解析为其他原始类型,您可能需要指定数据的 CultureInfo
。
像这样:Reader.AsEnumerable<MyClassInstance>(MyCultureInfo)
注意!
建议使用 CommandBehavoirs CloseConnection 和 KeyInfo 来创建 Reader。
像这样:Command.ExecuteReader((CommandBehavior)CommandBehavior.CloseConnection | CommandBehavior.KeyInfo)
Mapper 需要 KeyInfo 来知道字段是否可为空。CloseConnection 只是一个良好的习惯。
Mapper
Mapper 的核心是一个函数,它创建一个委托,该委托使用提供的 IDataRecord
来创建目标类的实例。该委托是从 lambda 表达式构建的,该 lambda 表达式使用 表达式树[^] 构建的。初始创建后,该委托会缓存一个映射,该映射同时作用于 TargetType 和 datareader 的 SourceFields。
如果 TargetType 是基本类型(如 String 或 Int32),Mapper 将使用 DataReader 的第一个字段,因为没有名称可供映射,并且 Reader 中只有一个字段也没有意义。
该委托创建一个目标实例,将 DataReader 的(已转换)值赋给该实例,然后将其返回给调用者。
如果它是一个复合类型,则存在一个双重循环,其中 DataRecord 中的所有字段都与要填充的类的 public
属性、字段或属性的名称匹配。
因此,当使用复合类型 Mapper 时,这是必需的。DataReader
的字段名必须与目标类中的属性/字段名或属性匹配。
此匹配不区分大小写,但如果需要,可以很容易地更改。
然后,它创建一个绑定,该绑定用于创建实例的 memberinit
表达式。
但我意识到我比我之前想的更懒,所以我增加了对 Tuples 的支持。
由于 Item1、Item2 等属性名几乎没有映射意义,因此它只是按位置映射。
它不映射嵌套元组,因此最多支持七个属性。
/// <summary>
/// Creates a delegate that creates an instance of Target from the supplied DataRecord
/// </summary>
/// <param name="RecordInstance">An instance of a DataRecord</param>
/// <returns>A Delegate that creates a new instance of Target with the values set from the supplied DataRecord</returns>
/// <remarks></remarks>
private static Func<IDataRecord, Target> GetInstanceCreator<Target>(IDataRecord RecordInstance, CultureInfo Culture,Boolean MustMapAllProperties)
{
Type RecordType = typeof(IDataRecord);
ParameterExpression RecordInstanceExpression = Expression.Parameter(RecordType, "SourceInstance");
Type TargetType = typeof(Target);
DataTable SchemaTable = ((IDataReader)RecordInstance).GetSchemaTable();
Expression Body = default(Expression);
//The actual names for Tuples are System.Tuple`1,System.Tuple`2 etc where the number stands for the number of Parameters
//This crashes whenever Microsoft creates a class in the System Namespace called Tuple`duple
if (TargetType.FullName.StartsWith("System.Tuple`"))
{
ConstructorInfo[] Constructors = TargetType.GetConstructors();
if (Constructors.Count() != 1)
throw new ArgumentException("Tuple must have one Constructor");
var Constructor = Constructors[0];
var Parameters = Constructor.GetParameters();
if (Parameters.Length > 7)
throw new NotSupportedException("Nested Tuples are not supported");
Expression[] TargetValueExpressions = new Expression[Parameters.Length];
for (int Ordinal = 0; Ordinal < Parameters.Length; Ordinal++)
{
var ParameterType = Parameters[Ordinal].ParameterType;
if (Ordinal >= RecordInstance.FieldCount)
{
if (MustMapAllProperties) { throw new ArgumentException("Tuple has more fields than the DataReader"); }
TargetValueExpressions[Ordinal] = Expression.Default(ParameterType);
}
else
{
TargetValueExpressions[Ordinal] = GetTargetValueExpression(
RecordInstance,
Culture,
RecordType,
RecordInstanceExpression,
SchemaTable,
Ordinal,
ParameterType);
}
}
Body = Expression.New(Constructor, TargetValueExpressions);
}
//Find out if SourceType is an elementary Type.
else if (TargetType.IsElementaryType())
{
//If you try to map an elementary type, e.g. ToList<Int32>, there is no name to map on. So to avoid error we map to the first field in the datareader
//If this is wrong, it is the query that's wrong.
const int Ordinal = 0;
Expression TargetValueExpression = GetTargetValueExpression(
RecordInstance,
Culture,
RecordType,
RecordInstanceExpression,
SchemaTable,
Ordinal,
TargetType);
ParameterExpression TargetExpression = Expression.Variable(TargetType, "Target");
Expression AssignExpression = Expression.Assign(TargetExpression, TargetValueExpression);
Body = Expression.Block(new ParameterExpression[] { TargetExpression }, AssignExpression);
}
else
{
//Loop through the Properties in the Target and the Fields in the Record to check which ones are matching
SortedDictionary<int, MemberBinding> Bindings = new SortedDictionary<int, MemberBinding>();
foreach (FieldInfo TargetMember in TargetType.GetFields(BindingFlags.Public | BindingFlags.Instance))
{
Action work = delegate
{
for (int Ordinal = 0; Ordinal < RecordInstance.FieldCount; Ordinal++)
{
//Check if the RecordFieldName matches the TargetMember
if (MemberMatchesName(TargetMember, RecordInstance.GetName(Ordinal)))
{
Expression TargetValueExpression = GetTargetValueExpression(
RecordInstance,
Culture,
RecordType,
RecordInstanceExpression,
SchemaTable,
Ordinal,
TargetMember.FieldType);
//Create a binding to the target member
MemberAssignment BindExpression = Expression.Bind(TargetMember, TargetValueExpression);
Bindings.Add(Ordinal, BindExpression);
return;
}
}
//If we reach this code the targetmember did not get mapped
if (MustMapAllProperties)
{
throw new ArgumentException(String.Format("TargetField {0} is not matched by any field in the DataReader", TargetMember.Name));
}
};
work();
}
foreach (PropertyInfo TargetMember in TargetType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (TargetMember.CanWrite)
{
Action work = delegate
{
for (int Ordinal = 0; Ordinal < RecordInstance.FieldCount; Ordinal++)
{
//Check if the RecordFieldName matches the TargetMember
if (MemberMatchesName(TargetMember, RecordInstance.GetName(Ordinal)))
{
Expression TargetValueExpression = GetTargetValueExpression(
RecordInstance,
Culture,
RecordType,
RecordInstanceExpression,
SchemaTable,
Ordinal,
TargetMember.PropertyType);
//Create a binding to the target member
MemberAssignment BindExpression = Expression.Bind(TargetMember, TargetValueExpression);
Bindings.Add(Ordinal, BindExpression);
return;
}
}
//If we reach this code the targetmember did not get mapped
if (MustMapAllProperties)
{
throw new ArgumentException(String.Format("TargetProperty {0} is not matched by any Field in the DataReader", TargetMember.Name));
}
};
work();
}
}
//Create a memberInitExpression that Creates a new instance of Target using bindings to the DataRecord
Body = Expression.MemberInit(Expression.New(TargetType), Bindings.Values);
}
//Compile the Expression to a Delegate
return Expression.Lambda<Func<IDataRecord, Target>>(Body, RecordInstanceExpression).Compile();
}
''' <summary>
''' Creates a delegate that creates an instance of Target from the supplied DataRecord
''' </summary>
''' <param name="RecordInstance">An instance of a DataRecord</param>
''' <returns>A Delegate that creates a new instance of Target with the values set from the supplied DataRecord</returns>
''' <remarks></remarks>
Private Function GetInstanceCreator(Of Target)(RecordInstance As IDataRecord, Culture As CultureInfo, MustMapAllProperties As Boolean) As Func(Of IDataRecord, Target)
Dim RecordType As Type = GetType(IDataRecord)
Dim RecordInstanceExpression As ParameterExpression = Expression.Parameter(RecordType, "SourceInstance")
Dim TargetType As Type = GetType(Target)
Dim SchemaTable As DataTable = DirectCast(RecordInstance, IDataReader).GetSchemaTable
Dim Body As Expression
'The actual names for Tuples are System.Tuple`1,System.Tuple`2 etc where the number stands for the number of Parameters
'This crashes whenever Microsoft creates a class in the System Namespace called Tuple`duple
If TargetType.FullName.StartsWith("System.Tuple`") Then
Dim Constructors As ConstructorInfo() = TargetType.GetConstructors()
If Constructors.Count() <> 1 Then Throw New ArgumentException("Tuple must have one Constructor")
Dim Constructor = Constructors(0)
Dim Parameters = Constructor.GetParameters()
If Parameters.Length > 7 Then Throw New NotSupportedException("Nested Tuples are not supported")
Dim TargetValueExpressions(Parameters.Length - 1) As Expression
For Ordinal = 0 To Parameters.Length - 1
Dim ParameterType = Parameters(Ordinal).ParameterType
If Ordinal >= RecordInstance.FieldCount Then
If MustMapAllProperties Then Throw New ArgumentException("Tuple has more fields than the DataReader")
TargetValueExpressions(Ordinal) = Expression.Default(ParameterType)
Else
TargetValueExpressions(Ordinal) = GetTargetValueExpression(
RecordInstance,
Culture,
RecordType,
RecordInstanceExpression,
SchemaTable,
Ordinal,
ParameterType)
End If
Next
Body = Expression.[New](Constructor, TargetValueExpressions)
'Find out if SourceType is an elementary Type.
ElseIf TargetType.IsElementaryType() Then
'If you try to map an elementary type, e.g. ToList(Of Int32), there is no name to map on. So to avoid error we map to the first field in the datareader
'If this is wrong, it is the query that's wrong.
Const Ordinal As Integer = 0
Dim TargetValueExpression As Expression = GetTargetValueExpression(
RecordInstance,
Culture,
RecordType,
RecordInstanceExpression,
SchemaTable,
Ordinal,
TargetType)
Dim TargetExpression As ParameterExpression = Expression.Variable(TargetType, "Target")
Dim AssignExpression As Expression = Expression.Assign(TargetExpression, TargetValueExpression)
Body = Expression.Block(New ParameterExpression() {TargetExpression}, AssignExpression)
Else
'Loop through the Properties in the Target and the Fields in the Record to check which ones are matching
Dim Bindings As New SortedDictionary(Of Integer, MemberBinding)
For Each TargetMember As FieldInfo In TargetType.GetFields(BindingFlags.Instance Or BindingFlags.[Public])
Do
For Ordinal As Integer = 0 To RecordInstance.FieldCount - 1
If MemberMatchesName(TargetMember, RecordInstance.GetName(Ordinal)) Then
Dim TargetValueExpression As Expression = GetTargetValueExpression(
RecordInstance,
Culture,
RecordType,
RecordInstanceExpression,
SchemaTable,
Ordinal,
TargetMember.FieldType)
'Create a binding to the target member
Dim BindExpression As MemberAssignment = Expression.Bind(TargetMember, TargetValueExpression)
Bindings.Add(Ordinal, BindExpression)
Exit Do
End If
Next
'If we reach this code the targetmember did not get mapped
If MustMapAllProperties Then
Throw New ArgumentException(String.Format("TargetField {0} is not matched by any field in the DataReader", TargetMember.Name))
End If
Loop While False 'Dummy loop for the Exit Do
Next
For Each TargetMember As PropertyInfo In TargetType.GetProperties(BindingFlags.Instance Or BindingFlags.[Public])
If TargetMember.CanWrite Then
Do
For Ordinal As Integer = 0 To RecordInstance.FieldCount - 1
If MemberMatchesName(TargetMember, RecordInstance.GetName(Ordinal)) Then
Dim TargetValueExpression As Expression = GetTargetValueExpression(
RecordInstance,
Culture,
RecordType,
RecordInstanceExpression,
SchemaTable,
Ordinal,
TargetMember.PropertyType)
'Create a binding to the target member
Dim BindExpression As MemberAssignment = Expression.Bind(TargetMember, TargetValueExpression)
Bindings.Add(Ordinal, BindExpression)
Exit Do
End If
Next
'If we reach this code the targetmember did not get mapped
If MustMapAllProperties Then
Throw New ArgumentException(String.Format("TargetProperty {0} is not matched by any field in the DataReader", TargetMember.Name))
End If
Loop While False 'Dummy loop for the Exit Do
End If
Next
'Create a memberInitExpression that Creates a new instance of Target using bindings to the DataRecord
Body = Expression.MemberInit(Expression.[New](TargetType), Bindings.Values)
End If
'Compile the Expression to a Delegate
Return Expression.Lambda(Of Func(Of IDataRecord, Target))(Body, RecordInstanceExpression).Compile()
End Function
通过比较 DataReader 的 Fieldname 与 Name 或 Property
的 FieldNameAttribute
来检查 Property
和 Field
之间是否存在匹配。
/// <summary>
/// Returns The FieldNameAttribute if existing
/// </summary>
/// <param name="Member">MemberInfo</param>
/// <returns>String</returns>
private static string GetFieldNameAttribute(MemberInfo Member)
{
if (Member.GetCustomAttributes(typeof(FieldNameAttribute), true).Count() > 0)
{
return ((FieldNameAttribute)Member.GetCustomAttributes(typeof(FieldNameAttribute), true)[0]).FieldName;
}
else
{
return string.Empty;
}
}
/// <summary>
/// Checks if the Field name matches the Member name or Members FieldNameAttribute
/// </summary>
/// <param name="Member">The Member of the Instance to check</param>
/// <param name="Name">The Name to compare with</param>
/// <returns>True if Fields match</returns>
/// <remarks>FieldNameAttribute takes precedence over TargetMembers name.</remarks>
private static bool MemberMatchesName(MemberInfo Member, string Name)
{
string FieldnameAttribute = GetFieldNameAttribute(Member);
return FieldnameAttribute.ToLower() == Name.ToLower() || Member.Name.ToLower() == Name.ToLower();
}
''' <summary>
''' Returns The FieldNameAttribute if existing
''' </summary>
''' <param name="Member">MemberInfo</param>
''' <returns>String</returns>
Private Function GetFieldNameAttribute(Member As MemberInfo) As String
If Member.GetCustomAttributes(GetType(FieldNameAttribute), True).Count() > 0 Then
Return DirectCast(Member.GetCustomAttributes(GetType(FieldNameAttribute), True)(0), FieldNameAttribute).FieldName
Else
Return String.Empty
End If
End Function
''' <summary>
''' Checks if the Field name matches the Member name or Members FieldNameAttribute
''' </summary>
''' <param name="Member">The Member of the Instance to check</param>
''' <param name="Name">The Name to compare with</param>
''' <returns>True if Fields match</returns>
''' <remarks>FieldNameAttribute takes precedence over TargetMembers name.</remarks>
Private Function MemberMatchesName(Member As MemberInfo, Name As String) As Boolean
Dim FieldNameAttribute As String = GetFieldNameAttribute(Member)
Return FieldNameAttribute.ToLower() = Name.ToLower() OrElse Member.Name.ToLower() = Name.ToLower()
End Function
FieldNameAttribute
具有优先权。
下面的属性显示了实际的 Attribute
。
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
class FieldNameAttribute : Attribute
{
private readonly string _FieldName;
public string FieldName
{
get { return _FieldName; }
}
public FieldNameAttribute(string FieldName)
{
_FieldName = FieldName;
}
}
<AttributeUsage(AttributeTargets.Field Or AttributeTargets.Property, AllowMultiple:=False)>
Class FieldNameAttribute
Inherits Attribute
Private ReadOnly _FieldName As String
Public ReadOnly Property FieldName As String
Get
Return _FieldName
End Get
End Property
Sub New(ByVal FieldName As String)
_FieldName = FieldName
End Sub
End Class
您只需像这样将属性添加到属性或字段即可使用它。
[FieldName("Shipping Country")]
public CountryEnum? ShipCountry { get; set; }
<FieldName("Shipping Country")> _
Public Property ShipCountry As CountryEnum?
对于每个映射的属性,我们需要检查源是否可为空,这一点很重要,因为它关系到性能。
如果我们知道源不包含 null
,则赋值可以简化。
如果源是 null
,我们将分配 Target
的默认值。
IDataReader
本身不处理可空类型,但我们需要的所有信息都存在于 SchemaTable
和 IsNull
字段中。
/// <summary>
/// Returns an Expression representing the value to set the TargetProperty to
/// </summary>
/// <remarks>Prepares the parameters to call the other overload</remarks>
private static Expression GetTargetValueExpression(
IDataRecord RecordInstance,
CultureInfo Culture,
Type RecordType,
ParameterExpression RecordInstanceExpression,
DataTable SchemaTable,
int Ordinal,
Type TargetMemberType)
{
Type RecordFieldType = RecordInstance.GetFieldType(Ordinal);
bool AllowDBNull = Convert.ToBoolean(SchemaTable.Rows[Ordinal]["AllowDBNull"]);
Expression RecordFieldExpression = GetRecordFieldExpression(RecordType, RecordInstanceExpression, Ordinal, RecordFieldType);
Expression ConvertedRecordFieldExpression = GetConversionExpression(RecordFieldType, RecordFieldExpression, TargetMemberType, Culture);
MethodCallExpression NullCheckExpression = GetNullCheckExpression(RecordType, RecordInstanceExpression, Ordinal);
//Create an expression that assigns the converted value to the target
Expression TargetValueExpression = default(Expression);
if (AllowDBNull)
{
TargetValueExpression = Expression.Condition(
NullCheckExpression,
Expression.Default(TargetMemberType),
ConvertedRecordFieldExpression,
TargetMemberType
);
}
else
{
TargetValueExpression = ConvertedRecordFieldExpression;
}
return TargetValueExpression;
}
''' <summary>
''' Returns an Expression representing the value to set the TargetProperty to
''' </summary>
''' <remarks>Prepares the parameters to call the other overload</remarks>
Private Function GetTargetValueExpression(ByVal RecordInstance As IDataRecord,
ByVal Culture As CultureInfo,
ByVal RecordType As Type,
ByVal RecordInstanceExpression As ParameterExpression,
ByVal SchemaTable As DataTable,
ByVal Ordinal As Integer,
ByVal TargetMemberType As Type) As Expression
Dim RecordFieldType As Type = RecordInstance.GetFieldType(Ordinal)
Dim AllowDBNull As Boolean = CBool(SchemaTable.Rows(Ordinal).Item("AllowDBNull"))
Dim RecordFieldExpression As Expression = GetRecordFieldExpression(RecordType, RecordInstanceExpression, Ordinal, RecordFieldType)
Dim ConvertedRecordFieldExpression As Expression = GetConversionExpression(RecordFieldType, RecordFieldExpression, TargetMemberType, Culture)
Dim NullCheckExpression As MethodCallExpression = GetNullCheckExpression(RecordType, RecordInstanceExpression, Ordinal)
'Create an expression that assigns the converted value to the target
Dim TargetValueExpression As Expression
If AllowDBNull Then
TargetValueExpression = Expression.Condition(
NullCheckExpression,
Expression.Default(TargetMemberType),
ConvertedRecordFieldExpression,
TargetMemberType
)
Else
TargetValueExpression = ConvertedRecordFieldExpression
End If
Return TargetValueExpression
End Function
这里我们检查 RecordValue
是否为 null
。这是通过检查 Reader
的 IsDBNull
属性的值来完成的。
/// <summary>
/// Gets an Expression that checks if the current RecordField is null
/// </summary>
/// <param name="RecordType">The Type of the Record</param>
/// <param name="RecordInstance">The Record instance</param>
/// <param name="Ordinal">The index of the parameter</param>
/// <returns>MethodCallExpression</returns>
private static MethodCallExpression GetNullCheckExpression(Type RecordType, ParameterExpression RecordInstance, int Ordinal)
{
MethodInfo GetNullValueMethod = RecordType.GetMethod("IsDBNull", new Type[] { typeof(int) });
MethodCallExpression NullCheckExpression = Expression.Call(RecordInstance, GetNullValueMethod, Expression.Constant(Ordinal, typeof(int)));
return NullCheckExpression;
}
''' <summary>
''' Gets an Expression that checks if the current RecordField is null
''' </summary>
''' <param name="RecordType">The Type of the Record</param>
''' <param name="RecordInstance">The Record instance</param>
''' <param name="Ordinal">The index of the parameter</param>
''' <returns>MethodCallExpression</returns>
Private Function GetNullCheckExpression(ByVal RecordType As Type, ByVal RecordInstance As ParameterExpression, ByVal Ordinal As Integer) As MethodCallExpression
Dim GetNullValueMethod As MethodInfo = RecordType.GetMethod("IsDBNull", New Type() {GetType(Integer)})
Dim NullCheckExpression As MethodCallExpression = Expression.[Call](RecordInstance, GetNullValueMethod, Expression.Constant(Ordinal, GetType(Integer)))
Return NullCheckExpression
End Function
我们还需要从 RecordField
创建一个 SourceExpression
。
如果我们使用 Reader
的正确 getter 方法,我们可以避免一些装箱和强制转换操作。
/// <summary>
/// Gets an Expression that represents the getter method for the RecordField
/// </summary>
/// <param name="RecordType">The Type of the Record</param>
/// <param name="RecordInstanceExpression">The Record instance</param>
/// <param name="Ordinal">The index of the parameter</param>
/// <param name="RecordFieldType">The Type of the RecordField</param>
/// <returns></returns>
private static Expression GetRecordFieldExpression(Type RecordType, ParameterExpression RecordInstanceExpression, int Ordinal, Type RecordFieldType)
{
MethodInfo GetValueMethod = default(MethodInfo);
switch (RecordFieldType.FullName)
{
case "System.Boolean" :
GetValueMethod = RecordType.GetMethod("GetBoolean", new Type[] { typeof(int) });
break;
case "System.Byte":
GetValueMethod = RecordType.GetMethod("GetByte", new Type[] { typeof(int) });
break;
case "System.Byte[]":
GetValueMethod = typeof(HelperFunctions).GetMethod("RecordFieldToBytes", new Type[] { typeof(IDataRecord), typeof(int) });
break;
case "System.Char":
GetValueMethod = RecordType.GetMethod("GetChar", new Type[] { typeof(int) });
break;
case "System.DateTime":
GetValueMethod = RecordType.GetMethod("GetDateTime", new Type[] { typeof(int) });
break;
case "System.Decimal":
GetValueMethod = RecordType.GetMethod("GetDecimal", new Type[] { typeof(int) });
break;
case "System.Double":
GetValueMethod = RecordType.GetMethod("GetDouble", new Type[] { typeof(int) });
break;
case "System.Single":
GetValueMethod = RecordType.GetMethod("GetFloat", new Type[] { typeof(int) });
break;
case "System.Guid":
GetValueMethod = RecordType.GetMethod("GetGuid", new Type[] { typeof(int) });
break;
case "System.Int16":
GetValueMethod = RecordType.GetMethod("GetInt16", new Type[] { typeof(int) });
break;
case "System.Int32":
GetValueMethod = RecordType.GetMethod("GetInt32", new Type[] { typeof(int) });
break;
case "System.Int64":
GetValueMethod = RecordType.GetMethod("GetInt64", new Type[] { typeof(int) });
break;
case "System.String":
GetValueMethod = RecordType.GetMethod("GetString", new Type[] { typeof(int) });
break;
default:
GetValueMethod = RecordType.GetMethod("GetValue", new Type[] { typeof(int) });
break;
}
Expression RecordFieldExpression;
if (object.ReferenceEquals(RecordFieldType, typeof(byte[])))
{
RecordFieldExpression = Expression.Call(GetValueMethod, new Expression[] { RecordInstanceExpression, Expression.Constant(Ordinal, typeof(int)) });
}
else
{
RecordFieldExpression = Expression.Call(RecordInstanceExpression, GetValueMethod, Expression.Constant(Ordinal, typeof(int)));
}
return RecordFieldExpression;
}
''' <summary>
''' Gets an Expression that represents the getter method for the RecordField
''' </summary>
''' <param name="RecordType">The Type of the Record</param>
''' <param name="RecordInstanceExpression">The Record instance</param>
''' <param name="Ordinal">The index of the parameter</param>
''' <param name="RecordFieldType">The Type of the RecordField</param>
''' <returns></returns>
Private Function GetRecordFieldExpression(ByVal RecordType As Type, ByVal RecordInstanceExpression As ParameterExpression, ByVal Ordinal As Integer, RecordFieldType As Type) As Expression
Dim GetValueMethod As MethodInfo
Select Case RecordFieldType
Case GetType(Boolean)
GetValueMethod = RecordType.GetMethod("GetBoolean", {GetType(Integer)})
Case GetType(Byte)
GetValueMethod = RecordType.GetMethod("GetByte", {GetType(Integer)})
Case GetType(Byte())
GetValueMethod = GetType(HelperFunctions).GetMethod("RecordFieldToBytes", {GetType(IDataRecord), GetType(Integer)})
Case GetType(Char)
GetValueMethod = RecordType.GetMethod("GetChar", {GetType(Integer)})
Case GetType(DateTime)
GetValueMethod = RecordType.GetMethod("GetDateTime", {GetType(Integer)})
Case GetType(Decimal)
GetValueMethod = RecordType.GetMethod("GetDecimal", {GetType(Integer)})
Case GetType(Double)
GetValueMethod = RecordType.GetMethod("GetDouble", {GetType(Integer)})
Case GetType(Single)
GetValueMethod = RecordType.GetMethod("GetFloat", {GetType(Integer)})
Case GetType(Guid)
GetValueMethod = RecordType.GetMethod("GetGuid", {GetType(Integer)})
Case GetType(Int16)
GetValueMethod = RecordType.GetMethod("GetInt16", {GetType(Integer)})
Case GetType(Int32)
GetValueMethod = RecordType.GetMethod("GetInt32", {GetType(Integer)})
Case GetType(Int64)
GetValueMethod = RecordType.GetMethod("GetInt64", {GetType(Integer)})
Case GetType(String)
GetValueMethod = RecordType.GetMethod("GetString", {GetType(Integer)})
Case Else
GetValueMethod = RecordType.GetMethod("GetValue", {GetType(Integer)})
End Select
Dim RecordFieldExpression As Expression
If RecordFieldType Is GetType(Byte()) Then
RecordFieldExpression = Expression.[Call](GetValueMethod, {RecordInstanceExpression, Expression.Constant(Ordinal, GetType(Integer))})
Else
RecordFieldExpression = Expression.[Call](RecordInstanceExpression, GetValueMethod, Expression.Constant(Ordinal, GetType(Integer)))
End If
Return RecordFieldExpression
End Function
转换字段
我们还需要检查 Source
和 Target
属性是否为不同类型,如果它们是不同类型,我们需要转换它们。
如果它们是相同类型,我们只需返回 Source
属性。
但如果它们不同,我们还需要将它们从 SourceType
强制转换为 TargetType
。
内置的 Expression.Convert
可以处理所有隐式和显式转换,但这里有两个特殊情况需要处理。
原始类型到 String
没有转换运算符。因此,如果尝试这样做,函数将抛出异常。
因此,通过调用源的 ToString
方法来处理此问题。ToString
与类型转换不同,但对于任何原始类型,它都可以。
另一种情况是从 String
转换为其他原始类型和 enum
,这通过在另一个方法中解析 String
来处理。
/// <summary>
/// Gets an expression representing the Source converted to the TargetType
/// </summary>
/// <param name="SourceType">The Type of the Source</param>
/// <param name="SourceExpression">An Expression representing the Source value</param>
/// <param name="TargetType">The Type of the Target</param>
/// <returns>Expression</returns>
private static Expression GetConversionExpression(Type SourceType, Expression SourceExpression, Type TargetType, CultureInfo Culture)
{
Expression TargetExpression;
if (object.ReferenceEquals(TargetType, SourceType))
{
//Just assign the RecordField
TargetExpression = SourceExpression;
}
else if (object.ReferenceEquals(SourceType, typeof(string)))
{
TargetExpression = GetParseExpression(SourceExpression, TargetType, Culture);
}
else if (object.ReferenceEquals(TargetType, typeof(string)))
{
//There are no casts from primitive types to String.
//And Expression.Convert Method (Expression, Type, MethodInfo) only works with static methods.
TargetExpression = Expression.Call(SourceExpression, SourceType.GetMethod("ToString", Type.EmptyTypes));
}
else if (object.ReferenceEquals(TargetType, typeof(bool)))
{
MethodInfo ToBooleanMethod = typeof(Convert).GetMethod("ToBoolean", new[] { SourceType });
TargetExpression = Expression.Call(ToBooleanMethod, SourceExpression);
}
else if (object.ReferenceEquals(SourceType, typeof(Byte[])))
{
TargetExpression = GetArrayHandlerExpression(SourceExpression, TargetType);
}
else
{
//Using Expression.Convert works wherever you can make an explicit or implicit cast.
//But it casts OR unboxes an object, therefore the double cast. First unbox to the SourceType and then cast to the TargetType
//It also doesn't convert a numerical type to a String or date, this will throw an exception.
TargetExpression = Expression.Convert(SourceExpression, TargetType);
}
return TargetExpression;
}
''' <summary>
''' Gets an expression representing the recordField converted to the TargetPropertyType
''' </summary>
''' <param name="SourceType">The Type of the Source</param>
''' <param name="SourceExpression">An Expression representing the Source value</param>
''' <param name="TargetType">The Type of the Target</param>
''' <returns>Expression</returns>
Private Function GetConversionExpression(ByVal SourceType As Type, ByVal SourceExpression As Expression, ByVal TargetType As Type, Culture As CultureInfo) As Expression
Dim TargetExpression As Expression
If TargetType Is SourceType Then
'Just assign the RecordField
TargetExpression = SourceExpression
ElseIf SourceType Is GetType(String) Then
TargetExpression = GetParseExpression(SourceExpression, TargetType, Culture)
ElseIf TargetType Is GetType(String) Then
'There are no casts from primitive types to String.
'And Expression.Convert Method (Expression, Type, MethodInfo) only works with static methods.
TargetExpression = Expression.Call(SourceExpression, SourceType.GetMethod("ToString", Type.EmptyTypes))
ElseIf TargetType Is GetType(Boolean) Then
Dim ToBooleanMethod As MethodInfo = GetType(Convert).GetMethod("ToBoolean", {SourceType})
TargetExpression = Expression.Call(ToBooleanMethod, SourceExpression)
ElseIf SourceType Is GetType(Byte()) Then
TargetExpression = GetArrayHandlerExpression(SourceExpression, TargetType)
Else
'Using Expression.Convert works wherever you can make an explicit or implicit cast.
'But it casts OR unboxes an object, therefore the double cast. First unbox to the SourceType and then cast to the TargetType
'It also doesn't convert a numerical type to a String or date, this will throw an exception.
TargetExpression = Expression.Convert(SourceExpression, TargetType)
End If
Return TargetExpression
End Function
不同类型使用不同的 Parse
方法,因此我们必须使用 Switch
来选择正确的方法。
所有 Numbers
实际上都使用相同的方法,但由于 Number
是 .NET Framework 中的内部类,因此 Switch
会有点冗长。
/// <summary>
/// Creates an Expression that parses a string
/// </summary>
/// <param name="SourceExpression"></param>
/// <param name="TargetType "></param>
/// <param name="Provider"></param>
/// <returns></returns>
private static Expression GetParseExpression(Expression SourceExpression, Type TargetType , CultureInfo Culture)
{
Type UnderlyingType = GetUnderlyingType(TargetType );
if (UnderlyingType.IsEnum)
{
MethodCallExpression ParsedEnumExpression = GetEnumParseExpression(SourceExpression, UnderlyingType);
//Enum.Parse returns an object that needs to be unboxed
return Expression.Unbox(ParsedEnumExpression, TargetType );
}
else
{
Expression ParseExpression = default(Expression);
switch (UnderlyingType.FullName)
{
case "System.Byte":
case "System.UInt16":
case "System.UInt32":
case "System.UInt64":
case "System.SByte":
case "System.Int16":
case "System.Int32":
case "System.Int64":
case "System.Double":
case "System.Decimal":
ParseExpression = GetNumberParseExpression(SourceExpression, UnderlyingType, Culture);
break;
case "System.DateTime":
ParseExpression = GetDateTimeParseExpression(SourceExpression, UnderlyingType, Culture);
break;
case "System.Boolean":
case "System.Char":
ParseExpression = GetGenericParseExpression(SourceExpression, UnderlyingType);
break;
default:
throw new ArgumentException(string.Format("Conversion from {0} to {1} is not supported", "String", TargetType ));
}
if (Nullable.GetUnderlyingType(TargetType ) == null)
{
return ParseExpression;
}
else
{
//Convert to nullable if necessary
return Expression.Convert(ParseExpression, TargetType );
}
}
}
''' <summary>
''' Creates an Expression that parses a string
''' </summary>
''' <param name="SourceExpression"></param>
''' <param name="TargetType "></param>
''' <param name="Culture"></param>
''' <returns></returns>
Private Function GetParseExpression(SourceExpression As Expression, TargetType As Type, Culture As CultureInfo) As Expression
Dim UnderlyingType As Type = GetUnderlyingType(TargetType )
If UnderlyingType.IsEnum Then
Dim ParsedEnumExpression As MethodCallExpression = GetEnumParseExpression(SourceExpression, UnderlyingType)
'Enum.Parse returns an object that needs to be unboxed
Return Expression.Unbox(ParsedEnumExpression, TargetType )
Else
Dim ParseExpression As Expression
Select Case UnderlyingType
Case GetType(Byte), GetType(UInt16), GetType(UInt32), GetType(UInt64), GetType(SByte), GetType(Int16), GetType(Int32), GetType(Int64), GetType(Single), GetType(Double), GetType(Decimal)
ParseExpression = GetNumberParseExpression(SourceExpression, UnderlyingType, Culture)
Case GetType(DateTime)
ParseExpression = GetDateTimeParseExpression(SourceExpression, UnderlyingType, Culture)
Case GetType(Boolean), GetType(Char)
ParseExpression = GetGenericParseExpression(SourceExpression, UnderlyingType)
Case Else
Throw New ArgumentException(String.Format("Conversion from {0} to {1} is not supported", "String", TargetType ))
End Select
If Nullable.GetUnderlyingType(TargetType ) Is Nothing Then
Return ParseExpression
Else
'Convert back to nullable if necessary
Return Expression.Convert(ParseExpression, TargetType )
End If
End If
End Function
实际解析是通过调用 Target
属性的 Parse
方法来完成的。
/// <summary>
/// Creates an Expression that parses a string to a number
/// </summary>
/// <param name="SourceExpression"></param>
/// <param name="TargetType "></param>
/// <param name="Provider"></param>
/// <returns></returns>
private static MethodCallExpression GetNumberParseExpression(Expression SourceExpression, Type TargetType , CultureInfo Culture)
{
MethodInfo ParseMetod = TargetType .GetMethod("Parse", new[] { typeof(string), typeof(NumberFormatInfo) });
ConstantExpression ProviderExpression = Expression.Constant(Culture.NumberFormat, typeof(NumberFormatInfo));
MethodCallExpression CallExpression = Expression.Call(ParseMetod, new[] { SourceExpression, ProviderExpression });
return CallExpression;
}
''' <summary>
''' Creates an Expression that parses a string to a number
''' </summary>
''' <param name="SourceExpression"></param>
''' <param name="TargetType "></param>
''' <param name="Culture"></param>
''' <returns></returns>
Private Function GetNumberParseExpression(SourceExpression As Expression, TargetType As Type, Culture As CultureInfo) As MethodCallExpression
Dim ParseMetod As MethodInfo = TargetType .GetMethod("Parse", {GetType(String), GetType(NumberFormatInfo)})
Dim ProviderExpression As ConstantExpression = Expression.Constant(Culture.NumberFormat, GetType(NumberFormatInfo))
Dim CallExpression As MethodCallExpression = Expression.Call(ParseMetod, {SourceExpression, ProviderExpression})
Return CallExpression
End Function
其他 Parse
方法遵循相同的模式,但使用不同的参数。
性能
这里有一些 TargetValueExpression
的 debugview 代码示例。
首先是类型为 int
的 NOT NULL
字段赋值给 int
属性。
.Call $SourceInstance.GetInt32(0)
这里我们有一个函数调用。
不必要地使用可为空的 int
如下所示。
(System.Nullable`1[System.Int32]).Call $SourceInstance.GetInt32(14)
这里我们有一个额外的强制转换。
但是,与将可为空的 int
解析为可能为 null
的 string
进行比较。
.If ( .Call $SourceInstance.IsDBNull(2) ) { null } .Else { (System.Nullable`1[System.Int32]).Call System.Int32.Parse( .Call $SourceInstance.GetString(2), .Constant<System.Globalization.NumberFormatInfo>(System.Globalization.NumberFormatInfo)) }
这里,我们有三个函数调用和一个强制转换。
避免转换对大多数人来说是显而易见的。
但为了性能,我不得不强调,当数据库字段不包含任何 null
值时,将其设为 NOT NULL 的重要性。
历史
- 2013 年 10 月 26 日:v1.0 首次发布
- 2014 年 1 月 14 日:v2.0 完全重写,使用
Expression.MemberInit
创建新实例,而不是在循环中仅设置现有实例的属性。 - 2014 年 1 月 26 日:v2.01 现在处理从
string
到enum
的转换。 - 2014 年 2 月 15 日:v2.02 改进了
null
处理和性能。 - 2014 年 2 月 15 日:v2.03 现在处理从
string
到可为空的enum
的转换。 - 2014 年 2 月 28 日:v2.04 现在通过
Attribute
处理FieldMatching
。 - 2014 年 5 月 23 日:v3.0 升级到 .NET 4.5,现在缓存机制会检查 reader 中字段的名称和类型,因此它可以从不同的
IDataReaders
创建相同类型的实例。 - 2014 年 5 月 28 日:v3.01 现在支持将
string
转换为所有原始类型的转换(解析)。 - 2014 年 6 月 25 日:v3.02 现在支持将除
Char
和DateTime
以外的所有原始类型转换为Boolean
。 - 2014 年 9 月 18 日:v3.03 修复了空 datareaders 的 bug。
- 2014 年 10 月 13 日:v3.04 添加了对基本类型泛型的支持。
- 2014 年 11 月 4 日:v3.05 错误修复,当在 VB 版本中使用 Single DataType 时,以及小幅性能提升。
- 2015 年 1 月 30 日:v3.06 添加了对 Tuples 的支持。
- 2015 年 5 月 4 日:v4.0 遇到了一个棘手的 bug,因为字段名拼写错误导致所有属性都未映射。因此,我添加了一个检查,如果所有属性都未映射,则抛出异常。由于在实例化项时可能需要不对所有属性进行设置,因此我在所有公共方法中添加了一个可选的
MustMapAllProperties
参数,默认值为 true。 - 2016 年 1 月 26 日:v4.01 添加了对 CommandBehavior.SequentialAccess 和 MemoryStreams 的支持。