eval in C# – Roslyn (Microsoft CodeAnalysis CSharp Scripting NuGet Package)

on

Why did I need to evaluate code at runtime in C#?

Recently I have been working on a project which required me to develop a decision maker. A decision maker is a system where you define a set of rules where each rule can evaluate the input parameters, and based on that provide an output. So, basically, a big if-then-else system.

The idea is that the client can enter their own input parameters. And, of course, then things got complicated, as we figured out that the conditions that need to be evaluated will be complex, as well as rules that will often contain input parameters inside some mathematical formulas. We needed to find a way to allow users to enter mathematical and logical expressions that can be evaluated on demand.

There is an amazing project called Roslyn from Microsoft. The goal of the project is to provide open-source C# and Visual Basic compilers with rich code analysis APIs. It enables building code analysis tools with the same APIs that are used by Visual Studio. This is indeed awesome. Not only that, but it’s completely open source.

For ASP.NET Core it is a part of the Microsoft.CodeAnalysis.CSharp.Scripting NuGet package.

The project requirements were:

  1. Input variables – these could only be constant values provided by the user
  2. Calculated variables – these could be values that could either be constant or calculated from input variables from formulas provided by the user
  3. Rule conditions – these needed to be evaluated to a boolean expression. Any rule could use both input and calculated variables.

If any rule condition was evaluated as true, some action needed to be taken, which is irrelevant for this blog post.

Code example of the solution

Let’s build a small console application that shows the concept of a potential decision maker in ASP.NET Core 2.1. The source code is available on GitHub.

Create a new project in Visual Studio. We will build a Console Application (.NET Core):

New Project Roslyn Decision Maker Demo

Add the Microsoft.CodeAnalysis.CSharp.Scripting using the Package Manager Console:

PM> Install-Package Microsoft.CodeAnalysis.CSharp.Scripting -Version 2.9.0

Define some initial data. We will define a dictionary for input variables, a dictionary for calculated variables and a string array for rules:

Dictionary<string, string> inputVariables = new Dictionary<string, string>();
inputVariables.Add("x", "30");
inputVariables.Add("y", "40");

Dictionary<string, string> calculatedVariables = new Dictionary<string, string>();
calculatedVariables.Add("area", "x * y");

string[] rules = {
                "area > 1000 && x < 2 * y",
                "area < 1000",
                "area > 1000 && y > 500"
            };

The input variables x and y have their respected values, and we’d like the calculated variable area to have the value of x * y. Three different rules that we will be evaluated are defined in the rules array.

Two different ways of approaching the problem will be demonstrated.

Solution using a state

Roslyn allows running C Sharp scripts on the server. Our scripts will depend on the actual input parameters provided by the user which means we cannot know how long the script will be or what it will look like. We will use the possibility of states, and run each command one by one keeping track of the state. Each command will just continue from the existing state.

First it is necessary to create an empty state and add all input variables:

// Create an empty state
var state = await CSharpScript.RunAsync("");

// Declare input variables
foreach (var item in inputVariables)
{
  state = await state.ContinueWithAsync(String.Format("var {0} = {1};", item.Key, item.Value));
}

Note that variables are added by actually running the command e.g.: var x = 40;

These variables with their respected (and calculated values) remain on the state so they can be reused later.

In a similar way, we can add all the calculated variables:

// Declare and calculate calculated variables
foreach (var item in calculatedVariables)
{
  state = await state.ContinueWithAsync(String.Format("var {0} = {1};", item.Key, item.Value));
}

We need to evaluate each of the rules. To do this, we need to continue execution from the state and just use the expression we need as a command. As such command is added to the state, Roslyn evaluates it and stores it in state.ReturnValue. The code for evaluating all rules looks like this:

// Evaluate each condition
foreach (var rule in rules)
{
  state = await state.ContinueWithAsync(rule);
  Console.WriteLine(String.Format("Rule '{0}' was evaluated as {1}", rule, (bool)state.ReturnValue;));
}

Solution using evaluation of formulas

In this solution the goal will be to manually replace all variable values with their respected values and then evaluate the actual expression. This is a more crude way to do the same thing which is not necessarily using all the features Roslyn provides, but does demo the Evaluate function.

First it is necessary to calculate the values of calculatedVariables:

List<string> keys = new List<string>(calculatedVariables.Keys);
foreach (var key in keys)
{
  foreach (var item in inputVariables)
  {
    calculatedVariables[key] = calculatedVariables[key].Replace(item.Key, item.Value);
  };
  calculatedVariables[key] = (string)(await CSharpScript.EvaluateAsync(calculatedVariables[key]));
}

Replacing of the variables is done here in a very crude way and it will cause issues if you have variables that have names that are subsets of each other (e.g. variable ‘a’ and variable ‘area’). Building a “smart” way to replace variables is not relevant for this post.

It is necessary to go through each rule, replace both input and output variables with their respected values and evaluate the rule as a boolean value:

for (var i = 0; i < rules.Length; i++)
{
  foreach (var item in inputVariables)
  {
    rules[i] = rules[i].Replace(item.Key, item.Value);
  }
  foreach (var item in calculatedVariables)
  {
    rules[i] = rules[i].Replace(item.Key, item.Value);
  }
  bool isRuleTrue = await CSharpScript.EvaluateAsync<bool>(rules[i]);
  Console.WriteLine(String.Format("Rule '{0}' was evaluated as {1}", rules[i], isRuleTrue));
}

Note the difference – when using state, we are not replacing or tampering with original string values. All the changes and calculations are done inside Roslyn. In approach with replacements and formula evaluation we do tamper with input data, so if this is important, please pay attention to it.

This is a basic proof-of-concept on how Roslyn (or, if you like, Microsoft CSharp Scripting) can be used to build a very simple decision maker.

I strongly suggest you to test it out with different input values, different variables, add your own, try with multiple conditions… See where it can take you.

What if Roslyn cannot evaluate?

This is a good question. Very often you do not have control over the input and cannot know for sure whether it can indeed be evaluated (or even parsed).

This is why it is strongly suggested to keep any evaluation code inside try-catch blocks:

try
{
  isConditionTrue = CSharpScript.EvaluateAsync<bool>(ruleCondition).Result;
}
catch (CompilationErrorException e)
{
  Console.WriteLine(string.Join(Environment.NewLine, e.Diagnostics));
}

Additional options

If, for example, more complex formulas in my calculated variables are needed, like using square roots or mathematical powering, System.Math would need to be available to users. Using the WithImports option allows you to do this:

var result = await CSharpScript.EvaluateAsync("Sqrt(2)", ScriptOptions.Default.WithImports("System.Math"));

Check https://github.com/dotnet/roslyn/wiki/Scripting-API-Samples for more examples on what you can do.

Conclusion

What I needed was just basic evaluation of basic math and logical expressions in C# at runtime. And for this purpose Roslyn (a.k.a. Microsoft CodeAnalysis CSharp Scripting) did the job superbly. But looking at the documentation, provided samples and possibilities, I cannot but wonder in how many places this could be used and how far it could take me. I will probably explore it in some other future projects I have planned, and will probably write about the findings here. But, so far, Roslyn looks like a very powerful tool indeed.

 

9 thoughts on “eval in C# – Roslyn (Microsoft CodeAnalysis CSharp Scripting NuGet Package)

  1. Thanks for the write-up! I also needed evaluation of mathematical expressions, and Roslyn seems to do this perfectly.

    What’s your experience with regards to performance of this approach?
    I’ll be taking the `CreateDelegate` route; I’m hoping this will give a better performance, but I haven’t been able to do performance testing yet.

    1. Hey, Martijn,

      Thanks for the comment.
      On this particular project I have not done performance testing. While using Roslyn, which is a full compiler, for something as small as this is probably like taking on a mosquito with a bazooka, the project did end up using much more features which made using Roslyn make sense. However, the idea of the blog post was just to scratch the surface and give an idea on where and how to try it.

  2. Super-helpful, I used this for allowing users to configure the results of various count queries in order to see if a certain threshold had been met (count == x) or exceeded (count >= x)

  3. Great article, congratulations!

    I am at the moment of research to create a decision-making system, based on rules written by the user.

    Initially, I also followed the strategy of going through the expressions by replacing parts of the strings that represent the rules, with values ​​provided by the user at run time, but later, looking at the examples on this page: https://github.com/dotnet/roslyn/wiki/Scripting-API-Samples#parameter

    I managed to evolve my model to something like this:

    public class Test
    {
    public int ValueA {get; set; }
    public int ValueB {get; set; }
    public int ValueC {get; set; }
    }

    Test objTest = new Test {ValueA = 1, ValueB = 2, ValueC = 3};
    string expression = “(((ValueC> ValueB) && (ValueB == ValueA))”;

    var result = CSharpScript.EvaluateAsync (expression, globals: objTest) .Result;

    Regarding the point of treatment of failures, I am using the strategy of testing the rules (using try-catch) with fictitious data, when the user includes them in the rules database. Of course, I also have a try-catch when it comes to processing them.

    1. Hey, Silvair,

      Thanks for the comment. I agree, if/when you do a deeper dive into this, it opens different possibilities. As I mentioned in the article, the replacing of variables is done in a very crude way and is used only to illustrate a basic example. What you are doing with the class Test is one way of doing it. A potential problem there is that you might need to predefine the class holding the variable names (in your case ValueA, ValueB and ValueC) as public properties, meaning that the user cannot necessarily easily add variables of their own. This again depends on the requirements of your specific case.

      But thanks for pointing out some new ideas and new directions to think in. It is definitely valuable for me and I am pretty sure it will be valuable for others reading this as well.

  4. Very cool, I originally wrote something like this, though using Roslyn to fully compile code presented in my UI to populated PDF fields with data from our service. Now, I’m rewriting this to give the user a real UI and use CSharpScript instead as you have, just to evaluate expressions. I provide a full UI with buttons to add IF , ELSE, LOOP, SETFIELD, etc. and navigate those statements as data. Lines are added to the script as I go.

    What’s cool about Roslyn is that you can get the location of both compile and runtime errors, and present those to the user.

    1. Yeah – it opens up endless possibilities, this blog post just scratches the bare surface. If you want, leave a link here to your post(s) and your work, it will sure be helpful for myself at least, possibly for others as well.

  5. Super helpful … nothing like that on the internet really … appreciated.

    You simply solved the problem of “How can the use enter his own variable names and formulas”

Leave a Reply

Your email address will not be published. Required fields are marked *

You are currently offline