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 DBReader
s 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<MyClass>
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)
Note!
It's recommended to create the Reader with the CommandBehavoirs CloseConnection and KeyInfo.
Like this: Command.ExecuteReader((CommandBehavior)CommandBehavior.CloseConnection | CommandBehavior.KeyInfo)
KeyInfo is needed by the mapper to know if a field is nullable or not. CloseConnection is just a good habit.
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.
But having realized I'm even lazier than previously thought, I have added support for Tuples.
Since PropertyNames like Item1, Item2 and so on makes very little sense to map on, it's simply mapping on position instead.
It doesn't map nested tuples, so seven properties is the maximum.
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);
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);
}
else if (TargetType.IsElementaryType())
{
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
{
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++)
{
if (MemberMatchesName(TargetMember, RecordInstance.GetName(Ordinal)))
{
Expression TargetValueExpression = GetTargetValueExpression(
RecordInstance,
Culture,
RecordType,
RecordInstanceExpression,
SchemaTable,
Ordinal,
TargetMember.FieldType);
MemberAssignment BindExpression = Expression.Bind(TargetMember, TargetValueExpression);
Bindings.Add(Ordinal, BindExpression);
return;
}
}
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++)
{
if (MemberMatchesName(TargetMember, RecordInstance.GetName(Ordinal)))
{
Expression TargetValueExpression = GetTargetValueExpression(
RecordInstance,
Culture,
RecordType,
RecordInstanceExpression,
SchemaTable,
Ordinal,
TargetMember.PropertyType);
MemberAssignment BindExpression = Expression.Bind(TargetMember, TargetValueExpression);
Bindings.Add(Ordinal, BindExpression);
return;
}
}
if (MustMapAllProperties)
{
throw new ArgumentException(String.Format("TargetProperty {0} is not matched by any Field in the DataReader", TargetMember.Name));
}
};
work();
}
}
Body = Expression.MemberInit(Expression.New(TargetType), Bindings.Values);
}
return Expression.Lambda<Func<IDataRecord, Target>>(Body, RecordInstanceExpression).Compile();
}
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
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)
ElseIf TargetType.IsElementaryType() Then
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
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)
Dim BindExpression As MemberAssignment = Expression.Bind(TargetMember, TargetValueExpression)
Bindings.Add(Ordinal, BindExpression)
Exit Do
End If
Next
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
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)
Dim BindExpression As MemberAssignment = Expression.Bind(TargetMember, TargetValueExpression)
Bindings.Add(Ordinal, BindExpression)
Exit Do
End If
Next
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
End If
Next
Body = Expression.MemberInit(Expression.[New](TargetType), Bindings.Values)
End If
Return Expression.Lambda(Of Func(Of IDataRecord, Target))(Body, RecordInstanceExpression).Compile()
End Function
Checking whether there is a match between a Property
and a Field
is done by comparing the Fieldname of the DataReader with the Name or a FieldNameAttribute
of the Property
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;
}
}
private static bool MemberMatchesName(MemberInfo Member, string Name)
{
string FieldnameAttribute = GetFieldNameAttribute(Member);
return FieldnameAttribute.ToLower() == Name.ToLower() || Member.Name.ToLower() == Name.ToLower();
}
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
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
The FieldNameAttribute
takes priority.
The actual Attribute
is shown 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 null
s, the assignment can be simplified.
And if the source is null
, we assign the Target
's default value.
IDataReader
s do not handle nullables as such but all the information we need exists in the SchemaTable
and the IsNull
field.
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);
Expression TargetValueExpression = default(Expression);
if (AllowDBNull)
{
TargetValueExpression = Expression.Condition(
NullCheckExpression,
Expression.Default(TargetMemberType),
ConvertedRecordFieldExpression,
TargetMemberType
);
}
else
{
TargetValueExpression = ConvertedRecordFieldExpression;
}
return TargetValueExpression;
}
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)
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
Here we check if the RecordValue
is null
. It's done by checking the value of the IsDBNull
property of the Reader
.
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;
}
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
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.
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;
}
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
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.
private static Expression GetConversionExpression(Type SourceType, Expression SourceExpression, Type TargetType, CultureInfo Culture)
{
Expression TargetExpression;
if (object.ReferenceEquals(TargetType, SourceType))
{
TargetExpression = SourceExpression;
}
else if (object.ReferenceEquals(SourceType, typeof(string)))
{
TargetExpression = GetParseExpression(SourceExpression, TargetType, Culture);
}
else if (object.ReferenceEquals(TargetType, typeof(string)))
{
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
{
TargetExpression = Expression.Convert(SourceExpression, TargetType);
}
return TargetExpression;
}
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
TargetExpression = SourceExpression
ElseIf SourceType Is GetType(String) Then
TargetExpression = GetParseExpression(SourceExpression, TargetType, Culture)
ElseIf TargetType Is GetType(String) Then
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
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.
private static Expression GetParseExpression(Expression SourceExpression, Type TargetType , CultureInfo Culture)
{
Type UnderlyingType = GetUnderlyingType(TargetType );
if (UnderlyingType.IsEnum)
{
MethodCallExpression ParsedEnumExpression = GetEnumParseExpression(SourceExpression, UnderlyingType);
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
{
return Expression.Convert(ParseExpression, TargetType );
}
}
}
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)
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
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.
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;
}
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))
}
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.
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, 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
string
s 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.
- 30th January, 2015: v3.06 Added support for Tuples
- 4th May, 2015: v4.0 Encountered a nasty bug when all properties wasn't mapped because of a misspelled fieldname. So I have added a check that throws an exception if not all properties have been mapped. As there can be a need to not set all properties at instantiation of an item I have added an optional
MustMapAllProperties
parameter to all public Methods that defaults to true. - 26th January 2016: v4.01 Added support for CommandBehavior.SequentialAccess and MemoryStreams.