Background:
We have a project with client side(Javascript) and server side(C#). There is a calculation logic need to run in both sides, so it is written in both Javascript and C#. We have many unit tests for the C# version classes. Our goal is to share the unit tests for both C# and Javascript implementation.
Current situation:
We are able to run the Javascript code in an embeded JS engine (Microsoft ClearScript). The code looks like this:
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);
}
However, writing such classes takes a lot of effort. We are looking for a way to create such classes dynamically at runtime.
For exmample, we have a C# class (also has it JS version in a JS fle):
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){ ... }
}
We want to create a dynamic class having the same methods but calling the Script engine to call the related JS code.
Is it possible to do it?
Sounds pretty easy. You don't even need to manually emit any IL nowadays :)
The easiest way would be to ignore the "create it dynamically" part. You can simply use a T4 template to create the class automatically at compile-time. If your only consideration is unit tests, this is a pretty easy way to solve your problem.
Now, if you want to really create the type dynamically (at runtime), this gets a bit more complicated.
First, create an interface that contains all the required methods. The C# class will just implement this interface directly, while we'll generate the helper class to conform to this interface.
Next, we create the helper 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) } );
The TypeBuilder
allows us to define all those methods, so let's do that next.
// 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);
}
To create the helper method body, I'm using these two helper methods:
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);
}
If you want, you can make this a lot more specific (generate the expression tree to be a better match for the given method), but this saves us a lot of trouble and allows us to use C# for most of the hard stuff.
I'm assuming all your methods have a return value. If not, you'll have to adjust for that.
And finally:
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);
Just for completeness, here's the IYourInterface
and YourClassBase
I've used:
public interface IYourInterface
{
decimal Add(decimal x, decimal y);
}
public abstract class YourClassBase
{
public ScriptEngine ScriptEngine { get; set; }
}
I do strongly suggest using text templates to generate the source code in compile-time, if you can, though. Dynamic code tends to be tricky to debug (and write, of course). On the other hand, if you just generate this stuff from a template, you'll see the whole generated helper class in code.
CodeDom may what are you finding. https://msdn.microsoft.com/en-us/library/y2k85ax6(v=vs.110).aspx
Here is a good example: http://www.codeproject.com/Articles/26312/Dynamic-Code-Integration-with-CodeDom
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With