How to map an IDataRecord to an entity class object using expressions

blur close up code computer

Recently I had a need to map an IDataRecord to an entity class object in C#. Here is the code for reference… First, we need the method that generates a function to map the properties while handling DBNull.

        private static Func<IDataReader, T> GenerateMapFunction<T>(IDataReader dataReader)
        {
            if (dataReader is null)
            {
                throw new ArgumentNullException(nameof(dataReader));
            }

            var expressions = new List<Expression>();

            var dataReaderParameterExpression = Expression.Parameter(typeof(IDataRecord), "o7thDR");

            var targetExpression = Expression.Variable(typeof(T));

            expressions.Add(Expression.Assign(targetExpression, Expression.New(targetExpression.Type)));

            // does int based lookup
            var indexerInfo = typeof(IDataRecord).GetProperty("Item", new[] { typeof(int) });

            var columnNames = Enumerable.Range(0, dataReader.FieldCount)
                                        .Select(index => new { index, name = dataReader.GetName(index) });

            foreach (var column in columnNames)
            {
                var property = targetExpression.Type.GetProperty(
                    column.name,
                    BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
                if (property is null)
                {
                    continue;
                }

                // index
                var columnIndexExpression = Expression.Constant(column.index);

                // reader[index]
                var readerPropertyExpression = Expression.MakeIndex(
                    dataReaderParameterExpression, indexerInfo, new[] { columnIndexExpression });

                // reader.IsDBNull(index);
                var isReaderDbNull = Expression.Call(
                    dataReaderParameterExpression, nameof(IDataReader.IsDBNull), null, columnIndexExpression);

                // reader[index] as PropertyType;
                var safeCastExpression = Expression.TypeAs(
                    readerPropertyExpression,
                    property.PropertyType);

                // T.Property
                var targetPropertyExpression = Expression.Property(targetExpression, property);

                // T.Property = reader.IsDBNull(index) ? default(PropertyType) :
                // (PropertyType)reader[index] as PropertyType;
                var assignmentBlock = Expression.Condition(
                    Expression.IsTrue(isReaderDbNull),
                    Expression.Assign(
                        targetPropertyExpression,
                        Expression.Default(property.PropertyType)),
                    Expression.Assign(
                        targetPropertyExpression,
                        Expression.Convert(safeCastExpression, property.PropertyType)));

                expressions.Add(assignmentBlock);
            }

            expressions.Add(targetExpression);

            return Expression.Lambda<Func<IDataReader, T>>(
                Expression.Block(
                    new[] { targetExpression },
                    expressions),
                dataReaderParameterExpression).Compile();
        }

Next we need the function that calls the above…

        public static T? Map<T>(this IDataReader dataReader) where T : class
        {
            if (dataReader is null)
            {
                return default;
            }

            //// Use the following loop for debugging field data types in your entity classes.
            //for (var i = 0; i < dataReader.FieldCount; i++)
            //{
            //    Trace.TraceInformation($"{i}:{dataReader.GetName(i)} = {dataReader.GetFieldType(i).Name}");
            //}

            var converter = GenerateMapFunction<T>(dataReader);
            return converter(dataReader);
        }

This does the job for a single row, but we want all the rows in the reader, right? For that we need this one…

        public static List<T> ToList<T>(this IDataReader dataReader) where T : class
{
var list = new List<T>();

if (dataReader is not null)
{
while (dataReader.Read())
{
var row = Map<T>(dataReader);
if (row is not null)
{
list.Add(row);
}
}
}

return list;
}

That’s it… It doesn’t use the async API of the data reader for the read… We might want an async version too…

        public static async Task<List<T>> ToListAsync<T>(
this DbDataReader dataReader,
CancellationToken cancellationToken = default) where T : class
{
var list = new List<T>();

if (dataReader is not null)
{
while (await dataReader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
var row = Map<T>(dataReader);
if (row is not null)
{
list.Add(row);
}
}
}

return list;
}

But what if we don’t want to pull the whole list all at once? Then you need this…

        public static async IAsyncEnumerable<T> EnumerateAsync<T>(
this DbDataReader dataReader,
[EnumeratorCancellation] CancellationToken cancellationToken = default) where T : class
{
if (dataReader is null)
{
yield break;
}

while (await dataReader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
var row = Map<T>(dataReader);
if (row is not null)
{
yield return row;
}
}
}

Oh, but what if you want a blocking version of that?

        public static IEnumerable<T> Enumerate<T>(this DbDataReader dataReader) where T : class
{
if (dataReader is null)
{
yield break;
}

while (dataReader.Read())
{
var row = Map<T>(dataReader);
if (row is not null)
{
yield return row;
}
}
}

Hopefully, part of this will come in handy for someone as it was for me.

The code for this is available on GitHub and via NuGet package.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Pin It on Pinterest

Share This