Reputation: 26688
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.
Upvotes: 1
Views: 396
Reputation: 3231
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);
Upvotes: 0