Javascript 如何在运行时动态创建C#类(根据现有类)
背景: 我们有一个客户端(Javascript)和服务器端(C#)的项目。两边都需要运行一个计算逻辑,因此它是用Javascript和C#编写的。 我们有许多针对C版本类的单元测试。我们的目标是共享C#和Javascript实现的单元测试 当前情况: 我们能够在嵌入式JS引擎(Microsoft ClearScript)中运行Javascript代码。代码如下所示:Javascript 如何在运行时动态创建C#类(根据现有类),javascript,c#,emit,clearscript,Javascript,C#,Emit,Clearscript,背景: 我们有一个客户端(Javascript)和服务器端(C#)的项目。两边都需要运行一个计算逻辑,因此它是用Javascript和C#编写的。 我们有许多针对C版本类的单元测试。我们的目标是共享C#和Javascript实现的单元测试 当前情况: 我们能够在嵌入式JS引擎(Microsoft ClearScript)中运行Javascript代码。代码如下所示: public decimal Calulate(decimal x, decimal y) { string scri
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);
}
然而,编写这样的类需要很多努力。我们正在寻找一种在运行时动态创建此类类的方法
例如,我们有一个C#类(在JS fle中也有JS版本):
我们希望创建一个具有相同方法的动态类,但是调用脚本引擎来调用相关的JS代码
有可能做到吗?听起来很简单。现在您甚至不需要手动发出任何IL:) 最简单的方法是忽略“动态创建”部分。您可以简单地使用T4模板在编译时自动创建类。如果您只考虑单元测试,那么这是解决问题的一种非常简单的方法 现在,如果您真的想动态地(在运行时)创建类型,这会变得有点复杂 首先,创建一个包含所有必需方法的接口。C#类将直接实现该接口,而我们将生成符合该接口的helper类 接下来,我们创建helper类:
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);
}
为了完整起见,这里是我使用的IYourInterface
和YourClassBase
:
public interface IYourInterface
{
decimal Add(decimal x, decimal y);
}
public abstract class YourClassBase
{
public ScriptEngine ScriptEngine { get; set; }
}
不过,如果可以的话,我强烈建议使用文本模板在编译时生成源代码。动态代码往往很难调试(当然还有编写)。另一方面,如果您只是从模板生成这些内容,您将在代码中看到整个生成的helper类。CodeDom可能就是您所发现的 这里有一个很好的例子:您可以使用C#的
动态
来共享单元测试代码。假设您有一个C#类:
假设您还创建了一个实现相同接口的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);
这样做的好处是,您没有为每个测试调用解析和编译新的脚本代码。CodeDom可能是您找到的。你的链接非常有用。谢谢
public class Calculator {
public decimal Add(decimal x, decimal y) { return x + y; }
}
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);