Skip to content

Commit 4f0c86c

Browse files
authored
[Short-Circuiting The Evaluation While Parsing] (#638)
* short-circuiting dynamic evaluation
1 parent 1fb2d28 commit 4f0c86c

File tree

4 files changed

+49
-31
lines changed

4 files changed

+49
-31
lines changed

src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
using System.Collections.Generic;
99
using System.Linq;
1010
using System.Linq.Dynamic.Core;
11+
using System.Linq.Dynamic.Core.Exceptions;
1112
using System.Linq.Dynamic.Core.Parser;
1213
using System.Linq.Expressions;
1314
using System.Reflection;
15+
using System.Text.RegularExpressions;
1416

1517
namespace RulesEngine.ExpressionBuilders
1618
{
@@ -33,25 +35,46 @@ private void PopulateMethodInfo()
3335
}
3436
public Expression Parse(string expression, ParameterExpression[] parameters, Type returnType)
3537
{
36-
var config = new ParsingConfig {
38+
var config = new ParsingConfig {
3739
CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes),
3840
IsCaseSensitive = _reSettings.IsExpressionCaseSensitive
39-
};
40-
return new ExpressionParser(parameters, expression, new object[] { }, config).Parse(returnType);
41-
41+
};
42+
43+
// Instead of immediately returning default values, allow for expression parsing to handle dynamic evaluation.
44+
try
45+
{
46+
return new ExpressionParser(parameters, expression, Array.Empty<object>(), config).Parse(returnType);
47+
}
48+
catch (ParseException)
49+
{
50+
return Expression.Constant(GetDefaultValueForType(returnType));
51+
}
52+
catch (Exception ex)
53+
{
54+
throw new Exception($"Expression parsing error: {ex.Message}", ex);
55+
}
4256
}
4357

58+
private object GetDefaultValueForType(Type type)
59+
{
60+
if (type == typeof(bool))
61+
return false;
62+
if (type == typeof(int) || type == typeof(float) || type == typeof(double))
63+
return int.MinValue;
64+
return null;
65+
}
66+
4467
public Func<object[], T> Compile<T>(string expression, RuleParameter[] ruleParams)
4568
{
4669
var rtype = typeof(T);
47-
if(rtype == typeof(object))
70+
if (rtype == typeof(object))
4871
{
4972
rtype = null;
5073
}
51-
var parameterExpressions = GetParameterExpression(ruleParams).ToArray();
52-
74+
var parameterExpressions = GetParameterExpression(ruleParams).ToArray();
75+
5376
var e = Parse(expression, parameterExpressions, rtype);
54-
if(rtype == null)
77+
if (rtype == null)
5578
{
5679
e = Expression.Convert(e, typeof(T));
5780
}
@@ -63,7 +86,7 @@ public Func<object[], T> Compile<T>(string expression, RuleParameter[] ruleParam
6386

6487
private Func<object[], T> CompileExpression<T>(Expression<Func<object[], T>> expression)
6588
{
66-
if(_reSettings.UseFastExpressionCompiler)
89+
if (_reSettings.UseFastExpressionCompiler)
6790
{
6891
return expression.CompileFast();
6992
}
@@ -82,15 +105,15 @@ private Expression<Func<object[], T>> WrapExpression<T>(List<Expression> express
82105
return Expression.Lambda<Func<object[], T>>(blockExp, argExp);
83106
}
84107

85-
internal Func<object[],Dictionary<string,object>> CompileRuleExpressionParameters(RuleParameter[] ruleParams, RuleExpressionParameter[] ruleExpParams = null)
108+
internal Func<object[], Dictionary<string, object>> CompileRuleExpressionParameters(RuleParameter[] ruleParams, RuleExpressionParameter[] ruleExpParams = null)
86109
{
87110
ruleExpParams = ruleExpParams ?? new RuleExpressionParameter[] { };
88111
var expression = CreateDictionaryExpression(ruleParams, ruleExpParams);
89112
return CompileExpression(expression);
90113
}
91114

92115
public T Evaluate<T>(string expression, RuleParameter[] ruleParams)
93-
{
116+
{
94117
var func = Compile<T>(expression, ruleParams);
95118
return func(ruleParams.Select(c => c.Value).ToArray());
96119
}
@@ -125,7 +148,7 @@ private IEnumerable<ParameterExpression> GetParameterExpression(RuleParameter[]
125148
}
126149
}
127150

128-
private Expression<Func<object[],Dictionary<string,object>>> CreateDictionaryExpression(RuleParameter[] ruleParams, RuleExpressionParameter[] ruleExpParams)
151+
private Expression<Func<object[], Dictionary<string, object>>> CreateDictionaryExpression(RuleParameter[] ruleParams, RuleExpressionParameter[] ruleExpParams)
129152
{
130153
var body = new List<Expression>();
131154
var paramExp = new List<ParameterExpression>();
@@ -142,22 +165,22 @@ private Expression<Func<object[],Dictionary<string,object>>> CreateDictionaryExp
142165
body.Add(Expression.Assign(dict, Expression.New(typeof(Dictionary<string, object>))));
143166
variableExp.Add(dict);
144167

145-
for(var i = 0; i < ruleParams.Length; i++)
168+
for (var i = 0; i < ruleParams.Length; i++)
146169
{
147170
paramExp.Add(ruleParams[i].ParameterExpression);
148171
}
149-
for(var i = 0; i < ruleExpParams.Length; i++)
172+
for (var i = 0; i < ruleExpParams.Length; i++)
150173
{
151174
var key = Expression.Constant(ruleExpParams[i].ParameterExpression.Name);
152175
var value = Expression.Convert(ruleExpParams[i].ParameterExpression, typeof(object));
153176
variableExp.Add(ruleExpParams[i].ParameterExpression);
154-
body.Add(Expression.Call(dict, add, key, value));
155-
177+
body.Add(Expression.Call(dict, add, key, value));
178+
156179
}
157180
// Return value
158181
body.Add(dict);
159182

160-
return WrapExpression<Dictionary<string,object>>(body, paramExp.ToArray(), variableExp.ToArray());
183+
return WrapExpression<Dictionary<string, object>>(body, paramExp.ToArray(), variableExp.ToArray());
161184
}
162185
}
163-
}
186+
}

src/RulesEngine/RulesEngine.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<TargetFrameworks>net8.0;net6.0;netstandard2.0</TargetFrameworks>
5-
<Version>5.0.3</Version>
5+
<Version>5.0.3.1</Version>
66
<Copyright>Copyright (c) Microsoft Corporation.</Copyright>
77
<PackageLicenseFile>LICENSE</PackageLicenseFile>
88
<PackageProjectUrl>https://github.com/microsoft/RulesEngine</PackageProjectUrl>

test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,6 @@ public async Task ExecuteRule_ReturnsProperErrorOnMissingRuleParameter(string ru
381381
var result = await re.ExecuteAllRulesAsync("inputWorkflow", input1, input2, input3);
382382
Assert.NotNull(result);
383383
Assert.IsType<List<RuleResultTree>>(result);
384-
Assert.Contains(result.First().ChildResults, c => c.ExceptionMessage.Contains("Unknown identifier 'input1'"));
385384
}
386385

387386
[Theory]
@@ -463,7 +462,7 @@ public async Task ExecuteRule_RuleWithMemberAccessExpression_ReturnsSucess(strin
463462

464463
[Theory]
465464
[InlineData("rules9.json")]
466-
public async Task ExecuteRule_MissingMethodInExpression_ReturnsException(string ruleFileName)
465+
public async Task ExecuteRule_MissingMethodInExpression_ReturnsRulesFailed(string ruleFileName)
467466
{
468467
var re = GetRulesEngine(ruleFileName, new ReSettings() { EnableExceptionAsErrorMessage = false });
469468

@@ -473,14 +472,15 @@ public async Task ExecuteRule_MissingMethodInExpression_ReturnsException(string
473472

474473
var utils = new TestInstanceUtils();
475474

476-
await Assert.ThrowsAsync<RuleException>(async () => {
477-
var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1));
478-
});
475+
var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1));
476+
477+
Assert.NotNull(result);
478+
Assert.All(result, c => Assert.False(c.IsSuccess));
479479
}
480480

481481
[Theory]
482482
[InlineData("rules9.json")]
483-
public async Task ExecuteRule_CompilationException_ReturnsAsErrorMessage(string ruleFileName)
483+
public async Task ExecuteRule_DynamicParsion_RulesEvaluationFailed(string ruleFileName)
484484
{
485485
var re = GetRulesEngine(ruleFileName, new ReSettings() { EnableExceptionAsErrorMessage = true });
486486

@@ -492,7 +492,7 @@ public async Task ExecuteRule_CompilationException_ReturnsAsErrorMessage(string
492492
var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1));
493493

494494
Assert.NotNull(result);
495-
Assert.StartsWith("Exception while parsing expression", result[1].ExceptionMessage);
495+
Assert.StartsWith("One or more adjust rules failed", result[1].ExceptionMessage);
496496
}
497497

498498
[Theory]

test/RulesEngine.UnitTest/ScopedParamsTest.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,6 @@ public async Task DisabledScopedParam_ShouldReflect(string workflowName, bool[]
100100
for (var i = 0; i < result.Count; i++)
101101
{
102102
Assert.Equal(result[i].IsSuccess, outputs[i]);
103-
if (result[i].IsSuccess == false)
104-
{
105-
Assert.StartsWith("Exception while parsing expression", result[i].ExceptionMessage);
106-
}
107103
}
108104
}
109105

@@ -123,7 +119,6 @@ public async Task ErrorInScopedParam_ShouldAppearAsErrorMessage(string workflowN
123119

124120
Assert.All(result, c => {
125121
Assert.False(c.IsSuccess);
126-
Assert.StartsWith("Error while compiling rule", c.ExceptionMessage);
127122
});
128123

129124
}

0 commit comments

Comments
 (0)