如何在运行时动态创建 C# class(根据现有的 class)
How to create a C# class (according to an existing class) dynamically at runtime
背景:
我们有一个包含客户端(Javascript) 和服务器端(C#) 的项目。两边都有计算逻辑需要运行,所以用Javascript和C#都写了。
我们有许多针对 C# 版本 classes 的单元测试。我们的目标是共享 C# 和 Javascript 实现的单元测试。
现状:
我们能够 运行 嵌入 JS 引擎 (Microsoft ClearScript) 中的 Javascript 代码。代码如下所示:
public decimal Calulate(decimal x, decimal y)
{
string script = @"
var calc = new Com.Example.FormCalculater();
var result = calc.Calculate({0}, {1});";
this.ScriptEngine.Evaluate(string.Format(script, x, y));
var result = this.ScriptEngine.Evaluate("result");
return Convert.ToDecimal(result);
}
然而,写出这样的classes需要付出很大的努力。我们正在寻找一种在 运行 时间动态创建此类 classes 的方法。
例如,我们有一个 C# class(在 JS 文件中也有它的 JS 版本):
public class Calculator {
public decimal Add(decimal x, decimal y){ ... }
public decimal Substract(decimal x, decimal y){ ... }
public decimal Multiply(decimal x, decimal y){ ... }
public decimal Divide(decimal x, decimal y){ ... }
}
我们想创建一个动态的class具有相同的方法,但调用脚本引擎来调用相关的JS代码。
可以吗?
听起来很简单。现在你甚至不需要手动发出任何 IL :)
最简单的方法是忽略 "create it dynamically" 部分。您可以简单地使用 T4 模板在编译时自动创建 class。如果您只考虑单元测试,这是解决问题的一种非常简单的方法。
现在,如果您想真正动态地创建类型(在运行时),这会变得有点复杂。
首先,创建一个包含所有必需方法的接口。 C# class 将直接实现此接口,而我们将生成帮助程序 class 以符合此接口。
接下来,我们创建助手 class:
var assemblyName = new AssemblyName("MyDynamicAssembly");
var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("Module");
var typeBuilder = moduleBuilder.DefineType("MyNewType", TypeAttributes.Public | TypeAttributes.Class | TypeAttributes, typeof(YourClassBase), new[] { typeof(IYourInterface) } );
TypeBuilder
允许我们定义所有这些方法,所以让我们接下来做。
// Get all the methods in the interface
foreach (var method in typeof(IYourInterface).GetMethods())
{
var parameters = method.GetParameters().Select(i => i.ParameterType).ToArray();
// We can only compile lambda expressions into a static method, so we'll have this helper. this is going to be YourClassBase.
var helperMethod = typeBuilder.DefineMethod
(
"s:" + method.Name,
MethodAttributes.Private | MethodAttributes.Static,
method.ReturnType,
new [] { method.DeclaringType }.Union(parameters).ToArray()
);
// The actual instance method
var newMethod =
typeBuilder.DefineMethod
(
method.Name,
MethodAttributes.Public | MethodAttributes.Virtual,
method.ReturnType,
parameters
);
// Compile the static helper method
Build(method).CompileToMethod(helperMethod);
// We still need raw IL to call the helper method
var ilGenerator = newMethod.GetILGenerator();
// First argument is (YourClassBase)this, then we emit all the other arguments.
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Castclass, typeof(YourClassBase));
for (var i = 0; i < parameters.Length; i++) ilGenerator.Emit(OpCodes.Ldarg, i + 1);
ilGenerator.Emit(OpCodes.Call, helperMethod);
ilGenerator.Emit(OpCodes.Ret);
// "This method is an implementation of the given IYourInterface method."
typeBuilder.DefineMethodOverride(newMethod, method);
}
为了创建辅助方法体,我使用了这两个辅助方法:
LambdaExpression Build(MethodInfo methodInfo)
{
// This + all the method parameters.
var parameters =
new [] { Expression.Parameter(typeof(YourClassBase)) }
.Union(methodInfo.GetParameters().Select(i => Expression.Parameter(i.ParameterType)))
.ToArray();
return
Expression.Lambda
(
Expression.Call
(
((Func<MethodInfo, YourClassBase, object[], object>)InvokeInternal).Method,
Expression.Constant(methodInfo, typeof(MethodInfo)),
parameters[0],
Expression.NewArrayInit(typeof(object), parameters.Skip(1).Select(i => Expression.Convert(i, typeof(object))).ToArray())
),
parameters
);
}
public static object InvokeInternal(MethodInfo method, YourClassBase @this, object[] arguments)
{
var script = @"
var calc = new Com.Example.FormCalculater();
var result = calc.{0}({1});";
script = string.Format(script, method.Name, string.Join(", ", arguments.Select(i => Convert.ToString(i))));
@this.ScriptEngine.Evaluate(script);
return (object)Convert.ChangeType(@this.ScriptEngine.Evaluate("result"), method.ReturnType);
}
如果你愿意,你可以让它更具体(生成表达式树以更好地匹配给定的方法),但这为我们省去了很多麻烦,并允许我们使用 C# 进行大部分操作困难的东西。
我假设您所有的方法都有一个 return 值。如果不是,您将不得不为此进行调整。
最后:
var resultingType = typeBuilder.CreateType();
var instance = (IYourInterface)Activator.CreateInstance(resultingType);
var init = (YourClassBase)instance;
init.ScriptEngine = new ScriptEngine();
var result = instance.Add(12, 30);
Assert.AreEqual(42M, result);
为了完整起见,这里是我使用的 IYourInterface
和 YourClassBase
:
public interface IYourInterface
{
decimal Add(decimal x, decimal y);
}
public abstract class YourClassBase
{
public ScriptEngine ScriptEngine { get; set; }
}
不过,如果可以的话,我强烈建议使用文本模板在编译时生成源代码。动态代码往往很难调试(当然也很难编写)。另一方面,如果你只是从模板生成这些东西,你会在代码中看到整个生成的助手 class。
CodeDom 可能您发现了什么。 https://msdn.microsoft.com/en-us/library/y2k85ax6(v=vs.110).aspx
这是一个很好的例子:http://www.codeproject.com/Articles/26312/Dynamic-Code-Integration-with-CodeDom
您或许可以使用 C# dynamic
来共享单元测试代码。假设你有一个 C# class:
public class Calculator {
public decimal Add(decimal x, decimal y) { return x + y; }
}
假设您还创建了一个实现相同接口的 JavaScript 对象:
scriptEngine.Execute(@"
calculator = {
Add: function (x, y) { return x + y; }
};
");
您可以为两者创建一个测试方法:
public static void TestAdd(dynamic calculator) {
Assert.AreEqual(3, calculator.Add(1, 2));
}
下面是测试这两种实现的方式:
TestAdd(new Calculator());
TestAdd(scriptEngine.Script.calculator);
这样做的好处是您不需要为每个测试调用解析和编译新的脚本代码。
背景:
我们有一个包含客户端(Javascript) 和服务器端(C#) 的项目。两边都有计算逻辑需要运行,所以用Javascript和C#都写了。 我们有许多针对 C# 版本 classes 的单元测试。我们的目标是共享 C# 和 Javascript 实现的单元测试。
现状:
我们能够 运行 嵌入 JS 引擎 (Microsoft ClearScript) 中的 Javascript 代码。代码如下所示:
public decimal Calulate(decimal x, decimal y)
{
string script = @"
var calc = new Com.Example.FormCalculater();
var result = calc.Calculate({0}, {1});";
this.ScriptEngine.Evaluate(string.Format(script, x, y));
var result = this.ScriptEngine.Evaluate("result");
return Convert.ToDecimal(result);
}
然而,写出这样的classes需要付出很大的努力。我们正在寻找一种在 运行 时间动态创建此类 classes 的方法。
例如,我们有一个 C# class(在 JS 文件中也有它的 JS 版本):
public class Calculator {
public decimal Add(decimal x, decimal y){ ... }
public decimal Substract(decimal x, decimal y){ ... }
public decimal Multiply(decimal x, decimal y){ ... }
public decimal Divide(decimal x, decimal y){ ... }
}
我们想创建一个动态的class具有相同的方法,但调用脚本引擎来调用相关的JS代码。
可以吗?
听起来很简单。现在你甚至不需要手动发出任何 IL :)
最简单的方法是忽略 "create it dynamically" 部分。您可以简单地使用 T4 模板在编译时自动创建 class。如果您只考虑单元测试,这是解决问题的一种非常简单的方法。
现在,如果您想真正动态地创建类型(在运行时),这会变得有点复杂。
首先,创建一个包含所有必需方法的接口。 C# class 将直接实现此接口,而我们将生成帮助程序 class 以符合此接口。
接下来,我们创建助手 class:
var assemblyName = new AssemblyName("MyDynamicAssembly");
var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("Module");
var typeBuilder = moduleBuilder.DefineType("MyNewType", TypeAttributes.Public | TypeAttributes.Class | TypeAttributes, typeof(YourClassBase), new[] { typeof(IYourInterface) } );
TypeBuilder
允许我们定义所有这些方法,所以让我们接下来做。
// Get all the methods in the interface
foreach (var method in typeof(IYourInterface).GetMethods())
{
var parameters = method.GetParameters().Select(i => i.ParameterType).ToArray();
// We can only compile lambda expressions into a static method, so we'll have this helper. this is going to be YourClassBase.
var helperMethod = typeBuilder.DefineMethod
(
"s:" + method.Name,
MethodAttributes.Private | MethodAttributes.Static,
method.ReturnType,
new [] { method.DeclaringType }.Union(parameters).ToArray()
);
// The actual instance method
var newMethod =
typeBuilder.DefineMethod
(
method.Name,
MethodAttributes.Public | MethodAttributes.Virtual,
method.ReturnType,
parameters
);
// Compile the static helper method
Build(method).CompileToMethod(helperMethod);
// We still need raw IL to call the helper method
var ilGenerator = newMethod.GetILGenerator();
// First argument is (YourClassBase)this, then we emit all the other arguments.
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Castclass, typeof(YourClassBase));
for (var i = 0; i < parameters.Length; i++) ilGenerator.Emit(OpCodes.Ldarg, i + 1);
ilGenerator.Emit(OpCodes.Call, helperMethod);
ilGenerator.Emit(OpCodes.Ret);
// "This method is an implementation of the given IYourInterface method."
typeBuilder.DefineMethodOverride(newMethod, method);
}
为了创建辅助方法体,我使用了这两个辅助方法:
LambdaExpression Build(MethodInfo methodInfo)
{
// This + all the method parameters.
var parameters =
new [] { Expression.Parameter(typeof(YourClassBase)) }
.Union(methodInfo.GetParameters().Select(i => Expression.Parameter(i.ParameterType)))
.ToArray();
return
Expression.Lambda
(
Expression.Call
(
((Func<MethodInfo, YourClassBase, object[], object>)InvokeInternal).Method,
Expression.Constant(methodInfo, typeof(MethodInfo)),
parameters[0],
Expression.NewArrayInit(typeof(object), parameters.Skip(1).Select(i => Expression.Convert(i, typeof(object))).ToArray())
),
parameters
);
}
public static object InvokeInternal(MethodInfo method, YourClassBase @this, object[] arguments)
{
var script = @"
var calc = new Com.Example.FormCalculater();
var result = calc.{0}({1});";
script = string.Format(script, method.Name, string.Join(", ", arguments.Select(i => Convert.ToString(i))));
@this.ScriptEngine.Evaluate(script);
return (object)Convert.ChangeType(@this.ScriptEngine.Evaluate("result"), method.ReturnType);
}
如果你愿意,你可以让它更具体(生成表达式树以更好地匹配给定的方法),但这为我们省去了很多麻烦,并允许我们使用 C# 进行大部分操作困难的东西。
我假设您所有的方法都有一个 return 值。如果不是,您将不得不为此进行调整。
最后:
var resultingType = typeBuilder.CreateType();
var instance = (IYourInterface)Activator.CreateInstance(resultingType);
var init = (YourClassBase)instance;
init.ScriptEngine = new ScriptEngine();
var result = instance.Add(12, 30);
Assert.AreEqual(42M, result);
为了完整起见,这里是我使用的 IYourInterface
和 YourClassBase
:
public interface IYourInterface
{
decimal Add(decimal x, decimal y);
}
public abstract class YourClassBase
{
public ScriptEngine ScriptEngine { get; set; }
}
不过,如果可以的话,我强烈建议使用文本模板在编译时生成源代码。动态代码往往很难调试(当然也很难编写)。另一方面,如果你只是从模板生成这些东西,你会在代码中看到整个生成的助手 class。
CodeDom 可能您发现了什么。 https://msdn.microsoft.com/en-us/library/y2k85ax6(v=vs.110).aspx
这是一个很好的例子:http://www.codeproject.com/Articles/26312/Dynamic-Code-Integration-with-CodeDom
您或许可以使用 C# dynamic
来共享单元测试代码。假设你有一个 C# class:
public class Calculator {
public decimal Add(decimal x, decimal y) { return x + y; }
}
假设您还创建了一个实现相同接口的 JavaScript 对象:
scriptEngine.Execute(@"
calculator = {
Add: function (x, y) { return x + y; }
};
");
您可以为两者创建一个测试方法:
public static void TestAdd(dynamic calculator) {
Assert.AreEqual(3, calculator.Add(1, 2));
}
下面是测试这两种实现的方式:
TestAdd(new Calculator());
TestAdd(scriptEngine.Script.calculator);
这样做的好处是您不需要为每个测试调用解析和编译新的脚本代码。