diff --git a/.github/workflows/dotnetcore-build.yml b/.github/workflows/dotnetcore-build.yml index 32f2930a..c59946ce 100644 --- a/.github/workflows/dotnetcore-build.yml +++ b/.github/workflows/dotnetcore-build.yml @@ -34,7 +34,7 @@ jobs: - name: Check Coverage shell: pwsh - run: ./scripts/check-coverage.ps1 -reportPath coveragereport/Cobertura.xml -threshold 93 + run: ./scripts/check-coverage.ps1 -reportPath coveragereport/Cobertura.xml -threshold 92 - name: Coveralls GitHub Action uses: coverallsapp/github-action@v2.3.6 diff --git a/demo/DemoApp/Program.cs b/demo/DemoApp/Program.cs index fe87de6d..61a0bf13 100644 --- a/demo/DemoApp/Program.cs +++ b/demo/DemoApp/Program.cs @@ -1,16 +1,16 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace DemoApp -{ - public static class Program - { - public static void Main(string[] args) - { - new BasicDemo().Run(); - new JSONDemo().Run(); - new NestedInputDemo().Run(); - new EFDemo().Run(); - } - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace DemoApp +{ + public static class Program + { + public static void Main(string[] args) + { + new BasicDemo().Run(); + new JSONDemo().Run(); + new NestedInputDemo().Run(); + new EFDemo().Run(); + } + } +} \ No newline at end of file diff --git a/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs b/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs index 1b38819c..dff25653 100644 --- a/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs +++ b/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs @@ -1,43 +1,44 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using FastExpressionCompiler; -using RulesEngine.HelperFunctions; -using RulesEngine.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Dynamic.Core; -using System.Linq.Dynamic.Core.Exceptions; -using System.Linq.Dynamic.Core.Parser; -using System.Linq.Expressions; -using System.Reflection; -using System.Text.RegularExpressions; - -namespace RulesEngine.ExpressionBuilders -{ - public class RuleExpressionParser - { - private readonly ReSettings _reSettings; - private readonly IDictionary _methodInfo; - - public RuleExpressionParser(ReSettings reSettings = null) - { - _reSettings = reSettings ?? new ReSettings(); - _methodInfo = new Dictionary(); - PopulateMethodInfo(); - } - - private void PopulateMethodInfo() - { - var dict_add = typeof(Dictionary).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(string), typeof(object) }, null); - _methodInfo.Add("dict_add", dict_add); - } - public Expression Parse(string expression, ParameterExpression[] parameters, Type returnType) - { +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using FastExpressionCompiler; +using RulesEngine.HelperFunctions; +using RulesEngine.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Linq.Dynamic.Core.Exceptions; +using System.Linq.Dynamic.Core.Parser; +using System.Linq.Expressions; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace RulesEngine.ExpressionBuilders +{ + public class RuleExpressionParser + { + private readonly ReSettings _reSettings; + private readonly IDictionary _methodInfo; + + public RuleExpressionParser(ReSettings reSettings = null) + { + _reSettings = reSettings ?? new ReSettings(); + _methodInfo = new Dictionary(); + PopulateMethodInfo(); + } + + private void PopulateMethodInfo() + { + var dict_add = typeof(Dictionary).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(string), typeof(object) }, null); + _methodInfo.Add("dict_add", dict_add); + } + + public Expression Parse(string expression, ParameterExpression[] parameters, Type returnType) + { var config = new ParsingConfig { - CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes), - IsCaseSensitive = _reSettings.IsExpressionCaseSensitive + CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes), + IsCaseSensitive = _reSettings.IsExpressionCaseSensitive }; // Instead of immediately returning default values, allow for expression parsing to handle dynamic evaluation. @@ -47,14 +48,18 @@ public Expression Parse(string expression, ParameterExpression[] parameters, Typ } catch (ParseException) { + if (_reSettings.EnableExceptionAsErrorMessageForRuleExpressionParsing) + { + throw; + } return Expression.Constant(GetDefaultValueForType(returnType)); } - catch (Exception ex) + catch (Exception) { - throw new Exception($"Expression parsing error: {ex.Message}", ex); - } - } - + throw; + } + } + private object GetDefaultValueForType(Type type) { if (type == typeof(bool)) @@ -63,124 +68,124 @@ private object GetDefaultValueForType(Type type) return int.MinValue; return null; } - - public Func Compile(string expression, RuleParameter[] ruleParams) - { - var rtype = typeof(T); - if (rtype == typeof(object)) - { - rtype = null; - } + + public Func Compile(string expression, RuleParameter[] ruleParams) + { + var rtype = typeof(T); + if (rtype == typeof(object)) + { + rtype = null; + } var parameterExpressions = GetParameterExpression(ruleParams).ToArray(); - var e = Parse(expression, parameterExpressions, rtype); - if (rtype == null) - { - e = Expression.Convert(e, typeof(T)); - } - var expressionBody = new List() { e }; - var wrappedExpression = WrapExpression(expressionBody, parameterExpressions, new ParameterExpression[] { }); - return CompileExpression(wrappedExpression); - - } - - private Func CompileExpression(Expression> expression) - { - if (_reSettings.UseFastExpressionCompiler) - { - return expression.CompileFast(); - } - return expression.Compile(); - } - - private Expression> WrapExpression(List expressionList, ParameterExpression[] parameters, ParameterExpression[] variables) - { - var argExp = Expression.Parameter(typeof(object[]), "args"); - var paramExps = parameters.Select((c, i) => { - var arg = Expression.ArrayAccess(argExp, Expression.Constant(i)); - return (Expression)Expression.Assign(c, Expression.Convert(arg, c.Type)); - }); - var blockExpSteps = paramExps.Concat(expressionList); - var blockExp = Expression.Block(parameters.Concat(variables), blockExpSteps); - return Expression.Lambda>(blockExp, argExp); - } - - internal Func> CompileRuleExpressionParameters(RuleParameter[] ruleParams, RuleExpressionParameter[] ruleExpParams = null) - { - ruleExpParams = ruleExpParams ?? new RuleExpressionParameter[] { }; - var expression = CreateDictionaryExpression(ruleParams, ruleExpParams); - return CompileExpression(expression); - } - - public T Evaluate(string expression, RuleParameter[] ruleParams) + var e = Parse(expression, parameterExpressions, rtype); + if (rtype == null) + { + e = Expression.Convert(e, typeof(T)); + } + var expressionBody = new List() { e }; + var wrappedExpression = WrapExpression(expressionBody, parameterExpressions, new ParameterExpression[] { }); + return CompileExpression(wrappedExpression); + + } + + private Func CompileExpression(Expression> expression) { - var func = Compile(expression, ruleParams); - return func(ruleParams.Select(c => c.Value).ToArray()); - } - - private IEnumerable CreateAssignedParameterExpression(RuleExpressionParameter[] ruleExpParams) - { - return ruleExpParams.Select((c, i) => { - return Expression.Assign(c.ParameterExpression, c.ValueExpression); - }); - } - - // - /// Gets the parameter expression. - /// - /// The types. - /// - /// - /// types - /// or - /// type - /// - private IEnumerable GetParameterExpression(RuleParameter[] ruleParams) - { - foreach (var ruleParam in ruleParams) - { - if (ruleParam == null) - { - throw new ArgumentException($"{nameof(ruleParam)} can't be null."); - } - - yield return ruleParam.ParameterExpression; - } - } - - private Expression>> CreateDictionaryExpression(RuleParameter[] ruleParams, RuleExpressionParameter[] ruleExpParams) - { - var body = new List(); - var paramExp = new List(); - var variableExp = new List(); - - - var variableExpressions = CreateAssignedParameterExpression(ruleExpParams); - - body.AddRange(variableExpressions); - - var dict = Expression.Variable(typeof(Dictionary)); - var add = _methodInfo["dict_add"]; - - body.Add(Expression.Assign(dict, Expression.New(typeof(Dictionary)))); - variableExp.Add(dict); - - for (var i = 0; i < ruleParams.Length; i++) - { - paramExp.Add(ruleParams[i].ParameterExpression); - } - for (var i = 0; i < ruleExpParams.Length; i++) - { - var key = Expression.Constant(ruleExpParams[i].ParameterExpression.Name); - var value = Expression.Convert(ruleExpParams[i].ParameterExpression, typeof(object)); - variableExp.Add(ruleExpParams[i].ParameterExpression); + if (_reSettings.UseFastExpressionCompiler) + { + return expression.CompileFast(); + } + return expression.Compile(); + } + + private Expression> WrapExpression(List expressionList, ParameterExpression[] parameters, ParameterExpression[] variables) + { + var argExp = Expression.Parameter(typeof(object[]), "args"); + var paramExps = parameters.Select((c, i) => { + var arg = Expression.ArrayAccess(argExp, Expression.Constant(i)); + return (Expression)Expression.Assign(c, Expression.Convert(arg, c.Type)); + }); + var blockExpSteps = paramExps.Concat(expressionList); + var blockExp = Expression.Block(parameters.Concat(variables), blockExpSteps); + return Expression.Lambda>(blockExp, argExp); + } + + internal Func> CompileRuleExpressionParameters(RuleParameter[] ruleParams, RuleExpressionParameter[] ruleExpParams = null) + { + ruleExpParams = ruleExpParams ?? new RuleExpressionParameter[] { }; + var expression = CreateDictionaryExpression(ruleParams, ruleExpParams); + return CompileExpression(expression); + } + + public T Evaluate(string expression, RuleParameter[] ruleParams) + { + var func = Compile(expression, ruleParams); + return func(ruleParams.Select(c => c.Value).ToArray()); + } + + private IEnumerable CreateAssignedParameterExpression(RuleExpressionParameter[] ruleExpParams) + { + return ruleExpParams.Select((c, i) => { + return Expression.Assign(c.ParameterExpression, c.ValueExpression); + }); + } + + // + /// Gets the parameter expression. + /// + /// The types. + /// + /// + /// types + /// or + /// type + /// + private IEnumerable GetParameterExpression(RuleParameter[] ruleParams) + { + foreach (var ruleParam in ruleParams) + { + if (ruleParam == null) + { + throw new ArgumentException($"{nameof(ruleParam)} can't be null."); + } + + yield return ruleParam.ParameterExpression; + } + } + + private Expression>> CreateDictionaryExpression(RuleParameter[] ruleParams, RuleExpressionParameter[] ruleExpParams) + { + var body = new List(); + var paramExp = new List(); + var variableExp = new List(); + + + var variableExpressions = CreateAssignedParameterExpression(ruleExpParams); + + body.AddRange(variableExpressions); + + var dict = Expression.Variable(typeof(Dictionary)); + var add = _methodInfo["dict_add"]; + + body.Add(Expression.Assign(dict, Expression.New(typeof(Dictionary)))); + variableExp.Add(dict); + + for (var i = 0; i < ruleParams.Length; i++) + { + paramExp.Add(ruleParams[i].ParameterExpression); + } + for (var i = 0; i < ruleExpParams.Length; i++) + { + var key = Expression.Constant(ruleExpParams[i].ParameterExpression.Name); + var value = Expression.Convert(ruleExpParams[i].ParameterExpression, typeof(object)); + variableExp.Add(ruleExpParams[i].ParameterExpression); body.Add(Expression.Call(dict, add, key, value)); - } - // Return value - body.Add(dict); - - return WrapExpression>(body, paramExp.ToArray(), variableExp.ToArray()); - } - } + } + // Return value + body.Add(dict); + + return WrapExpression>(body, paramExp.ToArray(), variableExp.ToArray()); + } + } } \ No newline at end of file diff --git a/src/RulesEngine/Models/ReSettings.cs b/src/RulesEngine/Models/ReSettings.cs index 5356533d..7071b131 100644 --- a/src/RulesEngine/Models/ReSettings.cs +++ b/src/RulesEngine/Models/ReSettings.cs @@ -28,6 +28,7 @@ internal ReSettings(ReSettings reSettings) IsExpressionCaseSensitive = reSettings.IsExpressionCaseSensitive; AutoRegisterInputType = reSettings.AutoRegisterInputType; UseFastExpressionCompiler = reSettings.UseFastExpressionCompiler; + EnableExceptionAsErrorMessageForRuleExpressionParsing = reSettings.EnableExceptionAsErrorMessageForRuleExpressionParsing; } @@ -84,6 +85,11 @@ internal ReSettings(ReSettings reSettings) /// Whether to use FastExpressionCompiler for rule compilation /// public bool UseFastExpressionCompiler { get; set; } = true; + /// + /// Sets the mode for ParsingException to cascade to child elements and result in a expression parser + /// Default: true + /// + public bool EnableExceptionAsErrorMessageForRuleExpressionParsing { get; set; } = true; } public enum NestedRuleExecutionMode diff --git a/src/RulesEngine/RulesEngine.cs b/src/RulesEngine/RulesEngine.cs index b28e0da7..627b7b43 100644 --- a/src/RulesEngine/RulesEngine.cs +++ b/src/RulesEngine/RulesEngine.cs @@ -290,7 +290,9 @@ private bool RegisterRule(string workflowName, params RuleParameter[] ruleParams var dictFunc = new Dictionary>(); if (_reSettings.AutoRegisterInputType) { - _reSettings.CustomTypes = _reSettings.CustomTypes.Safe().Union(ruleParams.Select(c => c.Type)).ToArray(); + //Disabling fast expression compiler if custom types are used + _reSettings.UseFastExpressionCompiler = (_reSettings.CustomTypes?.Length > 0) ? false : _reSettings.UseFastExpressionCompiler; + _reSettings.CustomTypes.Safe().Union(ruleParams.Select(c => c.Type)).ToArray(); } // add separate compilation for global params diff --git a/src/RulesEngine/RulesEngine.csproj b/src/RulesEngine/RulesEngine.csproj index 2b9296ee..d777f0e0 100644 --- a/src/RulesEngine/RulesEngine.csproj +++ b/src/RulesEngine/RulesEngine.csproj @@ -3,7 +3,7 @@ net6.0;net8.0;net9.0;netstandard2.0 13.0 - 5.0.5 + 5.0.6 Copyright (c) Microsoft Corporation. LICENSE https://github.com/microsoft/RulesEngine diff --git a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs index efd1a7c2..f60a6de6 100644 --- a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs +++ b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs @@ -388,7 +388,11 @@ public async Task ExecuteRule_ReturnsProperErrorOnMissingRuleParameter(string ru [InlineData("rules5.json", null, false)] public async Task ExecuteRule_WithInjectedUtils_ReturnsListOfRuleResultTree(string ruleFileName, string propValue, bool expectedResult) { - var re = GetRulesEngine(ruleFileName); + var reSettings = new ReSettings() { + CustomTypes = new Type[] { typeof(TestInstanceUtils) } + }; + + var re = GetRulesEngine(ruleFileName, reSettings); dynamic input1 = new ExpandoObject(); @@ -472,10 +476,9 @@ public async Task ExecuteRule_MissingMethodInExpression_ReturnsRulesFailed(strin var utils = new TestInstanceUtils(); - var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); - - Assert.NotNull(result); - Assert.All(result, c => Assert.False(c.IsSuccess)); + await Assert.ThrowsAsync(async () => { + var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); + }); } [Theory] @@ -492,7 +495,7 @@ public async Task ExecuteRule_DynamicParsion_RulesEvaluationFailed(string ruleFi var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); Assert.NotNull(result); - Assert.StartsWith("One or more adjust rules failed", result[1].ExceptionMessage); + Assert.StartsWith("Exception while parsing expression", result[1].ExceptionMessage); } [Theory] diff --git a/test/RulesEngine.UnitTest/ScopedParamsTest.cs b/test/RulesEngine.UnitTest/ScopedParamsTest.cs index 20a288f2..90677730 100644 --- a/test/RulesEngine.UnitTest/ScopedParamsTest.cs +++ b/test/RulesEngine.UnitTest/ScopedParamsTest.cs @@ -100,6 +100,10 @@ public async Task DisabledScopedParam_ShouldReflect(string workflowName, bool[] for (var i = 0; i < result.Count; i++) { Assert.Equal(result[i].IsSuccess, outputs[i]); + if (result[i].IsSuccess == false) + { + Assert.StartsWith("Exception while parsing expression", result[i].ExceptionMessage); + } } } @@ -119,6 +123,7 @@ public async Task ErrorInScopedParam_ShouldAppearAsErrorMessage(string workflowN Assert.All(result, c => { Assert.False(c.IsSuccess); + Assert.StartsWith("Error while compiling rule", c.ExceptionMessage); }); }