大抵の場合ではASP.NETのConfigurationBuilderでAddJsonFileを呼んでファイルパスを指定してJSONファイルを読み込みますが、もしかすると物理的なファイルではなく例えばStringから読み込みたいといった場合があるかもしれません。
そんな時は IFileProvider を実装することで対応できます。
例えば、AddJsonFileに指定されたパスをJSONそのものとして扱ってしまえというようなものも作れて、以下のようなコードで実現できます。
void Main()
{
var configuration = new ConfigurationBuilder()
.AddJsonFile(new VirtualFileProvider(), @"{
""Foo"": ""barbaz"",
""Hoge"": 1234
}", false, false)
.Build();
configuration.GetChildren().Dump();
}
public class VirtualFileProvider : IFileProvider
{
public IDirectoryContents GetDirectoryContents(string subpath)
{
return new NotFoundDirectoryContents();
}
public IFileInfo GetFileInfo(string subpath)
{
return new VirtualFileInfo("virtual.json", Encoding.UTF8.GetBytes(subpath));
}
public IChangeToken Watch(string filter)
{
return NullChangeToken.Singleton;
}
}
public class VirtualFileInfo : IFileInfo
{
private byte[] _bytes;
public VirtualFileInfo(string path, byte[] bytes)
{
_bytes = bytes;
PhysicalPath = path;
}
public bool Exists => true;
public long Length => _bytes.Length;
public string PhysicalPath { get; }
public string Name => Path.GetFileName(PhysicalPath);
public DateTimeOffset LastModified => DateTime.Now;
public bool IsDirectory => false;
public Stream CreateReadStream()
{
return new MemoryStream(_bytes);
}
}
要するにファイルを表す IFileInfo を返す、IFileProvider を実装して AddJsonFile メソッドに渡すことでその IFileProvider を通して内容を読み込むことになります。
上記の例では指定されたパスをそのままbyte配列にして IFileInfo で Stream として返すというだけのものです(重要ではないプロパティは適当なものを返す)。
テストやちょっと試したいときなどにファイルを作れない/作りたくないといった場合、他のデータソースから取得するといったパターンが必要になったときには役立つかもしれません。
すでにビルドされた.NETのアプリケーションやもしかするとUnityのゲームかもしれませんが、そういった一度作ってしまったアプリケーションに後からコードを差し込みたくなることがまれにあります。
例えばビルドしたアプリケーションをテストするために、後からコードを差し込んで診断用のコードを動かしたいといったパターンです。テスト用のコードをアプリケーションには含めておきたくない場合にはプラグインするかコードを差し込むかになります。
ここではMono.Cecilを利用してコードを後から差し込む方法を説明します。
Mono.Cecilとは
Mono.Cecilは.NETアセンブリを解析したり、書き換えたりするための便利なライブラリです。このライブラリを使うことで既存のコードを書き換えて新しいアセンブリを出力できます。
簡単な差し込み方
Mono.Cecilでコードを書き換えて既存のコードに自分のコードを差し込んでみます。
例えば以下のようなプログラム ConsoleApp1.exe があるとします。
using System;
namespace ConsoleApp1
{
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Hello Konnichiwa!");
}
}
}
そこでこの Console.WriteLine
の呼び出しより前に処理を差し込みます。差し込みたい処理は以下のようなコードで別なアセンブリ(.dll)にしておきます。
using System;
namespace ClassLibrary1
{
public class Class1
{
public static void Initialize()
{
Console.WriteLine("差し込み案件です!");
}
}
}
そしてstaticなメソッドにしておくことで呼び出すためのILを最低限とでき、コードを差し込んだ側のスタックやローカル変数のつじつま合わせを考えなくてよくなるので単純になります。もちろん元のコードやオブジェクトに依存した処理を行いたいときは何らかの処理が必要となります。
ILの書き換えをする
というわけでILの書き換えをして新たに作ったアセンブリのメソッドを呼び出すように書き換えます。具体的には以下のようなコードとなります。
var baseDir = @"Path\To\ConsoleAppDirectory";
var targetAssemblyName = "ConsoleApp1.exe";
var targetTypeName = "ConsoleApp1.Program";
var targetMethodName = "Main";
var injectAssemblyName = "ClassLibrary1.dll";
var injectTypeName = "ClassLibrary1.Class1";
var injectMethodName = "Initialize";
var outputAssembly = "ConsoleApp1_Injected.exe";
var assemblyResolver = new DefaultAssemblyResolver();
assemblyResolver.AddSearchDirectory(baseDir);
var asm = AssemblyDefinition.ReadAssembly(
Path.Combine(baseDir, targetAssemblyName),
new ReaderParameters
{
AssemblyResolver = assemblyResolver,
}
);
var targetType = asm.MainModule.GetType(targetType);
var targetMethod = targetType.Methods.First(x => x.Name == targetMethodName);
var asmInject = AssemblyDefinition.ReadAssembly(Path.Combine(baseDir, injectAssemblyName));
var injectType = asmInject.MainModule.GetType(injectTypeName);
var injectMethod = injectType.Methods.First(x => x.IsStatic && x.Name == injectMethodName);
var injectMethodRef = targetMethod.Module.Import(injectMethod);
var ilProcessor = targetMethod.Body.GetILProcessor();
ilProcessor.InsertBefore(targetMethod.Body.Instructions[0], Instruction.Create(OpCodes.Call, injectMethodRef));
asm.Write(Path.Combine(baseDir, outputAssembly));
まず、AssemblyResolverを用意します。これはCecilでアセンブリを追加したりするときに正しく見つけるために必要となります。
var assemblyResolver = new DefaultAssemblyResolver();
assemblyResolver.AddSearchDirectory(baseDir);
次に差し込み先となるアセンブリを読み込んで、型とメソッド(差し込み先)を取得します。
var asm = AssemblyDefinition.ReadAssembly(
Path.Combine(baseDir, targetAssembly),
new ReaderParameters
{
AssemblyResolver = assemblyResolver,
}
);
var targetType = asm.MainModule.GetType(targetType);
var targetMethod = targetType.Methods.First(x => x.Name == targetMethod);
差し込む先が見つかったら次は差し込みたい処理を読み込んで探します。
var asmInject = AssemblyDefinition.ReadAssembly(Path.Combine(baseDir, injectAssemblyName));
var injectType = asmInject.MainModule.GetType(injectTypeName);
var injectMethod = injectType.Methods.First(x => x.IsStatic && x.Name == injectMethodName);
差し込み先に参照を追加し、その差し込み先内でメソッドの参照を得ます。これは差し込み先で差し込まれる側のアセンブリに対する参照情報を使うので必要です(MethodDefinitionは直接アセンブリをまたぐことはできない)。
var injectMethodRef = targetMethod.Module.Import(injectMethod);
そして呼び出すためのコードを差し込み先のメソッドに書き込みます。
var ilProcessor = targetMethod.Body.GetILProcessor();
ilProcessor.InsertBefore(targetMethod.Body.Instructions[0], Instruction.Create(OpCodes.Call, injectMethodRef));
これが…
IL_0000: nop
IL_0001: ldstr "Hello Konnichiwa!"
IL_0006: call void [mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: ret
こうじゃ。
IL_0000: call void [ClassLibrary1]ClassLibrary1.Class1::Initialize()
IL_0005: nop
IL_0006: ldstr "Hello Konnichiwa!"
IL_000b: call void [mscorlib]System.Console::WriteLine(string)
IL_0010: nop
IL_0011: ret
最後に書き換えたアセンブリを書き出して終了です。
asm.Write(Path.Combine(baseDir, outputAssembly));
実行結果
これで書き換えたアセンブリ (ConsoleApp1_Injected.exe) を実行すると以下のような出力となります。
差し込み案件です!
Hello Konnichiwa!
まとめ
Mono.Cecilで簡単にアセンブリを書き換えられるのでビルドしてしまったアプリの魔改造がはかどります。普段、ビルド後のアセンブリを書き換えたい機会は多くないと思いますがいざというときに処理を差し込む一つの方法としては覚えておいて損はないのではないでしょうか。
サンプルコードは https://github.com/mayuki/Sample.MonoCecil に置いておきますのでどうぞご利用ください。