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

DataReader 的属性映射扩展

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (46投票s)

2013 年 10 月 26 日

CPOL

7分钟阅读

viewsIcon

111707

downloadIcon

3923

DataReader 的属性映射扩展。

引言

和大家一样,我生性懒惰,总是想尽可能少地工作,而且我经常需要从数据库中生成大量的专业报告,所以我想通过使用一个自动映射器来节省所有那些乏味的属性映射工作。市面上有很多映射器,但我想要一个简单的、占用空间小且性能极高的映射器。
另一个设计目标是我希望它能与任何 IDataReader 一起工作。
因此,这个映射器不仅可以与 DBReader(如 SQLDataReaderOracleDataReader)一起使用,还可以与 DataTableReaderStorageStream 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 或 PropertyFieldNameAttribute 来检查 PropertyField 之间是否存在匹配。

/// <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 本身不处理可空类型,但我们需要的所有信息都存在于 SchemaTableIsNull 字段中。

/// <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。这是通过检查 ReaderIsDBNull 属性的值来完成的。

/// <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

转换字段

我们还需要检查 SourceTarget 属性是否为不同类型,如果它们是不同类型,我们需要转换它们。
如果它们是相同类型,我们只需返回 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 代码示例。
首先是类型为 intNOT NULL 字段赋值给 int 属性。

.Call $SourceInstance.GetInt32(0)

这里我们有一个函数调用。

不必要地使用可为空的 int 如下所示。

(System.Nullable`1[System.Int32]).Call $SourceInstance.GetInt32(14)

这里我们有一个额外的强制转换。

但是,与将可为空的 int 解析为可能为 nullstring 进行比较。

.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 现在处理从 stringenum 的转换。
  • 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 现在支持将除 CharDateTime 以外的所有原始类型转换为 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 的支持。
© . All rights reserved.