A propertymapping Extension for DataReaders






4.91/5 (46 votes)
A propertymapping extension for DataReaders
- Download VB source - 5.5 KB
- Download VB demo - 715.1 KB
- Download C# source - 5.5 KB
- Download C# demo - 706.3 KB
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.
/// <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
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
/// <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
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.
/// <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
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="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
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="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
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 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
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.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)) }
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
toenum
- 15th February, 2014: v2.02 Improved
null
handling and performance - 15th February, 2014: v2.03 Now handles conversion from
string
to nullableenum
- 28th February, 2014: v2.04 Now handles
FieldMatching
using anAttribute
- 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 exceptChar
andDateTime
. - 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.