Search code examples
c#ililgenerator

Setting an object's sub property using ILGenerator.Emit


I have some code that uses IlGenerator.Emit to create and populate a generic object using a datareader. It works great however I need to extend it to populate simple child objects when the database field name contains an underscore.

For example a database field named "Address_Line1" should populate the property Line1, which is a property of the Address property on the Entity. In C# code that basically...

Entity.Address.Line1 = "value from reader";

I tried writing c# code and used ILSpy to try identify the IL code I should be writing, but I keep getting memory errors etc.

The code below includes the current working IL code and I've included my code attempt with comments. Can anyone help me out?

public static DynamicBuilder<T> CreateBuilder(IDataRecord reader)
{
    var result = new DynamicBuilder<T>();
    var method = new DynamicMethod("DynamicCreate", typeof(T), new Type[] { typeof(IDataReader) }, typeof(T), true);

    var generator = method.GetILGenerator();

    generator.DeclareLocal(typeof(T));
    generator.Emit(OpCodes.Newobj, typeof(T).GetConstructor(Type.EmptyTypes));
    generator.Emit(OpCodes.Stloc_0);

    var getValue = reader.GetType().GetMethod("get_Item", new Type[] { typeof(int) });

    for (int i = 0; i < reader.FieldCount; i++)
    {
        var name = reader.GetName(i).Split('_'); // MY CODE
        var propertyInfo = typeof(T).GetProperty(name[0]);

        if (propertyInfo != null && propertyInfo.GetSetMethod() != null)
        {
            var endIfLabel = generator.DefineLabel();

            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldc_I4, i);
            generator.Emit(OpCodes.Callvirt, typeof(IDataRecord).GetMethod("IsDBNull"));
            generator.Emit(OpCodes.Brtrue, endIfLabel);

            generator.Emit(OpCodes.Ldloc_0);
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldc_I4, i);
            generator.Emit(OpCodes.Callvirt, getValue);

            if (propertyInfo.PropertyType.Name.ToLower().Contains("nullable"))
                generator.Emit(OpCodes.Unbox_Any, GetNullableType(reader.GetFieldType(i)));
            else
                generator.Emit(OpCodes.Unbox_Any, reader.GetFieldType(i));

            // START MY CODE TO GET THE SUB PROPERTY
            if (name.Length > 1)
            {
                generator.Emit(OpCodes.Callvirt, propertyInfo.GetGetMethod());
                propertyInfo = propertyInfo.PropertyType.GetProperty(name[1]);
            }
            // END MY CODE

            generator.Emit(OpCodes.Callvirt, propertyInfo.GetSetMethod());
            generator.MarkLabel(endIfLabel);
        }
    }

    generator.Emit(OpCodes.Ldloc_0);
    generator.Emit(OpCodes.Ret);

    result.handler = (Load)method.CreateDelegate(typeof(Load));
    return result;
}

Solution

  • Code like this:

    static Entity DynamicCreate(IDataReader reader)
    {
        var entity = new Entity();
        entity.Property = (int)reader[0];
        return entity;
    }
    

    is compiled to IL that looks exactly like the code you're emitting (unimportant parts omitted):

    ldloc.0     // entity
    ldarg.0     // reader
    ldc.i4.0    
    callvirt    System.Data.IDataRecord.get_Item
    unbox.any   System.Int32
    callvirt    UserQuery+Entity.set_Property
    

    But if you add that second property access:

    static Entity DynamicCreate(IDataReader reader)
    {
        var entity = new Entity();
        entity.SubEntity.Property = (int)reader[0];
        return entity;
    }
    

    Then the IL looks like this:

    ldloc.0     // entity
    callvirt    UserQuery+Entity.get_SubEntity
    ldarg.0     // reader
    ldc.i4.0    
    callvirt    System.Data.IDataRecord.get_Item
    unbox.any   System.Int32
    callvirt    UserQuery+SubEntity.set_Property
    

    Notice that the call to get_SubEntity is between ldloc.0 and ldarg.0, not right before set_Property like in your code, so you have to move it there in your code too.

    The reason your code doesn't work is that IL is a stack based language: when you call a parameterless instance method (like a property getter), the object on the top of the stack (which here is the result of the unbox.any) will be used as its this, which is not what you want here. Basically, your code tries to do something like:

    entity.Property = ((int)reader[0]).SubEntity;