There are many 3rd party C# formula evaluation libraries out there, however the same thing can be achieved through dynamic scripts with the Roslyn C# Compiler.
Whilst this solution does not technically make much use of Reflection as requested and it is very fixed to the specified input types, it is only a primer to show you the way. You could use reflection to resolve or infer some aspects of this or you could generate the script really dynamically from the formula itself.
Instead of being based 100% on the formula, creating a fixed class wrapper to execute the formula and represents all the valid or possible inputs is a simpler approach. You gain direct control over the constraints but the formula can now take on any form of comparison logic or operators, as long as it conforms to the class structure and valid c# syntax.
It is also helpful to make the formulas more abstract, Person1 is really specific to a single context, you would not normally construct a separate formula for each individual person instead your formula should probably be in the form of:
"Person.Age > RentalCar.MinimumAge"
Then in the execution context you could use the formula as a single business rule to evaluate across any combination of selected Person and RentalCar.
IMO avoid using model or type names as the variable names in your formulas, give them context by using the business domain name, the name that describes them in the business model, not the data model. It can avoid conflict between what is a Type reference, and what is an Instance
"Driver.Age > Car.MinimumAge"
For a thorough background on this process, please have a read over this response to How to programmatically create a class library DLL using reflection?
Start by writing out in long form a class wrapper that has a single method with the user's formula injected within it. These are the requirements from the original post:
- Return a boolean value
- Field/Property/Parameter called Person1 that is a type of
Person
- Field/Property/Parameter called RentalCar5 that is a type of
RentalCar
the following script uses parameters to model the inputs, which will work, but I prefer to use instance Properties as I find it simplifies the interface, script processing and debugging process, especially when have a common context but multiple formulas. This example is just to get you going
namespace DynamicScriptEngine
{
public class PersonScript
{
public bool Evaluate(Person Driver, RentalCar Car)
{
return
#region Dynamic Script
Driver.Age > Car.MinimumAge
#endregion Dynamic Script
;
}
}
}
Now, we just compile that class, call the method and get the result. The following code will generate the above class with an injected formula and return the response.
Orchestration
Call this method to pass in the parameters and the formula from the context where you need the result.
public static bool EvaluateFormula(Person Driver, RentalCar Car, string formula)
{
string nameSpace = "DynamicScriptEngine";
string className = "PersonScript";
string methodName = "Evaluate";
List<Type> knownTypes = new List<Type> { typeof(Person), typeof(RentalCar) };
// 1. Generate the code
string script = BuildScript(nameSpace, knownTypes, className, methodName, formula);
// 2. Compile the script
// 3. Load the Assembly
Assembly dynamicAssembly = CompileScript(script);
// 4. Execute the code
object[] arguments = new object[] { person, rentalCar };
bool result = ExecuteScript(dynamicAssembly, nameSpace, className, methodName, arguments);
return result;
}
Usage
This is an example of how to call the above method to evaluate the formula:
static void Main(string[] args)
{
// create the input conditions
Person person1 = new Person { Name = "Person1", Age = 21 };
RentalCar car5 = new RentalCar { Name = "RentalCar1", MinimumAge = 20 };
RentalCar car1 = new RentalCar { Name = "RentalCar5", MinimumAge = 25 };
// Evaluate the formulas
Console.WriteLine("Compare Driver: {0}", person1);
Console.WriteLine();
string formula = "Driver.Age > 20";
Console.WriteLine("Formula: {0} => {1}", formula, EvaluateFormula(person1, null, formula));
Console.WriteLine();
Console.WriteLine("Compare against Car: {0}", car5);
formula = "Driver.Age > Car.MinimumAge";
Console.WriteLine("Formula: {0} => {1}", formula, EvaluateFormula(person1, car5, formula));
Console.WriteLine();
Console.WriteLine("Compare against Car: {0}", car1);
formula = "Driver.Age > Car.MinimumAge";
Console.WriteLine("Formula: {0} => {1}", formula, EvaluateFormula(person1, car1, formula));
}
Output

Code Generator
This method will generate the code wrapper around the formula so that it can be compiled into a standalone assembly for execution.
It is important that we include the namespaces and or aliases that might be required for the dynamic script to execute, you should consider including the namespaces for the input types as a bare minimum.
private static string BuildScript(string nameSpace, List<Type> knownTypes, string className, string methodName, string formula)
{
StringBuilder code = new StringBuilder();
code.AppendLine("using System;");
code.AppendLine("using System.Linq;");
// extract the usings from the list of known types
foreach(var ns in knownTypes.Select(x => x.Namespace).Distinct())
{
code.AppendLine($"using {ns};");
}
code.AppendLine();
code.AppendLine($"namespace {nameSpace}");
code.AppendLine("{");
code.AppendLine($" public class {className}");
code.AppendLine(" {");
// NOTE: here you could define the inputs are properties on this class, if you wanted
// You might also evaluate the parameter names somehow from the formula itself
// But that adds another layer of complexity, KISS
code.Append($" public bool {methodName}(");
code.Append("Person Driver, ");
code.Append("RentalCar Car");
code.AppendLine(")");
code.AppendLine(" {");
code.Append(" return ");
// NOTE: Here we insert the actual formula
code.Append(formula);
code.AppendLine(";");
code.AppendLine(" }");
code.AppendLine(" }");
code.AppendLine("}");
return code.ToString();
}
Compilation
private static Assembly CompileScript(string script)
{
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(script);
// use "mytest.dll" if you want, random works well enough
string assemblyName = System.IO.Path.GetRandomFileName();
List<string> dlls = new List<string> {
typeof(object).Assembly.Location,
typeof(Enumerable).Assembly.Location,
// NOTE: Include the Assembly that the Input Types exist in!
// And any other references, I just enumerate the working folder and load them all, but it's up to you.
typeof(Person).Assembly.Location
};
MetadataReference[] references = dlls.Distinct().Select(x => MetadataReference.CreateFromFile(x)).ToArray();
CSharpCompilation compilation = CSharpCompilation.Create(
assemblyName,
syntaxTrees: new[] { syntaxTree },
references: references,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
// Now we actually compile the script, this includes some very crude error handling, just to show you can
using (var ms = new MemoryStream())
{
EmitResult result = compilation.Emit(ms);
if (!result.Success)
{
IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic =>
diagnostic.IsWarningAsError ||
diagnostic.Severity == DiagnosticSeverity.Error);
List<string> errors = new List<string>();
foreach (Diagnostic diagnostic in failures)
{
//errors.AddDistinct(String.Format("{0} : {1}", diagnostic.Id, diagnostic.Location, diagnostic.GetMessage()));
errors.Add(diagnostic.ToString());
}
throw new ApplicationException("Compilation Errors: " + String.Join(Environment.NewLine, errors));
}
else
{
ms.Seek(0, SeekOrigin.Begin);
return Assembly.Load(ms.ToArray());
}
}
}
Execution
This is the method that actually executes the generated script, this method is very generic, the Orchestration Logic will prepare the parameters for us. If you were using Instance Properties then the code in here is a bit more complicated.
private static bool ExecuteScript(Assembly assembly, string nameSpace, string className, string methodName, object[] arguments)
{
var appType = assembly.GetType($"{nameSpace}.{className}");
object app = Activator.CreateInstance(appType);
MethodInfo method = appType.GetMethod(methodName);
object result = method.Invoke(app, arguments);
return (bool)result;
}
This is just a basic example and there is a lot of power in a solution like this, but you need to be aware that it can introduce a lot of vulnerability into your code. There are steps you can take to mitigate security concerns, ideally you should validate and or sanitise the formula when it is stored, and execute it in a different context or container
All In one example
// Install-Package 'Microsoft.CodeAnalysis.CSharp.Scripting'
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
namespace ConsoleApp2
{
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public override string ToString()
{
return $"Person: {Name} Age: {Age}";
}
}
public class RentalCar
{
public string Name { get; set; }
public int MinimumAge { get; set; }
public override string ToString()
{
return $"RentalCar: {Name} MinimumAge: {MinimumAge}";
}
}
class Program
{
static void Main(string[] args)
{
// create the input conditions
Person person1 = new Person { Name = "Person1", Age = 21 };
RentalCar car5 = new RentalCar { Name = "RentalCar1", MinimumAge = 20 };
RentalCar car1 = new RentalCar { Name = "RentalCar5", MinimumAge = 25 };
// Evaluate the formulas
Console.WriteLine("Compare Driver: {0}", person1);
Console.WriteLine();
string formula = "Driver.Age > 20";
Console.WriteLine("Formula: {0} => {1}", formula, EvaluateFormula(person1, null, formula));
Console.WriteLine();
Console.WriteLine("Compare against Car: {0}", car5);
formula = "Driver.Age > Car.MinimumAge";
Console.WriteLine("Formula: {0} => {1}", formula, EvaluateFormula(person1, car5, formula));
Console.WriteLine();
Console.WriteLine("Compare against Car: {0}", car1);
formula = "Driver.Age > Car.MinimumAge";
Console.WriteLine("Formula: {0} => {1}", formula, EvaluateFormula(person1, car1, formula));
}
public static bool EvaluateFormula(Person driver, RentalCar car, string formula)
{
string nameSpace = "DynamicScriptEngine";
string className = "PersonScript";
string methodName = "Evaluate";
List<Type> knownTypes = new List<Type> { typeof(Person), typeof(RentalCar) };
// 1. Generate the code
string script = BuildScript(nameSpace, knownTypes, className, methodName, formula);
// 2. Compile the script
// 3. Load the Assembly
Assembly dynamicAssembly = CompileScript(script);
// 4. Execute the code
object[] arguments = new object[] { driver, car };
bool result = ExecuteScript(dynamicAssembly, nameSpace, className, methodName, arguments);
return result;
}
private static string BuildScript(string nameSpace, List<Type> knownTypes, string className, string methodName, string formula)
{
StringBuilder code = new StringBuilder();
code.AppendLine("using System;");
code.AppendLine("using System.Linq;");
// extract the usings from the list of known types
foreach(var ns in knownTypes.Select(x => x.Namespace).Distinct())
{
code.AppendLine($"using {ns};");
}
code.AppendLine();
code.AppendLine($"namespace {nameSpace}");
code.AppendLine("{");
code.AppendLine($" public class {className}");
code.AppendLine(" {");
// NOTE: here you could define the inputs are properties on this class, if you wanted
// You might also evaluate the parameter names somehow from the formula itself
// But that adds another layer of complexity, KISS
code.Append($" public bool {methodName}(");
code.Append("Person Driver, ");
code.Append("RentalCar Car");
code.AppendLine(")");
code.AppendLine(" {");
code.Append(" return ");
// NOTE: Here we insert the actual formula
code.Append(formula);
code.AppendLine(";");
code.AppendLine(" }");
code.AppendLine(" }");
code.AppendLine("}");
return code.ToString();
}
private static Assembly CompileScript(string script)
{
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(script);
// use "mytest.dll" if you want, random works well enough
string assemblyName = System.IO.Path.GetRandomFileName();
List<string> dlls = new List<string> {
typeof(object).Assembly.Location,
typeof(Enumerable).Assembly.Location,
// NOTE: Include the Assembly that the Input Types exist in!
// And any other references, I just enumerate the working folder and load them all, but it's up to you.
typeof(Person).Assembly.Location
};
MetadataReference[] references = dlls.Distinct().Select(x => MetadataReference.CreateFromFile(x)).ToArray();
CSharpCompilation compilation = CSharpCompilation.Create(
assemblyName,
syntaxTrees: new[] { syntaxTree },
references: references,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
// Now we actually compile the script, this includes some very crude error handling, just to show you can
using (var ms = new MemoryStream())
{
EmitResult result = compilation.Emit(ms);
if (!result.Success)
{
IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic =>
diagnostic.IsWarningAsError ||
diagnostic.Severity == DiagnosticSeverity.Error);
List<string> errors = new List<string>();
foreach (Diagnostic diagnostic in failures)
{
//errors.AddDistinct(String.Format("{0} : {1}", diagnostic.Id, diagnostic.Location, diagnostic.GetMessage()));
errors.Add(diagnostic.ToString());
}
throw new ApplicationException("Compilation Errors: " + String.Join(Environment.NewLine, errors));
}
else
{
ms.Seek(0, SeekOrigin.Begin);
return Assembly.Load(ms.ToArray());
}
}
}
private static bool ExecuteScript(Assembly assembly, string nameSpace, string className, string methodName, object[] arguments)
{
var appType = assembly.GetType($"{nameSpace}.{className}");
object app = Activator.CreateInstance(appType);
MethodInfo method = appType.GetMethod(methodName);
object result = method.Invoke(app, arguments);
return (bool)result;
}
}
}
Personfrom the name, and even if you know the propertyAge, you still need thePerson1instance to get theAgespecific toPerson1. If you can get that, you might add some methods to the class that know how to parse a string and return the integer value specific to itself. Short of that, you might need to maintain a dictionary when the objects are created, in which case you wouldn't even need reflection...