0

I am trying to implement a solution where I have a predefined calculation against an item, this calculation may be different for each individual item this is why I was planning to do it this way.

Is it possible to pass in this calculation with string interpolation whilst still retaining the reference to the variables?

I've simplified this for ease of reading here but the principle is the same, these are the parameters being passed in:

double individualTotal = 100;
double parentTotal = 2000;
double siblingTotal = 1500;
double anotherTotal = 100;

The calculation would be stored as text in the database for example:

(siblingTotal/parentTotal)*individualTotal

Or another calculation could be:

((siblingTotal/(parentTotal)*individualTotal)+(anotherTotal*0.5)

I have tried the following with no luck, it just outputs the text:

var calculationText = "{(siblingCalculationTotalValue/parentCalculationTotalValue)*individualTotalValue}";
var calculation = $"" + calculationText + ""

And:

var calculationText = "{(siblingCalculationTotalValue/parentCalculationTotalValue)*individualTotalValue}";
var calculation = $"{calculationText}"

Both output:

"CalculatedValue": "{(siblingCalculationTotalValue/parentCalculationTotalValue)*individualTotalValue}"

I have also tried:

var calculationText = "(siblingCalculationTotalValue/parentCalculationTotalValue)*individualTotalValue";
var calculation = $"{calculationText}"

Output:

"CalculatedValue": "(siblingCalculationTotalValue/parentCalculationTotalValue)*individualTotalValue"

This works as expected when I pass the parameters directly into the string interpolation however it doesn't allow for the variability in calculations:

var calculation = $"{(siblingCalculationTotalValue/parentCalculationTotalValue)*individualTotalValue}"

Output:

"CalculatedValue": "75"

Any help would be really appreciated!

AMouat
  • 685
  • 15
  • 27
  • No, that is not possible. Interpolated strings can't be built at runtime like you are trying to do; the interpolated expressions are resolved at compile time. – InBetween Oct 16 '20 at 11:03
  • What do you expect to gain from mixing such complicated expressions with interpolation? Are you trying to create an expression parser? That's not what interpolated strings are. The expressions are evaluated *before* interpolation. They aren't parsed – Panagiotis Kanavos Oct 16 '20 at 11:03
  • @InBetween Ok thanks, do you know of another way to do this without using string interpolation? – AMouat Oct 16 '20 at 11:06
  • @PanagiotisKanavos This is just one way I thought might work, do you know of a better way? – AMouat Oct 16 '20 at 11:06
  • There are already seval similar questions on SO, e.g. https://stackoverflow.com/questions/4629/how-can-i-evaluate-c-sharp-code-dynamically or https://stackoverflow.com/questions/53844/how-can-i-evaluate-a-c-sharp-expression-dynamically – Klaus Gütter Oct 16 '20 at 11:25
  • Are the equations arbitrary depending on the item? Or are there a fixed amount of equations that will be used uniformly over all items? – Prime Oct 16 '20 at 11:38
  • @AlphaDelta They are arbitrary depending on the item. – AMouat Oct 16 '20 at 11:40

2 Answers2

2

The reason string interpolation can't be stored as a variable is that string interpolation is resolved at compile time into CIL (Almost certainly into String.Concat). For all intents and purposes any logic you have is the exact same as all the rest of your C# code and must be compiled.

So the question becomes, how do you compile arbitrary C# (in this instance, a mathematic equation/calculation) on run-time.

Warning

The following will compile and execute arbitrary C# code, this is a massive security vulnerability as malicious code can be entered and must be handled with the utmost care. Only attempt this on strings which either have zero end-user influence, or if your application is running on the end-user's machine, explain to them that anything they enter can be malicious.

Evaluating a lambda function of arbitrary C# code

First create an instance of ScriptOptions like so:

ScriptOptions scriptoptions = ScriptOptions.Default.WithImports("System");

If you require any functions that exist in your application in the equation/C# code some modifications are necessary:

ScriptOptions scriptoptions = ScriptOptions.Default
    .WithImports("System", "YOURNAMESPACE")
    .WithReferences(System.Reflection.Assembly.GetExecutingAssembly());

ScriptOptions are universal and may be static and reused for multiple evaluations.

You may then evaluate the equation/C# code like so:

Func<double, double, double, double, double> equation =
    CSharpScript.EvaluateAsync<Func<double, double, double, double, double>>(
        "(individualTotal, parentTotal, siblingTotal, anotherTotal) => { return " + calculationText + "; }",
        scriptoptions
    ).Result;

Where calculationText may be ((siblingTotal/(parentTotal)*individualTotal)+(anotherTotal*0.5).

equation may then be invoked in your code like so:

var calculation = equation(individualTotal, parentTotal, siblingTotal, anotherTotal);

Memory usage and GC Issues

This evaluation is not light on memory usage and can use up to 500MB of memory. This is only used for evaluating the expression and is marked for garbage collection just after CSharpScript.EvaluateAsync.

If you see your application using 500MB and being as confused as I was when I started using CSharpScript.EvaluateAsync rest assured knowing that your application is not using 500MB of memory, and that memory is already marked for garbage collection.

If this is an application the user will be running on their machine you can call a GC.Collect directly after the CSharpScript.EvaluateAsync to force the GC to clean the marked memory immediately, like so:

Func<double, double, double, double, double> equation =
    CSharpScript.EvaluateAsync<Func<double, double, double, double, double>>(
        "(individualTotal, parentTotal, siblingTotal, anotherTotal) => { return " + calculationText + "; }",
        scriptoptions
    ).Result;

GC.Collect();
Prime
  • 2,410
  • 1
  • 20
  • 35
  • I have personally used this method to create an application to plot arbitrary equations to a bitmapped image where the equation can be changed by the user by editing it in its TextBox; https://i.imgur.com/iufB93w.png (TextBox is just below the bitmap, reading `sin(x * y)`) – Prime Oct 16 '20 at 11:52
  • Also note that evaluating code is generally _not_ fast and may take multiple milliseconds per evaluation. – Prime Oct 16 '20 at 11:54
  • The alternative to this is to use either a 3rd party math evaluation library (Used one before but I don't feel qualified to suggest one), or to use `DataTable` https://stackoverflow.com/a/5839027/1481699 – Prime Oct 16 '20 at 11:59
1

You can create a function that returns the calc result, then you can interpolate it, you also will need to store the fields values directly in your database instead a string, so you can pass those values to the function.

Follow an example based in your two calcs, I made it in a console app just to illustrate how it could work:

class Program
    {
        static void Main(string[] args)
        {
            double individualTotal = 100;
            double parentTotal = 2000;
            double siblingTotal = 1500;
            double anotherTotal = 100;

            var test  = new Test();
            var calculation1 = $"{test.Calcs(CalculatorEnum.Calc1, individualTotal, parentTotal, siblingTotal, anotherTotal)}";
            var calculation2 = $"{test.Calcs(CalculatorEnum.Calc2, individualTotal, parentTotal, siblingTotal, anotherTotal)}";
            Console.WriteLine($"Calc 1: {calculation1}");
            Console.WriteLine($"Calc 2: {calculation2}");
        }

    }

I also created a class called Test with the method and an enum to order what calc this function should use:

public enum CalculatorEnum
    {
        Calc1 = 1,
        Calc2 = 2
    }

    public class Test
    {
        public double Calcs(CalculatorEnum calcType, double individualTotal, double parentTotal, double siblingTotal, double anotherTotal)
        {
            double resp = 0;

            switch (calcType)
            {
                case CalculatorEnum.Calc1:
                    resp = (siblingTotal / parentTotal) * individualTotal;
                    break;
                case CalculatorEnum.Calc2:
                    resp = ((siblingTotal / (parentTotal) * individualTotal) + (anotherTotal * 0.5));
                    break;
            }

            return resp;
        }
    }

Those are the results expected:

$ dotnet run
Calc 1: 75
Calc 2: 125