Configuring Logic
This question talks about removing a switch statement so that every time the business logic changes concerning a multiplier value, the C# code itself doesn’t have to be changed and the application recompiled. I proposed loading the keys and multiplier values from a configuration file into a dictionary and accessing the data when needed. (The following example shows it loaded in the constructor for brevity.)
public class TransportationCostCalculator
{
Dictionary<string,double> _travelModifier;
public double DistanceToDestination { get; set; }
TransportationCostCalculator()
{
_travelModifier = new Dictionary<string,double> ();
_travelModifier.Add("bicycle", 1);
_travelModifier.Add("bus", 2);
_travelModifier.Add("car", 3);
}
public decimal CostOfTravel(string transportationMethod) =>
(decimal) _travelModifier[transportationMethod] * DistanceToDestination;
}
A comment in the answer mentioned the benefits of creating extra classes, and how the dictionary approach could not handle more advanced calculations should the need arise. With a slight modification, and some additional code, this no longer becomes a hinderance. Expression Trees allow the program to dynamically create functions and execute them as it would with compiled code.
Based on the question and the example above, the current equation has two parts, the travelModifier (which is determined by the mode of transportation) and the DistanceToDestination. These are multiplied together, and return a decimal. Completely abstracting this out into its own function (which then becomes the model to base the configurable functions from), would make the method look like:
public decimal CalculateTravel
(int travelModifier, double distanceToDestination) =>
(decimal) travelModifier * distanceToDestination
Since the travel modifier already comes from the configuration file, it is unnecessary to pass that into the function, because when the application reads the configuration and creates the method, each entry will have the travelModifier value already coded into the function so that parameter can be removed, and an example function in C# would look like:
decimal CalculateBikeTravel(double distanceToDestination) =>
1 * distanceToDestination;
To accomplish this, each entry in the configuration file would need to have two parts, the method of travel (bicycle, bus, car, etc.), and the equation. The latter is a combination of the travelModifier constant, the distanceToDestination and operators (+,-,/,*). An entry in the file would look like this:
car,3 * distanceToDestination
Before loading the configuration file, the dictionary which will hold the function and retrieve it based on the selected method of travel will need to be changed. Currently it has a string as the key and a double as the value:
Dictionary<string,double> _travelModifier;
Instead, it needs a function as the value.
Dictionary<string,Func<double,decimal>> _travelModifier
Loading the contents from the configuration file has a few different steps. Retrieving and separating the parts, parsing the equation, and creating the method at runtime.
Loading the Configuration File and Separating the Parts
var entries = System.IO.File.ReadAllLines("ConfigFile.txt");
var keysAndEquations = entries.Select(entry => entry.split(','));
Parsing the Equation
It would be possible to parse the equation and immediately convert it to an Expression, but it’s normally easier to load it into an intermediate structure so data can be transformed and grouped into a usable structure first. The equation has three parts, and an enum can help distinguish between them.
enum OperatorType
{
Variable,
Operand,
Constant
}
and the class to hold the equation parts
class EquationPart
{
public EquationPart LeftOperand;
public EquationPart RightOperand;
public String Name;
public OperatorType OType;
public override string ToString() => ToString(0);
private string ToString(int indent)
{
return $@"{Name} : {OType}
{LeftOperand.ToString(indent + 1)}
{RightOperand.ToString(indent + 1)}";
}
}
In order to parse the equation, the program needs to determine what is an operator and what is a variable or constant and its execution order.
public static int OperandPrecedence(string item)
{
switch (item)
{
case "+":
case "-": return 1;
case "*":
case "/": return 2;
default: return 0;
}
}
public static EquationPart ParseStatement
(IEnumerable<string> statement, EquationPart tree)
{
if (!statement.Any()) { return tree; }
var part = statement.First();
switch (OperandPrecedence(part))
{
case 2:
{
var op = new EquationPart
{
Name = part,
OType = OperatorType.Operand,
LeftOperand = tree,
RightOperand = ParseStatement(statement.Skip(1).Take(1),tree)
};
return ParseStatement(statement.Skip(2), op);
}
case 1:
{
return new EquationPart
{
Name = part,
OType = OperatorType.Operand,
LeftOperand = tree,
RightOperand = ParseStatement(statement.Skip(1), null)
};
}
default:
{
int result;
return ParseStatement(statement.Skip(1), new EquationPart
{
Name = part,
OType = (int.TryParse(part, out result))?
OperatorType.Constant : OperatorType.Variable
});
}
}
A Note About Math things
Execution Order
Consider the following: **2 + 4 / 2**. At first glance, it looks like the answer is three, but that is incorrect. The multiplication and division have a higher operator order precedence and their calculations occur before addition and subtraction. This makes the actual answer 4. The C# compiler knows about order of operations and which happens first. When building the expression tree, the runtime doesn't take this into account, and will execute each operation strictly from left to right. It is important to note this when creating and grouping the intermediate objects to form a tree with the execution order, so it is correct.Making the Expression
The System.LINQ.Expressions.Expression is the class used to create the lambda expressions. The actual method to create the function is Expression.Lambda<T> and then call its compile function to turn it into a callable method.
Expression.Lambda<Func>int, double>> (body, parameterExpression).Compile();
The Lambda function requires two parameters, an Expression, and a ParameterExpression[]. The entries in the ParameterExpression[] are the parameters to the function and they are made by calling Expression.Parameter.
var distanceParm = Expression.Parameter(typeof(int), "distanceToDestination");
var parameterExpression = new ParameterExpression[] {distanceParm};
Expression Body
Each Expression object is a tree of Expression objects. The four methods used to create the operator functions (Expression.Add, Expression.Subtract, Expression.Multiply, and Expression.Divide) all take two Expression parameters (the left term and the right term), and each Expression can be one of three things, a constant (Expression.Constant), the supplied parameter (ParameterExpression), or another Expression.
With this, all that is necessary is to convert the EquationPart tree into an expression.
public static Expression MakeBody
(EquationPart tree, ParameterExpression distance)
{
if (tree.OType == OperatorType.Operand)
{
var leftAction = MakeBody(tree.LeftOperand, distance);
var rightAction = MakeBody(tree.RightOperand, distance);
var action = Code.GetModifier(tree.Name)(Expression.Convert
(leftAction, typeof(double)),
Expression.Convert(rightAction, typeof(double)));
return action;
}
return (tree.Name == distance.Name) ?
(Expression)distance : Expression.Constant(int.Parse(tree.Name), typeof(int));
}
Additional Actions
It might be necessary to do additional actions in the expression, for example method’s output could be logged to the console. To do this, the Lambda Expression would now need to:
- Calculate the result of the equation (calling the created equation).
- Assign that value to a variable.
- Write the variable contents out to the console.
- Return the result stored in the variable.
Right now, the body of the Lambda Expression is the result of a single Expression object. All the actions culminate to a single result, but when adding logging, this changes. Calculating the result and logging it are separate unrelated actions. The Expression.Block groups Expressions together, and returns the value from the last executed Expression.
The first step is creating a variable using Expression.Variable it takes a Type and optionally a variable name.
var result = Expression.Variable(typeof(double), "result");
Then assign the results of the body Expression to it:
var assign = Expression.Assign(result, body);
Now the system can log the result, by using Expression.Call.
var write = Expression.Call(
typeof(Console).GetMethod(
"WriteLine", new Type[1]{typeof(double)}),
result);
The Expression.Block method takes Expressions to be executed in the entered order. The only exception to this is the creation of the variable which much be passed into the method by a ParameterExpression[].
var block = Expression.Block(
new ParameterExpression[] {result},assign,write,result);
return Expression.Lambda<Func<int, double>> (block, parameterExpression).Compile();
The full method with the console output looks like this:
public static Func<int, double> CreateStatement(string statement)
{
var statementParts = statement.Split(' ');
var tree = ParseStatement(statementParts, null);
var travelParm = Expression.Parameter(typeof(int), "distanceToDestination");
var parameterExpression = new ParameterExpression[] { travelParm };
var body = MakeBody(tree, travelParm);
var result = Expression.Variable(typeof(double), "result");
var assign = Expression.Assign(result, body);
var write = Expression.Call(typeof(Console)
.GetMethod("WriteLine", new Type[1]{typeof(double)}),
result);
var block = Expression.Block(
new ParameterExpression[] {result},assign,write,result);
return Expression.Lambda<Func<int, double>>
(block, parameterExpression).Compile();
}
If/Then
The methods use the double type resulting in the impossibility of a DivideByZeroException. Per the C# specification, it returns the value infinity.
To create a conditional statement use the Expression.Condition method which has three parameters (the Expression for the test, the true block, and the false block).
Test Condition
The test condition is an Expression, and the double type has a static method for checking for the infinity value. To use it, the Expression.Call method works just like it did with writing data to the Console.WriteLine.
Expression.Call(
typeof(double).GetMethod("IsInfinity", new Type[1] {typeof(double)}),
resultToCheck);
True Block
If the condition is true (meaning that the value is infinity, then it should throw an exception indicating a problem. Expression has a method for throwing exceptions, Expression.Throw
var trueBlock = Expression.Throw
(Expression.Constant(new Exception("Result is infinity")));
Empty False Statement
A false statement isn’t necessary, because if the condition is false, it will continue to the next statement outside of the condition. The Expression.Condition will not allow null as the third parameter, so to have an empty false statement use Expression.Empty instead.
static Expression CreateInfinityCondition(ParameterExpression resultToCheck)
{
var test = Expression.Call(
typeof(double).GetMethod("IsInfinity", new Type[1] {typeof(double)}),
resultToCheck);
var trueBlock = Expression.Throw(
Expression.Constant(new Exception("Result is infinity")));
return Expression.Condition(test, trueBlock, Expression.Empty());
}
Try Catch
Instead of passing the exception to the calling method, a second option would be to log it first by wrapping the method contents in a try-catch block. The Expression.TryCatch method has two parameters: the expression which contains the body information in the try statement, and the CatchBlock. Expression.MakeCatchBlock has three parameters: the type of Exception the catch block is for, the ParameterExpression which allows the Expression to bind the Exception to a variable for use, and the Expression code inside the catch statement.
var parameterException = Expression.Parameter(typeof(Exception));
var logCatchException = Expression.Call(typeof(Console).GetMethod("WriteLine",
new Type[1] { typeof(string) }),
Expression.Call(parameterException,
typeof(Exception).GetMethod("ToString"))
);
var logCatchStatement = Expression.Call(typeof(Console).GetMethod("WriteLine",
new Type[1] { typeof(string) }),
Expression.Constant("Configuration statement: " + statement));
var catchBlockCode = Expression.Block(logCatchException, logCatchStatement,
Expression.Rethrow(typeof(double)));
var catchBlock = Expression.MakeCatchBlock
(typeof(Exception), parameterException ,catchBlockCode,null);
var tryCatch = Expression.TryCatch(mainBody, catchBlock);
Expression.Rethrow
Expression.Rethrow has two method signatures. The first has not parameters, and the second has a parameter of type of Type. In this example, since it is the last statement in the catch block (the the statement in a block determines what is returned from the block), if you use Expression.Throw(), the application will return with this error: Body of catch must have the same type as body of try. This is saying that the the try and catch blocks must have the same return type. In the example, the try block returns type double, so the catch block must do the same. The overload for Expression.Throw(Type), tells the runtime “This catch statement will return this type if necessary.” Since it’s throwing the exception, it won’t ever return a value, but this tells the Expression generator this will be the intended behavior if an exception doesn’t occur.