5

背景:

我们有一个带有客户端(Javascript)和服务器端(C#)的项目。两边都有计算逻辑需要跑,所以用Javascript和C#写的。我们有许多针对 C# 版本类的单元测试。我们的目标是共享 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);
}

但是,编写这样的类需要付出很多努力。我们正在寻找一种在运行时动态创建此类类的方法。

例如,我们有一个 C# 类(在 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){ ... }
}

我们想创建一个具有相同方法但调用脚本引擎来调用相关JS代码的动态类。

有可能做到吗?

4

3 回答 3

5

听起来很容易。现在你甚至不需要手动发出任何 IL :)

最简单的方法是忽略“动态创建”部分。您可以简单地使用 T4 模板在编译时自动创建类。如果您唯一考虑的是单元测试,那么这是解决问题的一种非常简单的方法。

现在,如果你想真正动态地创建类型(在运行时),这会变得有点复杂。

首先,创建一个包含所有必需方法的接口。C# 类将直接实现此接口,而我们将生成帮助程序类以符合此接口。

接下来,我们创建助手类:

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# 来处理大多数困难的事情。

我假设你所有的方法都有一个返回值。如果没有,您将不得不对此进行调整。

最后:

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);

为了完整起见,这是IYourInterfaceYourClassBase使用过的:

public interface IYourInterface
{
  decimal Add(decimal x, decimal y);
}

public abstract class YourClassBase
{
  public ScriptEngine ScriptEngine { get; set; }
}

如果可以的话,我强烈建议使用文本模板在编译时生成源代码。动态代码往往很难调试(当然也可以编写)。另一方面,如果您只是从模板生成这些内容,您将在代码中看到整个生成的帮助程序类。

于 2015-03-30T08:56:27.110 回答
1

CodeDom 可能你发现了什么。https://msdn.microsoft.com/en-us/library/y2k85ax6(v=vs.110).aspx

这是一个很好的例子: http: //www.codeproject.com/Articles/26312/Dynamic-Code-Integration-with-CodeDom

于 2015-03-30T09:04:19.890 回答
0

您也许可以使用 C#dynamic来共享单元测试代码。假设您有一个 C# 类:

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);

这样做的好处是您不必为每个测试调用解析和编译新的脚本代码。

于 2015-03-30T12:53:47.863 回答