C#で動的にインターフェイスを実装するクラスを作成する(メソッド呼び出し)
はじめに
前回記事の続きです。
前回はGetter, Setterを持つシンプルなI/Fを実装する例でしたが、今回はメソッドを持つI/Fを実装します。そして、そのメソッドは別で実装されたメソッドを呼び出します(ベタ書きですが、疑似的なモック化を狙っています)。
以下、全コードのサンプルになります。
サンプル
IMyMockTypeを実現するMyMockTypeを作成、インスタンス化する
using System.Reflection;
using System.Reflection.Emit;
namespace DemoAssemblyBuilder
{
/// <summary>
/// Mock検証用インターフェイス
/// </summary>
public interface IMyMockType
{
/// <summary>
/// 計算結果を返します
/// </summary>
/// <param name="number">入力値</param>
/// <returns>計算結果</returns>
int Calculate(int number);
}
public class Program
{
static void Main(string[] args)
{
// 以下のクラスを動的に作成します
/*
namespace MyExample
{
public class MyMockType : IMyMockType
{
public int Calculate(int number)
{
return ...置き換えた処理を実行します...;
}
}
}
*/
// アセンブリの定義
AssemblyName assemblyName = new AssemblyName("MyExample");
AssemblyBuilder assemblyBuilder =
AssemblyBuilder.DefineDynamicAssembly(
assemblyName,
AssemblyBuilderAccess.Run);
// モジュールの定義
// モジュール名は多くの場合アセンブリ名と同じにすると良い
ModuleBuilder moduleBuilder =
assemblyBuilder.DefineDynamicModule(assemblyName.Name!);
// 型情報の定義
// 以降、MyMockTypeを作っていきます
TypeBuilder typeBuilder = moduleBuilder.DefineType(
"MyMockType",
TypeAttributes.Public);
// IMyMockTypeを実現します
Type myInterfaceType = typeof(IMyMockType);
typeBuilder.AddInterfaceImplementation(myInterfaceType);
// Calculateメソッドを定義します
MethodBuilder meth = typeBuilder.DefineMethod(
"Calculate",
MethodAttributes.Public | MethodAttributes.Virtual,
typeof(int),
new Type[] { typeof(int) });
// 引数を受取り、その引数を呼び出すメソッドの引数に渡します
ILGenerator methIL = meth.GetILGenerator();
methIL.Emit(OpCodes.Ldarg_1);
var methodInfo = typeof(Program).GetMethod(nameof(MockFunc), BindingFlags.Static | BindingFlags.Public);
methIL.Emit(OpCodes.Call, methodInfo!);
methIL.Emit(OpCodes.Ret);
typeBuilder.DefineMethodOverride(meth, typeof(IMyMockType).GetMethod("Calculate")!);
// 以降、動作確認
Type myMockType = typeBuilder.CreateType()!;
IMyMockType mock = (IMyMockType)Activator.CreateInstance(myMockType)!;
Console.WriteLine($"mock.Calculate : " + mock.Calculate(3));
Console.ReadKey();
}
/// <summary>
/// モック化したい処理の実装です
/// </summary>
/// <param name="number">入力値</param>
/// <returns>計算結果</returns>
public static int MockFunc(int number)
{
return number * number;
}
}
}
個人的な学習ポイント
- OpCodes.Callの場合、MethodInfoを引数に渡して呼び出すことができる(他にもOpCodes.Callvirt, OpCodes.Newobjも大丈夫そうなのですが、他のOpCodesの意味は分かってないです)
-
前回記事ではGetter, Setterで毎回
methIL.Emit(OpCodes.Ldarg_0);
と書いていたのでお約束的に必要なのかと思ったら、呼び出し元(this)が必要な場合のみ書くらしい
終わりに
普段は内部ロジックなんて書かないのですが、一回書いてみると新しい発見が見つかりますね。まだまだMoqのようにはいきませんが、MethodInfoを渡せることを知ったので、ラムダ関数の書き方で書いたActionやFuncをメソッドの引数に渡し、そこからMethodInfoを抽出すればMoqのようなことができそうな気がします。