Click here to Skip to main content
Click here to Skip to main content

A propertymapping Extension for DataReaders

, 14 Oct 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
A propertymapping extension for DataReaders

Introduction

Just like everyone else, I'm lazy by nature and want to do as little work as possible, and as I'm doing a lot of specialized reporting from databases, I wanted to save some work doing all those tedious property mappings by using an automapper. There are plenty of mappers around, but I wanted a simple one with a small footprint and a really high performance.
Another design goal was that I wanted it to work with any IDataReader.
So this mapper doesn't just work with DBReaders such as SQLDataReader or OracleDataReader but can just as well be used with a DataTableReader, StorageStream DataReader or why not Sebastien Lorions CSVReader[^].
This means you can map your data to POCOs using any datareader that implements IDataReader.

Using the Code

The public methods are just simple extension methods to IDataReader so using the mapper is really easy.

Just use it like: Reader.AsEnumerable<MyClassInstance>
Or if you want a generic list or LinkedList in one go: Reader.ToList<MyClass>()
Same with Dictionary: Reader.ToDictionary<MyClass => MyClass.Key, MyClass>()

If your data needs to be parsed from string to other primitive types, you might need to specify the CultureInfo of your data.
Like this: Reader.AsEnumerable<MyClassInstance>(MyCultureInfo)

The Mapper

The core of the mapper is a function that creates a delegate that uses the supplied IDataRecord to create an instance of the target class. This delegate is created from a lambda expression that is built using expression trees[^]. After initial creation this delegate is cached with a mapping on both TargetType and the SourceFields of the datareader

If the TargetType is an elementary Type, such as String or Int32, the mapper will use the first field of the DataReader since there isn't any name to map on and it simply doesn't make sense to have more than one field in the Reader.
The Delegate creates an instance of the target and assigns the (converted) value from the DataReader to this instance and returns it to the caller.

If it's a composite Type, there is a double loop where all the fields in the DataRecord are matched with the names of the public properties, fields or an attribute in the class that's going to be populated.
So this is a requirement when using the mapper with composite Types. The fieldnames of the DataReader must match the property/fieldnames or an attribute in the target class.
This matching is not case sensitive, but that's really easy to change if one would want that.

And then, it creates a binding that is used by the memberinit expression that creates the instance.

/// <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)
{
    List<MemberBinding> Bindings = new List<MemberBinding>();
    Type SourceType = typeof(IDataRecord);
    ParameterExpression SourceInstance = Expression.Parameter(SourceType, "SourceInstance");
    Type TargetType = typeof(Target);
    DataTable SchemaTable = ((IDataReader)RecordInstance).GetSchemaTable();
    Expression Body = default(Expression);
    //Find out if SourceType is an elementary Type. (Horrible hack, I bloody hope MSDN isn't lying)
    if (TargetType.FullName.Remove(TargetType.FullName.Length - TargetType.Name.Length) == "System.")
    {
        //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, OK!.
        const int i = 0;
        Expression TargetValueExpression = GetTargetValueExpression(RecordInstance, Culture, SourceType, SourceInstance, SchemaTable, i, 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
        foreach (MemberInfo TargetMember in TargetType.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.ExactBinding))
        {
            for (int i = 0; i <= RecordInstance.FieldCount - 1; i++)
            {
                //Check if the RecordFieldName matches the TargetMember
                if (FieldsMatch(RecordInstance.GetName(i), TargetMember))
                {
                    Type TargetMemberType = GetTargetMemberType(TargetMember);
                    Expression TargetValueExpression = GetTargetValueExpression(RecordInstance, Culture, SourceType, SourceInstance, SchemaTable, i, TargetMemberType);

                    //Create a binding to the target member
                    MemberAssignment BindExpression = Expression.Bind(TargetMember, TargetValueExpression);
                    Bindings.Add(BindExpression);
                    break;
                }
            }
        }
        //Create a memberInitExpression that Creates a new instance of Target using bindings to the DataRecord
        Body = Expression.MemberInit(Expression.New(TargetType), Bindings);
    }
    //Compile the Expression to a Delegate
    return Expression.Lambda<Func<IDataRecord, Target>>(Body, SourceInstance).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) As Func(Of IDataRecord, Target)
    Dim Bindings As New List(Of MemberBinding)
    Dim SourceType As Type = GetType(IDataRecord)
    Dim SourceInstance As ParameterExpression = Expression.Parameter(SourceType, "SourceInstance")
    Dim TargetType As Type = GetType(Target)
    Dim SchemaTable As DataTable = DirectCast(RecordInstance, IDataReader).GetSchemaTable
    Dim Body As Expression
    'Find out if SourceType is an elementary Type. (Horrible hack, I bloody hope MSDN isn't lying)
    If TargetType.FullName.Remove(TargetType.FullName.Length - TargetType.Name.Length) = "System." 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, OK!.
        Const i As Integer = 0
        Dim TargetValueExpression As Expression = GetTargetValueExpression(RecordInstance, Culture, SourceType, SourceInstance, SchemaTable, i, 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
        For Each TargetMember As MemberInfo In TargetType.GetMembers(BindingFlags.Public Or BindingFlags.Instance Or BindingFlags.ExactBinding)
            For i As Integer = 0 To RecordInstance.FieldCount - 1
                'Check if the RecordFieldName matches the TargetMember
                If FieldsMatch(RecordInstance.GetName(i), TargetMember) Then
                    Dim TargetMemberType As Type = GetTargetMemberType(TargetMember)
                    Dim TargetValueExpression As Expression = GetTargetValueExpression(RecordInstance, Culture, SourceType, SourceInstance, SchemaTable, i, TargetMemberType)

                    'Create a binding to the target member
                    Dim BindExpression As MemberAssignment = Expression.Bind(TargetMember, TargetValueExpression)
                    Bindings.Add(BindExpression)
                    Exit For
                End If
            Next
        Next
        'Create a memberInitExpression that Creates a new instance of Target using bindings to the DataRecord
        Body = Expression.MemberInit(Expression.[New](TargetType), Bindings)
    End If
    'Compile the Expression to a Delegate
    Return Expression.Lambda(Of Func(Of IDataRecord, Target))(Body, SourceInstance).Compile()
End Function

Here's the function that checks whether the fields match or not.
It checks whether the TargetMember is a Field or a Property, whether this property can be written to and if it has a FieldNameAttribute.
Only one FieldNameAttribute is allowed per member.
Then, it checks if the RecordFieldName matches either the FieldNameAttribute or the Field/PropertyName.
The FieldNameAttribute takes priority.

/// <summary>
/// Checks if the Field matches the Member
/// </summary>
/// <param name="FieldName">The Name of the Field</param>
/// <param name="Member">The Member of the Instance</param>
/// <returns>True if Fields match</returns>
/// <remarks>FieldNameAttribute takes precedence over Members name.</remarks>
private static bool FieldsMatch(string FieldName, MemberInfo Member)
{
    string _Fieldname = string.Empty;
    switch (Member.MemberType)
    {
        case MemberTypes.Field:
            FieldInfo FieldInfo = (FieldInfo)Member;
            if (FieldInfo.GetCustomAttributes(typeof(FieldNameAttribute), true).Count() > 0)
            {
                _Fieldname = ((FieldNameAttribute)FieldInfo.GetCustomAttributes(typeof(FieldNameAttribute), true)[0]).FieldName;
            }
            break;
        case MemberTypes.Property:
            if (((PropertyInfo)Member).CanWrite)
            {
                PropertyInfo PropertyInfo = (PropertyInfo)Member;
                if (PropertyInfo.GetCustomAttributes(typeof(FieldNameAttribute), true).Count() > 0)
                {
                    _Fieldname = ((FieldNameAttribute)PropertyInfo.GetCustomAttributes(typeof(FieldNameAttribute), true)[0]).FieldName;
                }
            }
            else
            {
                return false;
            }
            break;
    }
    return _Fieldname.ToLower() == FieldName.ToLower() || Member.Name.ToLower() == FieldName.ToLower();
}
''' <summary>
''' Checks if the Field matches the Member
''' </summary>
''' <param name="FieldName">The Name of the Field</param>
''' <param name="Member">The Member of the Instance</param>
''' <returns>True if Fields match</returns>
''' <remarks>FieldNameAttribute takes precedence over Members name.</remarks>
Private Function FieldsMatch(ByVal FieldName As String, ByVal Member As MemberInfo) As Boolean
    Dim _Fieldname As String = String.Empty
    Select Case Member.MemberType
        Case MemberTypes.Field
            Dim FieldInfo As FieldInfo = DirectCast(Member, FieldInfo)
            If FieldInfo.GetCustomAttributes(GetType(FieldNameAttribute), True).Count > 0 Then
                _Fieldname = DirectCast(FieldInfo.GetCustomAttributes(GetType(FieldNameAttribute), True)(0), FieldNameAttribute).FieldName
            End If
        Case MemberTypes.Property
            If DirectCast(Member, PropertyInfo).CanWrite Then
                Dim PropertyInfo As PropertyInfo = DirectCast(Member, PropertyInfo)
                If PropertyInfo.GetCustomAttributes(GetType(FieldNameAttribute), True).Count > 0 Then
                    _Fieldname = DirectCast(PropertyInfo.GetCustomAttributes(GetType(FieldNameAttribute), True)(0), FieldNameAttribute).FieldName
                End If
            Else
                Return False
            End If
    End Select

    Return _Fieldname.ToLower = FieldName.ToLower OrElse Member.Name.ToLower = FieldName.ToLower
End Function

The addition to use an Attribute for field matching is done per suggestion from Duncan Edward Jones. (See the comment section below.)

[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

You use it by simply adding the attribute to a property or field like this:

[FieldName("Shipping Country")]
public CountryEnum? ShipCountry { get; set; }
<FieldName("Shipping Country")> _
Public Property ShipCountry As CountryEnum?

For each mapped property, we need to check whether the source is nullable or not, the reason that this is important is performance.
If we know that the source does not contain nulls, the assignment can be simplified.
And if the source is null, we assign the Target's default value.

IDataReaders do not handle nullables as such but all the information we need exists in the SchemaTable and the IsNull field.

/// <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 SourceType, 
    ParameterExpression SourceInstance, 
    DataTable SchemaTable, 
    int i, 
    Type TargetMemberType)
{
    Type RecordFieldType = RecordInstance.GetFieldType(i);
    bool AllowDBNull = Convert.ToBoolean(SchemaTable.Rows[i]["AllowDBNull"]);
    Expression RecordFieldExpression = GetRecordFieldExpression(SourceType, SourceInstance, i, RecordFieldType);
    Expression ConvertedRecordFieldExpression = GetConversionExpression(RecordFieldType, RecordFieldExpression, TargetMemberType, Culture);
    MethodCallExpression NullCheckExpression = GetNullCheckExpression(SourceType, SourceInstance, i);

    //Create an expression that assigns the converted value to the target
    Expression TargetValueExpression = default(Expression);
    if (AllowDBNull)
    {
        TargetValueExpression = Expression.Condition(
            NullCheckExpression, 
            Expression.Constant(GetDefaultValue(TargetMemberType), 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 SourceType As Type,
                                            ByVal SourceInstance As ParameterExpression,
                                            ByVal SchemaTable As DataTable,
                                            ByVal i As Integer,
                                            ByVal TargetMemberType As Type) As Expression
    Dim RecordFieldType As Type = RecordInstance.GetFieldType(i)
    Dim AllowDBNull As Boolean = CBool(SchemaTable.Rows(i).Item("AllowDBNull"))
    Dim RecordFieldExpression As Expression = GetRecordFieldExpression(SourceType, SourceInstance, i, RecordFieldType)
    Dim ConvertedRecordFieldExpression As Expression = GetConversionExpression(RecordFieldType, RecordFieldExpression, TargetMemberType, Culture)
    Dim NullCheckExpression As MethodCallExpression = GetNullCheckExpression(SourceType, SourceInstance, i)

    'Create an expression that assigns the converted value to the target
    Dim TargetValueExpression As Expression
    If AllowDBNull Then
        TargetValueExpression = Expression.Condition(
            NullCheckExpression,
            Expression.Constant(GetDefaultValue(TargetMemberType), TargetMemberType),
            ConvertedRecordFieldExpression,
            TargetMemberType
            )
    Else
        TargetValueExpression = ConvertedRecordFieldExpression
    End If
    Return TargetValueExpression
End Function

Here we check if the RecordValue is null. It's done by checking the value of the IsDBNull property of the Reader.

/// <summary>
/// Gets an Expression that checks if the current RecordField is null
/// </summary>
/// <param name="SourceType">The Type of the Record</param>
/// <param name="SourceInstance">The Record instance</param>
/// <param name="i">The index of the parameter</param>
/// <returns>MethodCallExpression</returns>
private static MethodCallExpression GetNullCheckExpression(Type SourceType, ParameterExpression SourceInstance, int i)
{
    MethodInfo GetNullValueMethod = SourceType.GetMethod("IsDBNull", new Type[] { typeof(int) });
    MethodCallExpression NullCheckExpression = Expression.Call(SourceInstance, GetNullValueMethod, Expression.Constant(i, typeof(int)));
    return NullCheckExpression;
}
''' <summary>
''' Gets an Expression that checks if the current RecordField is null
''' </summary>
''' <param name="SourceType">The Type of the Record</param>
''' <param name="SourceInstance">The Record instance</param>
''' <param name="i">The index of the parameter</param>
''' <returns>MethodCallExpression</returns>
Private Function GetNullCheckExpression(ByVal SourceType As Type, ByVal SourceInstance As ParameterExpression, ByVal i As Integer) As MethodCallExpression
    Dim GetNullValueMethod As MethodInfo = SourceType.GetMethod("IsDBNull", New Type() {GetType(Integer)})
    Dim NullCheckExpression As MethodCallExpression = Expression.[Call](SourceInstance, GetNullValueMethod, Expression.Constant(i, GetType(Integer)))
    Return NullCheckExpression
End Function

We also need to create a SourceExpression from the RecordField.
If we use the proper getter method from the Reader, we can avoid some boxing and casting operations.

/// <summary>
/// Gets an Expression that represents the getter method for the RecordField
/// </summary>
/// <param name="SourceType">The Type of the Record</param>
/// <param name="SourceInstance">The Record instance</param>
/// <param name="i">The index of the parameter</param>
/// <param name="RecordFieldType">The Type of the RecordField</param>
/// <returns></returns>
private static Expression GetRecordFieldExpression(Type SourceType, ParameterExpression SourceInstance, int i, Type RecordFieldType)
{
    MethodInfo GetValueMethod = default(MethodInfo);

    switch (RecordFieldType.Name )
    {
        case "Boolean" :
            GetValueMethod = SourceType.GetMethod("GetBoolean", new Type[] { typeof(int) });
            break;
        case "Byte" :
            GetValueMethod = SourceType.GetMethod("GetByte", new Type[] { typeof(int) });
            break;
        case "Char" :
            GetValueMethod = SourceType.GetMethod("GetChar", new Type[] { typeof(int) });
            break;
        case "DateTime" :
            GetValueMethod = SourceType.GetMethod("GetDateTime", new Type[] { typeof(int) });
            break;
        case "Decimal" :
            GetValueMethod = SourceType.GetMethod("GetDecimal", new Type[] { typeof(int) });
            break;
        case "Double" :
            GetValueMethod = SourceType.GetMethod("GetDouble", new Type[] { typeof(int) });
            break;
        case "Single" :
            GetValueMethod = SourceType.GetMethod("GetFloat", new Type[] { typeof(int) });
            break;
        case "Guid" :
            GetValueMethod = SourceType.GetMethod("GetGuid", new Type[] { typeof(int) });
            break;
        case "Int16" :
            GetValueMethod = SourceType.GetMethod("GetInt16", new Type[] { typeof(int) });
            break;
        case "Int32" :
            GetValueMethod = SourceType.GetMethod("GetInt32", new Type[] { typeof(int) });
            break;>
        case "Int64":
            GetValueMethod = SourceType.GetMethod("GetInt64", new Type[] { typeof(int) });
            break;
        case "String":
            GetValueMethod = SourceType.GetMethod("GetString", new Type[] { typeof(int) });
            break;
        default:
            GetValueMethod = SourceType.GetMethod("GetValue", new Type[] { typeof(int) });
            break;
    }

    Expression RecordFieldExpression = Expression.Call(SourceInstance, GetValueMethod, Expression.Constant(i, typeof(int)));
    return RecordFieldExpression;
}
''' <summary>
''' Gets an Expression that represents the getter method for the RecordField
''' </summary>
''' <param name="SourceType">The Type of the Record</param>
''' <param name="SourceInstance">The Record instance</param>
''' <param name="i">The index of the parameter</param>
''' <param name="RecordFieldType">The Type of the RecordField</param>
''' <returns></returns>
Private Function GetRecordFieldExpression(ByVal SourceType As Type, ByVal SourceInstance As ParameterExpression, ByVal i As Integer, RecordFieldType As Type) As Expression
    Dim GetValueMethod As MethodInfo

    Select Case RecordFieldType
        Case GetType(Boolean)
            GetValueMethod = SourceType.GetMethod("GetBoolean", New Type() {GetType(Integer)})
        Case GetType(Byte)
            GetValueMethod = SourceType.GetMethod("GetByte", New Type() {GetType(Integer)})
        Case GetType(Char)
            GetValueMethod = SourceType.GetMethod("GetChar", New Type() {GetType(Integer)})
        Case GetType(DateTime)
            GetValueMethod = SourceType.GetMethod("GetDateTime", New Type() {GetType(Integer)})
        Case GetType(Decimal)
            GetValueMethod = SourceType.GetMethod("GetDecimal", New Type() {GetType(Integer)})
        Case GetType(Double)
            GetValueMethod = SourceType.GetMethod("GetDouble", New Type() {GetType(Integer)})
        Case GetType(Single)
            GetValueMethod = SourceType.GetMethod("GetFloat", New Type() {GetType(Integer)})
        Case GetType(Guid)
            GetValueMethod = SourceType.GetMethod("GetGuid", New Type() {GetType(Integer)})
        Case GetType(Int16)
            GetValueMethod = SourceType.GetMethod("GetInt16", New Type() {GetType(Integer)})
        Case GetType(Int32)
            GetValueMethod = SourceType.GetMethod("GetInt32", New Type() {GetType(Integer)})
        Case GetType(Int64)
            GetValueMethod = SourceType.GetMethod("GetInt64", New Type() {GetType(Integer)})
        Case GetType(String)
            GetValueMethod = SourceType.GetMethod("GetString", New Type() {GetType(Integer)})
        Case Else
            GetValueMethod = SourceType.GetMethod("GetValue", New Type() {GetType(Integer)})
    End Select

    Dim RecordFieldExpression As Expression = Expression.[Call](SourceInstance, GetValueMethod, Expression.Constant(i, GetType(Integer)))
    Return RecordFieldExpression
End Function

Converting the Fields

We also need to check if the Source and Target properties are of different types, and if they are we need to convert them.
If they are the same type, we only simply return the Source property.
But if they are different, we also need to cast them from the SourceType to the TargetType.

The built in Expression.Convert can handle all implicit and explicit casts, but there are two special cases that need to be handled here.
There are no operators for converting primitive types to String. So if we were to try this, the function would throw an exception.
So this is handled by calling the ToString method of the source. ToString is not the same as a type conversion but for any primitive type, it will do fine.
The other case is the conversion from String to other primitive types and enum, this is handled by parsing the String in a different method.

/// <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 ConvertedRecordFieldExpression;
    if (object.ReferenceEquals(TargetType, SourceType))
    {
        //Just assign the RecordField
        ConvertedRecordFieldExpression = SourceExpression;
    }
    else if (object.ReferenceEquals(SourceType, typeof(string)))
    {
        ConvertedRecordFieldExpression = 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.
        ConvertedRecordFieldExpression = Expression.Call(SourceExpression, SourceType.GetMethod("ToString", Type.EmptyTypes));
    }
    else if (object.ReferenceEquals(TargetType, typeof(bool)))
    {
        MethodInfo ToBooleanMethod = typeof(Convert).GetMethod("ToBoolean", new[] { SourceType });
        ConvertedRecordFieldExpression = Expression.Call(ToBooleanMethod, SourceExpression);
    }
    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.
        ConvertedRecordFieldExpression = Expression.Convert(SourceExpression, TargetType);
    }
    return ConvertedRecordFieldExpression;
} 
''' <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)
    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

Different types use different Parse methods so we have to use a Switch to choose the right method.
All Numbers actually use the same method, but since Number is an internal Class in the .NET Framework, the Switch becomes a bit verbose.

/// <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.Single":
            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
        

The actual parsing is done by calling the Parse Method of the Target property.

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

The other Parse methods follow the same pattern, but use different parameters.

Performance

Here are some examples of the debugview code for the TargetValueExpression.
First the assignment of a NOT NULL field of the type int to an int property.

.Call $SourceInstance.GetInt32(0)

Here we have one function call.

The unnecessary use of a nullable int looks like this:

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

Here we're having an extra cast.

But compare this with the parsing of a string that can be null to a nullable int.

.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))
}</system.globalization.numberformatinfo>

Here, we have three function calls and a cast.

Trying to avoid conversions is obvious for most.
But for the sake of performance, I can't stress enough the importance of making the database fields NOT NULL when they don't contain any null values.

Todo

Supporting streams would be nice.

History

  • 26th October, 2013: v1.0 First release
  • 14th January, 2014: v2.0 Complete rewrite to use Expression.MemberInit to create a new instance instead of merely setting the properties of an existing instance in a loop
  • 26th January, 2014: v2.01 Now handles conversion from string to enum
  • 15th February, 2014: v2.02 Improved null handling and performance
  • 15th February, 2014: v2.03 Now handles conversion from string to nullable enum
  • 28th February, 2014: v2.04 Now handles FieldMatching using an Attribute
  • 23th May, 2014: v3.0 Upgraded to .NET 4.5.1, Now the caching mechanism checks name and type of the fields in the reader and therefore it can create instances of the same type from different IDataReaders
  • 28th May, 2014: v3.01 Now supports conversion (parsing) of strings to all primitive types
  • 25th June, 2014: v3.02 Now supports conversion to Boolean from all primitive types except Char and DateTime.
  • 18th September, 2014: v3.03 Fixed bug with empty datareaders.
  • 13th Oktober, 2014: v3.04 Added support for elementary type generics.
  • 4th November, 2014: v3.05 Bugfix, when using Single DataType in the VB version, and small performance enhancement.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Jörgen Andersson
Database Developer
Sweden Sweden
No Biography provided

Comments and Discussions

 
GeneralMy Vote of 5 Pinprofessionalaarif moh shaikh15-Oct-14 4:00 
QuestionMy vote of 5 Pinmemberdagware19-Mar-14 4:28 
AnswerRe: My vote of 5 PinprofessionalJörgen Andersson20-Mar-14 0:41 
AnswerRe: My vote of 5 Pinprofessionalaarif moh shaikh15-Oct-14 4:00 
QuestionCote of 5 and thoughts PinmemberDuncan Edwards Jones26-Feb-14 1:28 
AnswerRe: Cote of 5 and thoughts PinprofessionalJörgen Andersson26-Feb-14 2:33 
GeneralRe: Cote of 5 and thoughts PinmemberDuncan Edwards Jones26-Feb-14 2:37 
GeneralRe: Cote of 5 and thoughts PinprofessionalJörgen Andersson26-Feb-14 2:43 
GeneralRe: Cote of 5 and thoughts PinprofessionalJörgen Andersson26-Feb-14 12:34 
GeneralRe: Cote of 5 and thoughts PinprofessionalJörgen Andersson28-Feb-14 11:14 
GeneralRe: Cote of 5 and thoughts PinprofessionalDuncan Edwards Jones15-Sep-14 4:31 
QuestionNullable object must have a value. Pinmemberleopar3430-Jan-14 14:22 
AnswerRe: Nullable object must have a value. PinprofessionalJörgen Andersson30-Jan-14 21:58 
GeneralRe: Nullable object must have a value. PinmemberEnigmaticatious16-Sep-14 17:57 
GeneralMy vote of 5 PinmemberM Rayhan15-Jan-14 20:32 
GeneralRe: My vote of 5 PinprofessionalJörgen Andersson15-Jan-14 22:27 
GeneralThoughts PinprofessionalPIEBALDconsult28-Oct-13 19:26 
GeneralRe: Thoughts PinmemberJörgen Andersson29-Oct-13 12:39 
GeneralRe: Thoughts PinprofessionalPIEBALDconsult29-Oct-13 14:34 
GeneralRe: Thoughts PinprofessionalJörgen Andersson27-Jan-14 5:53 
GeneralRe: Thoughts PinmemberPIEBALDconsult27-Jan-14 6:24 
GeneralRe: Thoughts PinprofessionalJörgen Andersson27-Jan-14 9:46 
GeneralRe: Thoughts PinmemberPIEBALDconsult27-Jan-14 10:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141216.1 | Last Updated 14 Oct 2014
Article Copyright 2013 by Jörgen Andersson
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid