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

ヘルパー メソッド本体を作成するために、次の 2 つのヘルパー メソッドを使用しています。

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

dynamicC# を使用して単体テスト コードを共有できる場合があります。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; }
    };
");

両方に対して 1 つのテスト メソッドを作成できます。

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 に答える