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.

Pin It on Pinterest

Share This