Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic Relational to Composite C# Object Mapper

Tags:

c#

datareader

I have following code that's capable of mapping Reader to simple objects. The trouble is in case the object is composite it fails to map. I am not able to perform recursion by checking the property if it is a class itself

prop.PropertyType.IsClass as Type is required to call DataReaderMapper(). Any idea on how this may be achieved or some other approach? Also, currently I am not wishing to use any ORM.

public static class MapperHelper
{

    /// <summary>
    /// extension Method for Reader :Maps reader to type defined
    /// </summary>
    /// <typeparam name="T">Generic type:Model Class Type</typeparam>
    /// <param name="dataReader">this :current Reader</param>
    /// <returns>List of Objects</returns>
    public static IEnumerable<T> DataReaderMapper<T>(this IDataReader dataReader)where T : class, new()
    {
        T obj = default(T);

        //optimized taken out of both foreach and while loop
        PropertyInfo[] PropertyInfo;
        var temp = typeof(T);
        PropertyInfo = temp.GetProperties();

        while (dataReader.Read())
        {  
            obj = new T();

            foreach (PropertyInfo prop in PropertyInfo)
            {
                if (DataConverterHelper.ColumnExists(dataReader,prop.Name) && !dataReader.IsDBNull(prop.Name))
                {
                    prop.SetValue(obj, dataReader[prop.Name], null);
                }
            }
            yield return obj;

        }
    }
}
like image 271
Abdul Khan Avatar asked May 14 '15 06:05

Abdul Khan


2 Answers

Don't make DataReaderMapper recursive. Just make the mapping part recursive:

static void Assign(IDataReader reader, object instance) {
        foreach (PropertyInfo prop in PropertyInfo)
        {
            if (IsValue(prop))
            {
                prop.SetValue(obj, dataReader[prop.Name], null);
            }
            else if (IsClass(prop)) {
               var subInstance = Activator.CreateInstance(prop.PropertyType);
                prop.SetValue(obj, subInstance, null);
               Assign(subInstance, dataReader);
            }
        }
}

Like that. This recursively initializes all class type properties with default constructed instances and assigns data reader values to them.

The code is clearly simplified. I elided some of your stuff and IsValue/IsClass are left to implement to your liking. Also, you probably want to use a naming scheme so that a.b.c as a column name maps to that property. That's doable by passing the current name prefix as a parameter to Assign.

Further note, that DataReaderMapper being generic isn't required. I'm saying this because you struggled with that. Replace typeof(T) with a Type parameter and return an IEnumerable<object>. Then call Cast<T>() on the result of your method. So you see that this algorithm can in principle work without generics.

like image 123
usr Avatar answered Nov 15 '22 19:11

usr


My preference would be to leave the heavy lifting to the calling code. This avoids relatively slow recursion, and allows you to build objects where the field names don't line up exactly or that don't have default constructors:

public static class MapperHelper
{
    public static IEnumerable<T> DataReaderMapper<T>(this IDataReader dataReader, Func<IDataRecord, T> map)
    {
        while (dataReader.Read())
        {  
            yield return map(dataReader);
        }
    }

Then I would move your existing property code to a method that could be passed here as a default implementation:

    public static T DefaultMapper<T>(IDataRecord record) where T : class, new()
    {
        //This is now effectively inside the while loop,
        // but .Net caches the expensive recursive calls for you
        PropertyInfo[] PropertyInfo;
        var temp = typeof(T);
        PropertyInfo = temp.GetProperties();

        obj = new T();
        foreach (PropertyInfo prop in PropertyInfo)
        {
            if (DataConverterHelper.ColumnExists(dataReader,prop.Name) && !dataReader.IsDBNull(prop.Name))
            {
                prop.SetValue(obj, dataReader[prop.Name], null);
            }
        }
        return obj;
    }
}

You could call it with the default mapper like this:

foreach (var record in myDataReader.DataReaderMapper<SomeType>(MapperHelper.DefaultMapper) )
{
    //...
}

Of course this will still fail on complex composite types, but I don't see how you can expect any generic mapper to succeed in that scenario... if your inner type itself has properties to fill, there's no good way to get specify the matching name. This allows you quickly write your own mapper, if you need to:

foreach (var record in myDataRecord.DataReaderMapper<SomeType>(r => {
    //complex mapping goes here
    SomeType result = new SomeType() {
        field1 = r["field1"],
        field2 = new OtherType() {
           subField = r["subField"],
           otherField = r["otherField"]
        }
    }
    return result;      
})
{
    //...
}

And of course you can always build the logic for that conversion into a method that could instead pass by name.

like image 21
Joel Coehoorn Avatar answered Nov 15 '22 18:11

Joel Coehoorn