Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB
Print

A generic Collection to DataTable Mapper

4.91/5 (24 votes)
27 Apr 2015CPOL2 min read 36.3K   1.7K  
A high performance extension for creating a DataTable from a Generic collection.

Introduction

You know how it is, sometimes you're having a very large generic list but what you need is a DataTable, because that old legacy application needs it, or you want to bulkcopy it to the database.

No problems you might think. There's an AsDataTable function in the Linq namespace.
Well yes there is, but it only works with Linq to Dataset.
So what other solutions are there besides hardcoding it every time you need it?
Well, you can use reflection, the problem is just that the performance sucks.

So, since I wasn't able to find anyone having created the functionality, I had to do it myself.

Using the code

There's only one public method, an Extension method called AsDataTable that takes an IEnumerable as a parameter and is simply used like MyGenericList.AsDataTable()

/// <summary>
/// Creates a DataTable from an IEnumerable
/// </summary>
/// <typeparam name="TSource">The Generic type of the Collection</typeparam>
/// <param name="Collection"></param>
/// <returns>DataTable</returns>
public static DataTable AsDataTable<TSource>(this IEnumerable<TSource> Collection)
{
    DataTable dt = DataTableCreator<TSource>.GetDataTable();
    Func<TSource, object[]> Map = DataRowMapperCache<TSource>.GetDataRowMapper(dt);

    foreach (TSource item in Collection)
    {
        dt.Rows.Add(Map(item));
    }
    return dt;
}

This part is pretty simple, it fetches a new DataTable from a cache and fetches a Delegate that acts like a mapper between an Instance and an ObjectArray.
Then the delegate is used on every item in the collection to create an ObjectArray that's added to the DataTables RowCollection.
The reason for adding an Array instead of a DataRow is that the DataRow does not have a public constructor. It was simpler this way.

Creating the DataTable

The construction of the DataTable is done by reflection, since it is cached it's only done once per Item Class and will not affect performance more than once

/// <summary>
/// Creates a DataTable with the same fields as the Generic Type argument
/// </summary>
/// <typeparam name="TSource">The Generic type</typeparam>
/// <returns>DataTable</returns>
static internal DataTable CreateDataTable<TSource>()
{
    DataTable dt = new DataTable();
    foreach (FieldInfo SourceMember in typeof(TSource).GetFields(BindingFlags.Instance | BindingFlags.Public))
    {
        dt.AddTableColumn(SourceMember, SourceMember.FieldType);
    }

    foreach (PropertyInfo SourceMember in typeof(TSource).GetProperties(BindingFlags.Instance | BindingFlags.Public))
    {
        if (SourceMember.CanRead)
        {
            dt.AddTableColumn(SourceMember, SourceMember.PropertyType);
        }
    }
    return dt;
}

What this method does is to loop through all public instance members of the TSource class, and checks if they're fields or readable properties.

If the Member is of a type that is supported by a DataColumn it will be added using the Name, Type and whether it should allow DbNull or not.

/// <summary>
/// Adds a Column to a DataTable
/// </summary>
public static void AddTableColumn(this DataTable dt, MemberInfo SourceMember, Type MemberType)
{
    if (MemberType.IsAllowedType())
    {
        DataColumn dc;
        string FieldName = GetFieldNameAttribute(SourceMember);
        if (string.IsNullOrWhiteSpace(FieldName))
        {
            FieldName = SourceMember.Name;
        }
        if (Nullable.GetUnderlyingType(MemberType) == null)
        {
            dc = new DataColumn(FieldName, MemberType);
            dc.AllowDBNull = !MemberType.IsValueType;
        }
        else
        {
            dc = new DataColumn(FieldName, Nullable.GetUnderlyingType(MemberType));
            dc.AllowDBNull = true;
        }
        dt.Columns.Add(dc);
    }
}

If you would want the DataColumn to have a different name than the Member you can add a FieldNameAttribute to it

[FieldName("Some odd fieldname")]
public string Name { get; set; }

The FieldNameAttribute obviously takes precedence over TargetMembers name

Creating the DataRowMapper

The mapper is created using an Expression Tree and reflection.
It's done by looping through the DataColumns in the DataTable and matching them by name or FieldnameAttribute to the Instanceclass

/// <summary>
/// Creates a delegate that maps an instance of TSource to an ItemArray of the supplied DataTable
/// </summary>
/// <typeparam name="TSource">The Generic Type to map from</typeparam>
/// <param name="dt">The DataTable to map to</param>
/// <returns>Func(Of TSource, Object())</returns>
static internal Func<TSource, object[]> CreateDataRowMapper<TSource>(DataTable dt)
{
    Type SourceType = typeof(TSource);
    ParameterExpression SourceInstanceExpression = Expression.Parameter(SourceType, "SourceInstance");
    List<Expression> Values = new List<Expression>();

    foreach (DataColumn col in dt.Columns)
    {
        foreach (FieldInfo SourceMember in SourceType.GetFields(BindingFlags.Instance | BindingFlags.Public))
        {
            if (MemberMatchesName(SourceMember, col.ColumnName))
            {
                Values.Add(GetSourceValueExpression(SourceInstanceExpression, SourceMember));
                break;
            }
        }
        foreach (PropertyInfo SourceMember in SourceType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
        {
            if (SourceMember.CanRead && MemberMatchesName(SourceMember, col.ColumnName))
            {
                Values.Add(GetSourceValueExpression(SourceInstanceExpression, SourceMember));
                break;
            }
        }
    }
    NewArrayExpression body = Expression.NewArrayInit(Type.GetType("System.Object"), Values);
    return Expression.Lambda<Func<TSource, object[]>>(body, SourceInstanceExpression).Compile();
}

When we have a match we create a MemberExpression representing the value in the Field or Property that we add to an array that's used to create a NewArrayInitExpression.

/// <summary>
/// Creates an Expression representing the value of the SourceMember
/// </summary>
/// <param name="SourceInstanceExpression"></param>
/// <param name="SourceMember"></param>
/// <returns></returns>
private static Expression GetSourceValueExpression(ParameterExpression SourceInstanceExpression, MemberInfo SourceMember)
{
    MemberExpression MemberExpression = Expression.PropertyOrField(SourceInstanceExpression, SourceMember.Name);
    Expression SourceValueExpression;

    if (Nullable.GetUnderlyingType(SourceMember.ReflectedType) == null)
    {
        SourceValueExpression = Expression.Convert(MemberExpression, typeof(object));
    }
    else
    {
        SourceValueExpression = Expression.Condition(
            Expression.Property(Expression.Constant(SourceInstanceExpression), "HasValue"),
            MemberExpression,
            Expression.Constant(DBNull.Value),
            typeof(object));
    }
    return SourceValueExpression;
}

This expression is then compiled into a delegate.

The DataTable and MapperDelegate is then cached to enhance performance

Points of Interest

The creation of the MemberExpression could be done at the same time as the creation of the DataTable, but I've decided against it, to make future enhancements easier

History

  • 25th March, 2015: v1.0 First release
  • 28th April, 2015: v1.1 Some refactoring and typechecking

License

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