1

I'm not even sure if this is possible.. In a method I am creating a dynamic assembly, defining a type, and emitting IL for a constructor for that type. This method takes an IEnumerable<Action> as a parameter and I'd like to be able to use that reference inside the class I am generating.

I've written some database migration helpers to work with either FluentMigrator or MigratorDotNet, and I am trying to implement unit tests to verify correct function. With FluentMigrator I am able to instantiate a runner and pass it instances of Migration classes. With MigratorDotNet, however, it requires me to pass it an assembly that it scans for Migration class to instantiate and run - hence the dynamic generation.

This is the base class I'm dynamically sub-classing:

    public class ActionMigration : Migration
    {
        private readonly IEnumerable<Action<Migration>> _up;
        private readonly IEnumerable<Action<Migration>> _down;
        public ActionMigration(Action<Migration> migration) : this(migration, migration) { }
        public ActionMigration(Action<Migration> up, Action<Migration> down) : this(new[] { up }, new[] { down }) { }
        public ActionMigration(IEnumerable<Action<Migration>> actions) : this(actions, actions) { }
        public ActionMigration(IEnumerable<Action<Migration>> up, IEnumerable<Action<Migration>> down) { _up = up; _down = down; }
        public override void Down() => _down?.ForEach(m => m(this));
        public override void Up() => _up?.ForEach(m => m(this));
    }

This is my code generating the dynamic implementation:

    private Assembly BuildMigrationAssembly(IEnumerable<Action<Migration>> actions)
    {
        var assemblyName = $"mdn_test_{Guid.NewGuid()}";
        var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(assemblyName), AssemblyBuilderAccess.RunAndSave);
        var moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyBuilder.GetName().Name, assemblyName + ".dll");
        BuildMigrationClass(moduleBuilder, 1, actions);

        return assemblyBuilder;
    }

    private void BuildMigrationClass(ModuleBuilder moduleBuilder, long version, IEnumerable<Action<Migration>> actions)
    {
        var baseType = typeof(ActionMigration);
        var typeBuilder = moduleBuilder.DefineType($"Migration{version}",
            TypeAttributes.Public | TypeAttributes.Class |
            TypeAttributes.AutoClass | TypeAttributes.AnsiClass |
            TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout,
            baseType);

        var migAttrType = typeof(MigrationAttribute);
        var migAttrCtor = migAttrType.GetConstructor(new[] { typeof(long) });
        typeBuilder.SetCustomAttribute(migAttrCtor, BitConverter.GetBytes(version));

        var baseCtor = baseType.GetConstructor(new[] { typeof(IEnumerable<Action<Migration>>) });
        var ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, null);
        var ilg = ctor.GetILGenerator();
        ilg.Emit(OpCodes.Ldarg_0);
        // how can I pass the local 'actions' object to the base constructor here?
        ilg.Emit(OpCodes.Call, baseCtor);
        ilg.Emit(OpCodes.Nop);
        ilg.Emit(OpCodes.Nop);
        ilg.Emit(OpCodes.Ret);
    }

I opened a project and created some sample sub-classes to inspect:

namespace MdnTest
{
    [Migration(1)]
    public class Migration1 : EasyMigrator.Tests.Integration.Migrators.MigratorDotNet.ActionMigration
    {
        public Migration1() : base(new List<Action<Migration>>()) { }
    }
}

Or:

namespace MdnTest
{
    [Migration(1)]
    public class Migration1 : EasyMigrator.Tests.Integration.Migrators.MigratorDotNet.ActionMigration
    {
        static private readonly IEnumerable<Action<Migration>> _actions;
        public Migration1() : base(_actions) { }
    }
}

This is the IL they generate:

.class public auto ansi beforefieldinit 
  MdnTest.Migration1
    extends [EasyMigrator.Tests]EasyMigrator.Tests.Integration.Migrators.MigratorDotNet/ActionMigration
{
  .custom instance void [Migrator.Framework]Migrator.Framework.MigrationAttribute::.ctor(int64) 
    = (01 00 01 00 00 00 00 00 00 00 00 00 ) // ............
    // int64(1) // 0x0000000000000001

  .method public hidebysig specialname rtspecialname instance void 
    .ctor() cil managed 
  {
    .maxstack 8

    // [14 31 - 14 66]
    IL_0000: ldarg.0      // this
    IL_0001: newobj       instance void class [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Action`1<class [Migrator.Framework]Migrator.Framework.Migration>>::.ctor()
    IL_0006: call         instance void [EasyMigrator.Tests]EasyMigrator.Tests.Integration.Migrators.MigratorDotNet/ActionMigration::.ctor(class [mscorlib]System.Collections.Generic.IEnumerable`1<class [mscorlib]System.Action`1<class [Migrator.Framework]Migrator.Framework.Migration>>)
    IL_000b: nop          

    // [14 67 - 14 68]
    IL_000c: nop          

    // [14 69 - 14 70]
    IL_000d: ret          

  } // end of method Migration1::.ctor
} // end of class MdnTest.Migration1

Or:

.class public auto ansi beforefieldinit 
  MdnTest.Migration1
    extends [EasyMigrator.Tests]EasyMigrator.Tests.Integration.Migrators.MigratorDotNet/ActionMigration
{
  .custom instance void [Migrator.Framework]Migrator.Framework.MigrationAttribute::.ctor(int64) 
    = (01 00 01 00 00 00 00 00 00 00 00 00 ) // ............
    // int64(1) // 0x0000000000000001

  .field private static initonly class [mscorlib]System.Collections.Generic.IEnumerable`1<class [mscorlib]System.Action`1<class [Migrator.Framework]Migrator.Framework.Migration>> _actions

  .method public hidebysig specialname rtspecialname instance void 
    .ctor() cil managed 
  {
    .maxstack 8

    // [15 31 - 15 45]
    IL_0000: ldarg.0      // this
    IL_0001: ldsfld       class [mscorlib]System.Collections.Generic.IEnumerable`1<class [mscorlib]System.Action`1<class [Migrator.Framework]Migrator.Framework.Migration>> MdnTest.Migration1::_actions
    IL_0006: call         instance void [EasyMigrator.Tests]EasyMigrator.Tests.Integration.Migrators.MigratorDotNet/ActionMigration::.ctor(class [mscorlib]System.Collections.Generic.IEnumerable`1<class [mscorlib]System.Action`1<class [Migrator.Framework]Migrator.Framework.Migration>>)
    IL_000b: nop          

    // [15 46 - 15 47]
    IL_000c: nop          

    // [15 48 - 15 49]
    IL_000d: ret          

  } // end of method Migration1::.ctor
} // end of class MdnTest.Migration1

I'm not sure how to adapt this to what I'm trying to achieve.. Can I just plop a reference to this local object that exists outside the context of the dynamic assembly in an IL load instruction? Could Expressions help? Maybe trying to pass it in the constructor is the wrong way to go about it - perhaps instead overriding the up and down implementations (can I get like a function handle to an Action and emit a call to it, rather than pass in the reference to the IEnumerable?).

I'm passingly familiar with assembly and IL and after reviewing the operations I'm starting to think I might not be able to do what I'm attempting to do.

So, is this even possible, and if so can someone nudge me in the right direction?

For the curious, the full code is here.

quentin-starin
  • 26,121
  • 7
  • 68
  • 86
  • Not sure if this gets you closer or not : http://stackoverflow.com/q/8419839 – Cody Lohse Mar 13 '17 at 03:31
  • @CALohse it doesn't. I never did find a way to directly emit any kind of reference to the Action I have at the point I'm emitting IL - but what did work is to store that Action in a static dictionary and emit a call in the dynamic assembly to call back into a static function that returns the correct Action for the dynamic class to then call. I'll have to add an answer here with the code. – quentin-starin Mar 14 '17 at 16:20

1 Answers1

0

You can path reference to object that exist outside dynamic assembly by defining constructor with parameter of type IEnumerable<Action<Migration>> for your dynamic class:

var ctor = typeBuilder.DefineConstructor(
    MethodAttributes.Public,
    CallingConventions.Standard, 
    new[] { typeof(IEnumerable<Action<Migration>>) });

Then use those parameter to path it in base class constructor:

var ilg = ctor.GetILGenerator();
ilg.Emit(OpCodes.Ldarg_0);        // load 'this' onto stack 
ilg.Emit(OpCodes.Ldarg_1);        // load constructor argument onto the stack 
ilg.Emit(OpCodes.Call, baseCtor); // call base constructor
ilg.Emit(OpCodes.Ret);

After this you will be able to create instance of your dynamic class using Activator:

var type = typeBuilder.CreateType();
var args = new object[] { new List<Action<Migration>>() };
var instance = Activator.CreateInstance(type, args);
Andrey Tretyak
  • 3,043
  • 2
  • 20
  • 33
  • @CALohse comment above led to the same sort of solution. The problem is I don't control the instantiation of the class I'm dynamically generating. The migration runner - an MSBuild target or exe - instantiates the class and will always look for a no-argument constructor. – quentin-starin May 07 '17 at 18:05