I am working on a project to evaluate tokenized user-defined expressions of varying complexity, using C# as the scripting language.
I have a working model using CodeDOM and reflection to generate an evaluator class, create and load the assembly (GenerateInMemory = true), instantiate the class, and Execute the evaluate method. However, I want to load the assembly in an AppDomain so that I can unload it when execution is complete. While researching this issue, I was directed to the AppDomain.DefineDynamicAssembly method. This seems to be exactly what I need, as I can create a collectible assembly.
Here are a couple of examples of the user-defined expressions, and the classes generated by my CodeDOM project:
Simple user-defined expression:
return Abs(@HDL@/@LDL@ * 5.5);
Generated class:
namespace Lab.ResultProcessing
{
public sealed class ExpressionEvaluator
{
public double Evaluate()
{
return System.Math.Abs(449.86881550861/74.934407754305 * 5.5);
}
}
}
More complex user-defined expression:
double GFR;
double MA_GFR;
double MB_GFR;
double FA_GFR;
double FB_GFR;
GFR = (170 *
Pow(@CREAT@, -0.999) *
Pow(@YEARS@, -0.176) *
Pow(@BUN@, -0.170) *
Pow(@ALBUMIN@, 0.318));
MA_GFR = GFR;
MB_GFR = GFR * 1.180;
FA_GFR = GFR * 0.762;
FB_GFR = GFR * 1.180 * 0.762;
if (("@RACE@" != "B") && ("@GENDER@" == "M"))
{
return MA_GFR;
}
else if (("@RACE@" == "B") && ("@GENDER@" == "M"))
{
return MB_GFR;
}
else if (("@RACE@" != "B") && ("@GENDER@" == "F"))
{
return FA_GFR;
}
else if (("@RACE@" == "B") && ("@GENDER@" == "F"))
{
return FB_GFR;
}
else
{
return GFR;
}
Generated class:
namespace Lab.ResultProcessing
{
public sealed class ExpressionEvaluator
{
public double Evaluate()
{
double GFR;
double MA_GFR;
double MB_GFR;
double FA_GFR;
double FB_GFR;
GFR = (170 *
System.Math.Pow(0.797258181752292, -0.999) *
System.Math.Pow(63.6814545438073, -0.176) *
System.Math.Pow(5.47258181752292, -0.170) *
System.Math.Pow(3.79725818175229, 0.318));
MA_GFR = GFR;
MB_GFR = GFR * 1.180;
FA_GFR = GFR * 0.762;
FB_GFR = GFR * 1.180 * 0.762;
if (("B" != "B") && ("M" == "M"))
{
return MA_GFR;
}
else if (("B" == "B") && ("M" == "M"))
{
return MB_GFR;
}
else if (("B" != "B") && ("M" == "F"))
{
return FA_GFR;
}
else if (("B" == "B") && ("M" == "F"))
{
return FB_GFR;
}
else
{
return GFR;
}
;
}
}
}
I am now attempting to duplicate the functionality described above using Reflection.Emit. My problem is that I haven't found a way to inject the detokenized formula into the emitted class.
Here is the code I am using:
public static object DynamicEvaluate2(string expression)
{
AssemblyName assemblyName = new AssemblyName("Lab.ResultProcessing");
AppDomain appDomain = AppDomain.CurrentDomain;
AssemblyBuilder assemblyBuilder = appDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndCollect);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name);
TypeBuilder typeBuilder = moduleBuilder.DefineType("ExpressionEvaluator", TypeAttributes.Sealed);
MethodBuilder methodBuilder = typeBuilder.DefineMethod("Evaluate", MethodAttributes.Public | MethodAttributes.Final, typeof(double), null);
ILGenerator methodGenerator = methodBuilder.GetILGenerator();
methodGenerator.Emit(OpCodes.Ldobj, expression);
methodGenerator.Emit(OpCodes.Ret);
Type evaluatorType = typeBuilder.CreateType();
MethodInfo methodInfo = evaluatorType.GetMethod("Evaluate");
object evaluator = Activator.CreateInstance(evaluatorType);
object result = methodInfo.Invoke(evaluator, null);
return result;
}
When the methodInfo.Invoke method is called I get the following error:
Test method ResultCalculatorTest.ResultCalculatorClassFactoryTest.DynamicEvaluate2Test threw exception: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.BadImageFormatException: Bad class token.
So I have a couple of questions:
How can in inject the detokenized user-defined expression using Reflection.Emit?
Is there any way to see C# code for the emitted class, or is it only in IL?
How do I debug the emitted class?
Any help would be greatly appreciated.
methodGenerator.Emit(OpCodes.Ldobj, expression);
This doesn't do what you want it to: the ldobj
instruction expects a Type
, not a string
. According to MSDN, the purpose of the ldobj
instruction is to copy the value type object pointed to by an address.
Unlike CodeDom, Reflection.Emit won't parse your expression for you. Your code will need to parse the expression
string and emit the right sequence of IL opcodes to calculate that expression. In effect, you need to write your own compiler.
An alternative to Reflection.Emit is the types in System.Linq.Expressions
. These are higher level than Reflection.Emit and lower level than CodeDom. You'll still need to parse your string, but you'll build an abstract syntax tree in memory instead of emitting raw opcodes.
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