先日、Expression と DynamicMethod でそれぞれ動的にデリゲートを生成するサンプルを書いてみた。
tnakamura.hatenablog.com
メタプログラミングの目的がリフレクションの高速化なら、
動的なデリゲート生成で事足りる場合が多い。
今回は一歩進んで、動的にクラスを生成してみたいと思う。
お題は、「WCF のサービスコントラクトから動的にクライアントのクラスを生成する」。
using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.ServiceModel;
using System.ServiceModel.Channels;
namespace BlackMagicSample
{
[ServiceContract]
public interface IGreetingService
{
[OperationContract]
string GoodMorning(string name);
[OperationContract]
string Hello(string name);
[OperationContract]
string GoodBye();
[OperationContract]
void GoodAfternoon(string name);
}
class GreetingService : IGreetingService
{
public void GoodAfternoon(string name)
{
Console.WriteLine($"Good afternoon, {name}");
}
public string GoodBye()
{
return "Good bye.";
}
public string GoodMorning(string name)
{
return $"Good morning, {name}.";
}
public string Hello(string name)
{
return $"Hello, {name}.";
}
}
class Program
{
static void Main(string[] args)
{
Console.Title = "BlackMagicSample";
var address = "net.pipe://localhost/GreetingService";
var binding = new NetNamedPipeBinding();
var host = new ServiceHost(typeof(GreetingService));
host.AddServiceEndpoint(
typeof(IGreetingService),
binding,
address);
host.Open();
var clientType = GenerateClientType<IGreetingService>();
var client = (IGreetingService)Activator.CreateInstance(
clientType,
binding,
new EndpointAddress(address));
Console.WriteLine(client.GoodMorning("Honda"));
Console.WriteLine(client.Hello("Kagawa"));
Console.WriteLine(client.GoodBye());
client.GoodAfternoon("Nagatomo");
Console.ReadLine();
((ClientBase<IGreetingService>)client).Close();
host.Close();
}
static Type GenerateClientType<T>()
where T : class
{
var interfaceType = typeof(T);
var moduleName = $"Client_{interfaceType.Name}";
var clientBaseType = typeof(ClientBase<T>);
var clientBaseConstructor = clientBaseType.GetConstructor(
bindingAttr: BindingFlags.NonPublic | BindingFlags.Instance,
binder: null,
types: new[] { typeof(Binding), typeof(EndpointAddress) },
modifiers: null);
var channelProperty = clientBaseType.GetProperty(
"Channel",
BindingFlags.NonPublic | BindingFlags.Instance);
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(
name: new AssemblyName(moduleName),
access: AssemblyBuilderAccess.RunAndSave);
var moduleBuilder = assemblyBuilder.DefineDynamicModule(
moduleName,
moduleName + ".cs");
var typeBuilder = moduleBuilder.DefineType(
name: moduleName,
attr: TypeAttributes.Public,
parent: typeof(ClientBase<T>),
interfaces: new[] { typeof(T) });
var constructorBuilder = typeBuilder.DefineConstructor(
attributes: MethodAttributes.Public
| MethodAttributes.HideBySig
| MethodAttributes.SpecialName
| MethodAttributes.RTSpecialName,
callingConvention: CallingConventions.Standard,
parameterTypes: new[] { typeof(Binding), typeof(EndpointAddress) });
var ctorIL = constructorBuilder.GetILGenerator();
ctorIL.Emit(OpCodes.Ldarg_0);
ctorIL.Emit(OpCodes.Ldarg_1);
ctorIL.Emit(OpCodes.Ldarg_2);
ctorIL.Emit(OpCodes.Call, clientBaseConstructor);
ctorIL.Emit(OpCodes.Ret);
var methods = interfaceType.GetMethods(BindingFlags.Public | BindingFlags.Instance);
foreach (var method in methods)
{
var parameterTypes = method.GetParameters()
.Select(p => p.ParameterType)
.ToArray();
var methodBuilder = typeBuilder.DefineMethod(
name: method.Name,
attributes: MethodAttributes.Public
| MethodAttributes.HideBySig
| MethodAttributes.NewSlot
| MethodAttributes.Virtual
| MethodAttributes.Final,
returnType: method.ReturnType,
parameterTypes: parameterTypes);
var methodIL = methodBuilder.GetILGenerator();
methodIL.Emit(OpCodes.Ldarg_0);
methodIL.Emit(OpCodes.Call, channelProperty.GetMethod);
for (var i = 0; i < parameterTypes.Length; i++)
{
methodIL.Emit(OpCodes.Ldarg, i + 1);
}
methodIL.Emit(OpCodes.Callvirt, method);
methodIL.Emit(OpCodes.Ret);
}
return typeBuilder.CreateType();
}
}
}
動的にクラスを生成する場合は、AssemblyBuilder を使う。
AssemblyBuilder を使って、
動的にアセンブリを作り、
動的にモジュールを作り、
動的に型を作り、
動的にメソッドを作る。
メソッドの中身は IL 手書き。
超大変なので、今回も ILSpy を使ってカンニングした。
実行結果は次の通り。
動的に生成したクライアントのクラスを使って、WCF サービスの呼び出しに成功した。
この手のクライアントは .NET Remoting の RealProxy を使っていたけど、
.NET Remoting は .NET Core で使えないし未来もないので、
動的にクラスを生成する手法に乗り換えたいところだ。