diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f9e178..2e43ff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. +## [3.1.0-preview.1] +- Added globalParams feature which can be applied to all rules +- Enabled localParams support for nested Rules +- Made certain fields in Rule model optional allowing users to define workflow with minimal fields +- Added option to disable Rule in workflow json +- Added `GetAllRegisteredWorkflow` to RulesEngine to return all registeredWorkflows +- Fixed Rule compilation exception not returned when Rule has ErrorMessage field defined - #95 + ## [3.0.2] - Fixed LocalParams cache not getting cleaned up when RemoveWorkflow and ClearWorkflows are called diff --git a/README.md b/README.md index 0c446ea..b4d7c92 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Rules Engine -![build](https://github.com/microsoft/RulesEngine/workflows/build/badge.svg?branch=master) -[![Coverage Status](https://coveralls.io/repos/github/microsoft/RulesEngine/badge.svg?branch=master)](https://coveralls.io/github/microsoft/RulesEngine?branch=master) +![build](https://github.com/microsoft/RulesEngine/workflows/build/badge.svg?branch=main) +[![Coverage Status](https://coveralls.io/repos/github/microsoft/RulesEngine/badge.svg?branch=main)](https://coveralls.io/github/microsoft/RulesEngine?branch=main) [![Nuget download][download-image]][download-url] [download-image]: https://img.shields.io/nuget/dt/RulesEngine @@ -13,7 +13,7 @@ To install this library, please download the latest version of [NuGet Package]( ## How to use it -You need to store the rules based on the [schema definition](https://github.com/microsoft/RulesEngine/blob/master/schema/workflowRules-schema.json) given and they can be stored in any store as deemed appropriate like Azure Blob Storage, Cosmos DB, Azure App Configuration, SQL Servers, file systems etc. The expressions are supposed to be a [lambda expressions](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). +You need to store the rules based on the [schema definition](https://github.com/microsoft/RulesEngine/blob/main/schema/workflowRules-schema.json) given and they can be stored in any store as deemed appropriate like Azure Blob Storage, Cosmos DB, Azure App Configuration, SQL Servers, file systems etc. The expressions are supposed to be a [lambda expressions](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions). An example rule could be - ```json @@ -59,13 +59,13 @@ The *response* will contain a list of [*RuleResultTree*](https://github.com/micr _Note: A detailed example showcasing how to use Rules Engine is explained in [Getting Started page](https://github.com/microsoft/RulesEngine/wiki/Getting-Started) of [Rules Engine Wiki](https://github.com/microsoft/RulesEngine/wiki)._ -_A demo app for the is available at [this location](https://github.com/microsoft/RulesEngine/tree/master/demo)._ +_A demo app for the is available at [this location](https://github.com/microsoft/RulesEngine/tree/main/demo)._ ## How it works -![](https://github.com/microsoft/RulesEngine/blob/master/assets/BlockDiagram.png) +![](https://github.com/microsoft/RulesEngine/blob/main/assets/BlockDiagram.png) -The rules can be stored in any store and be fed to the system in a structure which follows a proper [schema](https://github.com/microsoft/RulesEngine/blob/master/schema/workflowRules-schema.json) of WorkFlow model. +The rules can be stored in any store and be fed to the system in a structure which follows a proper [schema](https://github.com/microsoft/RulesEngine/blob/main/schema/workflowRules-schema.json) of WorkFlow model. The wrapper needs to be created over the Rules Engine package, which will get the rules and input message(s) from any store that your system dictates and put it into the Engine. Also, the wrapper then needs to handle the output using appropriate means. diff --git a/benchmark/RulesEngineBenchmark/Program.cs b/benchmark/RulesEngineBenchmark/Program.cs index 683dc46..10ba032 100644 --- a/benchmark/RulesEngineBenchmark/Program.cs +++ b/benchmark/RulesEngineBenchmark/Program.cs @@ -38,7 +38,7 @@ namespace RulesEngineBenchmark rulesEngine = new RulesEngine.RulesEngine(workflows.ToArray(), null, new ReSettings { EnableFormattedErrorMessage = false, - EnableLocalParams = false + EnableScopedParams = false }); ruleInput = new { diff --git a/benchmark/RulesEngineBenchmark/RulesEngineBenchmark.csproj b/benchmark/RulesEngineBenchmark/RulesEngineBenchmark.csproj index 07c60a3..0e40433 100644 --- a/benchmark/RulesEngineBenchmark/RulesEngineBenchmark.csproj +++ b/benchmark/RulesEngineBenchmark/RulesEngineBenchmark.csproj @@ -7,11 +7,11 @@ - + - + diff --git a/src/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs b/src/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs index adb54d9..9b55075 100644 --- a/src/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs +++ b/src/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs @@ -4,12 +4,12 @@ using RulesEngine.HelperFunctions; using RulesEngine.Models; using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; namespace RulesEngine.ExpressionBuilders { - /// - /// This class will build the list expression - /// internal sealed class LambdaExpressionBuilder : RuleExpressionBuilderBase { private readonly ReSettings _reSettings; @@ -26,19 +26,32 @@ namespace RulesEngine.ExpressionBuilders try { var ruleDelegate = _ruleExpressionParser.Compile(rule.Expression, ruleParams); - bool func(object[] paramList) => ruleDelegate(paramList); - return Helpers.ToResultTree(rule, null, func); + return Helpers.ToResultTree(rule, null, ruleDelegate); } catch (Exception ex) { ex.Data.Add(nameof(rule.RuleName), rule.RuleName); ex.Data.Add(nameof(rule.Expression), rule.Expression); - if (!_reSettings.EnableExceptionAsErrorMessage) throw; + if (!_reSettings.EnableExceptionAsErrorMessage) + { + throw; + } + bool func(object[] param) => false; - var exceptionMessage = $"Exception while parsing expression `{rule?.Expression}` - {ex.Message}"; - return Helpers.ToResultTree(rule, null, func, exceptionMessage); + var exceptionMessage = _reSettings.IgnoreException ? "" : $"Exception while parsing expression `{rule?.Expression}` - {ex.Message}"; + return Helpers.ToResultTree(rule, null,func, exceptionMessage); } } + + internal override LambdaExpression Parse(string expression, ParameterExpression[] parameters, Type returnType) + { + return _ruleExpressionParser.Parse(expression, parameters, returnType); + } + + internal override Func> CompileScopedParams(RuleParameter[] ruleParameters, RuleExpressionParameter[] scopedParameters) + { + return _ruleExpressionParser.CompileRuleExpressionParameters(ruleParameters, scopedParameters); + } } } diff --git a/src/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs b/src/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs index 0d03dc5..61c1c5d 100644 --- a/src/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs +++ b/src/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs @@ -2,6 +2,9 @@ // Licensed under the MIT License. using RulesEngine.Models; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; namespace RulesEngine.ExpressionBuilders { @@ -18,5 +21,9 @@ namespace RulesEngine.ExpressionBuilders /// The rule input exp. /// Expression type internal abstract RuleFunc BuildDelegateForRule(Rule rule, RuleParameter[] ruleParams); + + internal abstract LambdaExpression Parse(string expression, ParameterExpression[] parameters, Type returnType); + + internal abstract Func> CompileScopedParams(RuleParameter[] ruleParameters, RuleExpressionParameter[] scopedParameters); } } diff --git a/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs b/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs index 2c7f72b..7abcc14 100644 --- a/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs +++ b/src/RulesEngine/ExpressionBuilders/RuleExpressionParser.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Dynamic.Core; using System.Linq.Expressions; +using System.Reflection; namespace RulesEngine.ExpressionBuilders { @@ -25,32 +26,45 @@ namespace RulesEngine.ExpressionBuilders }); } - public Func Compile(string expression, RuleParameter[] ruleParams) + public LambdaExpression Parse(string expression, ParameterExpression[] parameters, Type returnType) { + var config = new ParsingConfig { CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes) }; + + return DynamicExpressionParser.ParseLambda(config, false, parameters, returnType, expression); + } + + public Func Compile(string expression, RuleParameter[] ruleParams) + { var cacheKey = GetCacheKey(expression, ruleParams, typeof(T)); return _memoryCache.GetOrCreate(cacheKey, (entry) => { entry.SetSize(1); - var config = new ParsingConfig { CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes) }; - var typeParamExpressions = GetParameterExpression(ruleParams).ToArray(); - var e = DynamicExpressionParser.ParseLambda(config, true, typeParamExpressions.ToArray(), typeof(T), expression); - var wrappedExpression = WrapExpression(e, typeParamExpressions); - return wrappedExpression.CompileFast>(); + var parameterExpressions = GetParameterExpression(ruleParams).ToArray(); + + var e = Parse(expression, parameterExpressions, typeof(T)); + var expressionBody = new List() { e.Body }; + var wrappedExpression = WrapExpression(expressionBody, parameterExpressions, new ParameterExpression[] { }); + return wrappedExpression.CompileFast(); }); - } - private Expression> WrapExpression(LambdaExpression expression, ParameterExpression[] parameters) + 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(new List { expression.Body }); - var blockExp = Expression.Block(parameters, blockExpSteps); + 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 expression.CompileFast(); + } public T Evaluate(string expression, RuleParameter[] ruleParams) { @@ -58,6 +72,13 @@ namespace RulesEngine.ExpressionBuilders 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. /// @@ -68,7 +89,7 @@ namespace RulesEngine.ExpressionBuilders /// or /// type /// - private IEnumerable GetParameterExpression(params RuleParameter[] ruleParams) + private IEnumerable GetParameterExpression(RuleParameter[] ruleParams) { foreach (var ruleParam in ruleParams) { @@ -77,10 +98,45 @@ namespace RulesEngine.ExpressionBuilders throw new ArgumentException($"{nameof(ruleParam)} can't be null."); } - yield return Expression.Parameter(ruleParam.Type, ruleParam.Name); + 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 = typeof(Dictionary).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(string), typeof(object) }, null); + + 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()); + } + private string GetCacheKey(string expression, RuleParameter[] ruleParameters, Type returnType) { var paramKey = string.Join("|", ruleParameters.Select(c => c.Type.ToString())); diff --git a/src/RulesEngine/Extensions/ListofRuleResultTreeExtension.cs b/src/RulesEngine/Extensions/ListofRuleResultTreeExtension.cs index 38d65f8..0b3caa9 100644 --- a/src/RulesEngine/Extensions/ListofRuleResultTreeExtension.cs +++ b/src/RulesEngine/Extensions/ListofRuleResultTreeExtension.cs @@ -13,6 +13,13 @@ namespace RulesEngine.Extensions public delegate void OnSuccessFunc(string eventName); public delegate void OnFailureFunc(); + + /// + /// Calls the Success Func for the first rule which succeeded among the the ruleReults + /// + /// + /// + /// public static List OnSuccess(this List ruleResultTrees, OnSuccessFunc onSuccessFunc) { var successfulRuleResult = ruleResultTrees.FirstOrDefault(ruleResult => ruleResult.IsSuccess == true); @@ -25,6 +32,12 @@ namespace RulesEngine.Extensions return ruleResultTrees; } + /// + /// Calls the Failure Func if all rules failed in the ruleReults + /// + /// + /// + /// public static List OnFail(this List ruleResultTrees, OnFailureFunc onFailureFunc) { bool allFailure = ruleResultTrees.All(ruleResult => ruleResult.IsSuccess == false); diff --git a/src/RulesEngine/HelperFunctions/Helpers.cs b/src/RulesEngine/HelperFunctions/Helpers.cs index 3aa0a8e..da687f0 100644 --- a/src/RulesEngine/HelperFunctions/Helpers.cs +++ b/src/RulesEngine/HelperFunctions/Helpers.cs @@ -29,6 +29,7 @@ namespace RulesEngine.HelperFunctions /// /// ruleResultTree /// ruleResultMessage + [Obsolete] internal static void ToResultTreeMessages(RuleResultTree ruleResultTree, ref RuleResultMessage ruleResultMessage) { if (ruleResultTree.ChildResults != null) @@ -40,7 +41,7 @@ namespace RulesEngine.HelperFunctions if (!ruleResultTree.IsSuccess) { string errMsg = ruleResultTree.Rule.ErrorMessage; - errMsg = string.IsNullOrEmpty(errMsg) ? $"Error message does not configured for {ruleResultTree.Rule.RuleName}" : errMsg; + errMsg = string.IsNullOrEmpty(errMsg) ? $"Error message is not configured for {ruleResultTree.Rule.RuleName}" : errMsg; if (ruleResultTree.Rule.ErrorType == ErrorType.Error && !ruleResultMessage.ErrorMessages.Contains(errMsg)) { @@ -59,6 +60,7 @@ namespace RulesEngine.HelperFunctions /// /// childResultTree /// ruleResultMessage + [Obsolete] private static void GetChildRuleMessages(IEnumerable childResultTree, ref RuleResultMessage ruleResultMessage) { foreach (var item in childResultTree) diff --git a/src/RulesEngine/HelperFunctions/Utils.cs b/src/RulesEngine/HelperFunctions/Utils.cs index 431295b..4ae9262 100644 --- a/src/RulesEngine/HelperFunctions/Utils.cs +++ b/src/RulesEngine/HelperFunctions/Utils.cs @@ -73,36 +73,38 @@ namespace RulesEngine.HelperFunctions } object obj = Activator.CreateInstance(type); + var typeProps = type.GetProperties().ToDictionary(c => c.Name); + foreach (var expando in (IDictionary)input) { - if (type.GetProperties().Any(c => c.Name == expando.Key) && + if (typeProps.ContainsKey(expando.Key) && expando.Value != null && (expando.Value.GetType().Name != "DBNull" || expando.Value != DBNull.Value)) { object val; + var propInfo = typeProps[expando.Key]; if (expando.Value is ExpandoObject) { - var propType = type.GetProperty(expando.Key).PropertyType; + var propType = propInfo.PropertyType; val = CreateObject(propType, expando.Value); } else if (expando.Value is IList) { - var internalType = type.GetProperty(expando.Key).PropertyType.GenericTypeArguments.FirstOrDefault() ?? typeof(object); + var internalType = propInfo.PropertyType.GenericTypeArguments.FirstOrDefault() ?? typeof(object); var temp = (IList)expando.Value; - var newList = new List(); + var newList = new List().Cast(internalType).ToList(internalType); for (int i = 0; i < temp.Count; i++) { var child = CreateObject(internalType, temp[i]); newList.Add(child); }; - val = newList.Cast(internalType).ToList(internalType); + val = newList; } else { val = expando.Value; } - type.GetProperty(expando.Key).SetValue(obj, val, null); + propInfo.SetValue(obj, val, null); } - } return obj; diff --git a/src/RulesEngine/Interfaces/IRulesEngine.cs b/src/RulesEngine/Interfaces/IRulesEngine.cs index c8298a0..4767f32 100644 --- a/src/RulesEngine/Interfaces/IRulesEngine.cs +++ b/src/RulesEngine/Interfaces/IRulesEngine.cs @@ -25,8 +25,28 @@ namespace RulesEngine.Interfaces /// List of rule results ValueTask> ExecuteAllRulesAsync(string workflowName, params RuleParameter[] ruleParams); ValueTask ExecuteActionWorkflowAsync(string workflowName, string ruleName, RuleParameter[] ruleParameters); + + /// + /// Adds new workflows to RulesEngine + /// + /// void AddWorkflow(params WorkflowRules[] workflowRules); + + /// + /// Removes all registered workflows from RulesEngine + /// void ClearWorkflows(); + + /// + /// Removes the workflow from RulesEngine + /// + /// void RemoveWorkflow(params string[] workflowNames); + + /// + /// Returns the list of all registered workflow names + /// + /// + List GetAllRegisteredWorkflowNames(); } } diff --git a/src/RulesEngine/Models/CompiledParam.cs b/src/RulesEngine/Models/CompiledParam.cs deleted file mode 100644 index c6e2788..0000000 --- a/src/RulesEngine/Models/CompiledParam.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Diagnostics.CodeAnalysis; - -namespace RulesEngine.Models -{ - /// - /// CompiledParam class. - /// - [ExcludeFromCodeCoverage] - internal class CompiledParam - { - internal string Name { get; set; } - internal Type ReturnType { get; set; } - internal Func Value { get; set; } - internal RuleParameter AsRuleParameter() - { - return new RuleParameter(Name, ReturnType); - } - } -} diff --git a/src/RulesEngine/Models/ReSettings.cs b/src/RulesEngine/Models/ReSettings.cs index c8cbb36..3dd689c 100644 --- a/src/RulesEngine/Models/ReSettings.cs +++ b/src/RulesEngine/Models/ReSettings.cs @@ -11,10 +11,46 @@ namespace RulesEngine.Models [ExcludeFromCodeCoverage] public class ReSettings { + /// + /// Get/Set the custom types to be used in Rule expressions + /// public Type[] CustomTypes { get; set; } + + /// + /// Get/Set the custom actions that can be used in the Rules + /// public Dictionary> CustomActions { get; set; } + + /// + /// When set to true, returns any exception occurred + /// while rule execution as ErrorMessage + /// otherwise throws an exception + /// + /// This setting is only applicable if IgnoreException is set to false public bool EnableExceptionAsErrorMessage { get; set; } = true; + + /// + /// When set to true, it will ignore any exception thrown with rule compilation/execution + /// + public bool IgnoreException { get; set; } = false; + + /// + /// Enables ErrorMessage Formatting + /// public bool EnableFormattedErrorMessage { get; set; } = true; - public bool EnableLocalParams { get; set; } = true; + + /// + /// Enables Global params and local params for rules + /// + public bool EnableScopedParams { get; set; } = true; + + /// + /// Enables Local params for rules + /// + [Obsolete("Use 'EnableScopedParams' instead. This will be removed in next major version")] + public bool EnableLocalParams { + get { return EnableScopedParams; } + set { EnableScopedParams = value; } + } } } diff --git a/src/RulesEngine/Models/Rule.cs b/src/RulesEngine/Models/Rule.cs index 6ee2e26..6fa9e77 100644 --- a/src/RulesEngine/Models/Rule.cs +++ b/src/RulesEngine/Models/Rule.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using RulesEngine.Enums; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -15,6 +16,9 @@ namespace RulesEngine.Models [ExcludeFromCodeCoverage] public class Rule { + /// + /// Rule name for the Rule + /// public string RuleName { get; set; } /// /// Gets or sets the custom property or tags of the rule. @@ -26,20 +30,21 @@ namespace RulesEngine.Models public string Operator { get; set; } public string ErrorMessage { get; set; } + /// + /// Gets or sets whether the rule is enabled. + /// + public bool Enabled { get; set; } = true; + + [Obsolete("will be removed in next major version")] [JsonConverter(typeof(StringEnumConverter))] - public ErrorType ErrorType { get; set; } + public ErrorType ErrorType { get; set; } = ErrorType.Warning; [JsonConverter(typeof(StringEnumConverter))] - public RuleExpressionType? RuleExpressionType { get; set; } - + public RuleExpressionType RuleExpressionType { get; set; } = RuleExpressionType.LambdaExpression; public List WorkflowRulesToInject { get; set; } - public List Rules { get; set; } - - [JsonProperty] - public IEnumerable LocalParams { get; set; } + public IEnumerable LocalParams { get; set; } public string Expression { get; set; } - public Dictionary Actions { get; set; } public string SuccessEvent { get; set; } diff --git a/src/RulesEngine/Models/RuleExpressionParameter.cs b/src/RulesEngine/Models/RuleExpressionParameter.cs new file mode 100644 index 0000000..ad20e61 --- /dev/null +++ b/src/RulesEngine/Models/RuleExpressionParameter.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; + +namespace RulesEngine.Models +{ + /// + /// CompiledParam class. + /// + [ExcludeFromCodeCoverage] + public class RuleExpressionParameter + { + public ParameterExpression ParameterExpression { get; set; } + + public Expression ValueExpression { get; set; } + + } +} diff --git a/src/RulesEngine/Models/RuleParameter.cs b/src/RulesEngine/Models/RuleParameter.cs index b509731..5aa54bb 100644 --- a/src/RulesEngine/Models/RuleParameter.cs +++ b/src/RulesEngine/Models/RuleParameter.cs @@ -4,6 +4,7 @@ using RulesEngine.HelperFunctions; using System; using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; namespace RulesEngine.Models { @@ -13,18 +14,24 @@ namespace RulesEngine.Models public RuleParameter(string name, object value) { Value = Utils.GetTypedObject(value); - Type = Value.GetType(); - Name = name; + Init(name, Value.GetType()); } internal RuleParameter(string name, Type type) + { + Init(name, type); + } + public Type Type { get; private set; } + public string Name { get; private set; } + public object Value { get; private set; } + public ParameterExpression ParameterExpression { get; private set; } + + private void Init(string name, Type type) { Name = name; Type = type; + ParameterExpression = Expression.Parameter(Type, Name); } - public Type Type { get; } - public string Name { get; } - public object Value { get; } } } diff --git a/src/RulesEngine/Models/RuleResultTree.cs b/src/RulesEngine/Models/RuleResultTree.cs index 5282503..1968915 100644 --- a/src/RulesEngine/Models/RuleResultTree.cs +++ b/src/RulesEngine/Models/RuleResultTree.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using RulesEngine.HelperFunctions; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -55,6 +56,7 @@ namespace RulesEngine.Models /// /// The rule evaluated parameters. /// + [Obsolete("Use `Inputs` field to get details of all input, localParams and globalParams")] public IEnumerable RuleEvaluatedParams { get; set; } /// @@ -62,9 +64,10 @@ namespace RulesEngine.Models /// /// RuleResultMessage [ExcludeFromCodeCoverage] + [Obsolete("will be removed in next major version")] public RuleResultMessage GetMessages() { - RuleResultMessage ruleResultMessage = new RuleResultMessage(); + var ruleResultMessage = new RuleResultMessage(); Helpers.ToResultTreeMessages(this, ref ruleResultMessage); diff --git a/src/RulesEngine/Models/LocalParam.cs b/src/RulesEngine/Models/ScopedParam.cs similarity index 65% rename from src/RulesEngine/Models/LocalParam.cs rename to src/RulesEngine/Models/ScopedParam.cs index 6792b4a..2ccd61b 100644 --- a/src/RulesEngine/Models/LocalParam.cs +++ b/src/RulesEngine/Models/ScopedParam.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Newtonsoft.Json; using System.Diagnostics.CodeAnalysis; namespace RulesEngine.Models @@ -9,22 +8,23 @@ namespace RulesEngine.Models /// Class LocalParam. /// [ExcludeFromCodeCoverage] - public class LocalParam + public class ScopedParam { /// - /// Gets or sets the name of the rule. + /// Gets or sets the name of the param. /// /// /// The name of the rule. - /// - [JsonProperty, JsonRequired] + /// ] public string Name { get; set; } /// - /// Gets or Sets the lambda expression. + /// Gets or Sets the lambda expression which can be reference in Rule. /// - [JsonProperty, JsonRequired] public string Expression { get; set; } } + + [ExcludeFromCodeCoverage] + public class LocalParam : ScopedParam { } } diff --git a/src/RulesEngine/Models/WorkflowRules.cs b/src/RulesEngine/Models/WorkflowRules.cs index 0364719..baf07f4 100644 --- a/src/RulesEngine/Models/WorkflowRules.cs +++ b/src/RulesEngine/Models/WorkflowRules.cs @@ -21,6 +21,11 @@ namespace RulesEngine.Models /// The workflow rules to inject. public IEnumerable WorkflowRulesToInject { get; set; } + /// + /// Gets or Sets the global params which will be applicable to all rules + /// + public IEnumerable GlobalParams { get; set; } + /// /// list of rules. /// diff --git a/src/RulesEngine/ParamCompiler.cs b/src/RulesEngine/ParamCompiler.cs deleted file mode 100644 index 56587de..0000000 --- a/src/RulesEngine/ParamCompiler.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using RulesEngine.ExpressionBuilders; -using RulesEngine.Models; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace RulesEngine -{ - /// - /// Rule param compilers - /// - internal class ParamCompiler - { - - private readonly ReSettings _reSettings; - private readonly RuleExpressionParser _ruleExpressionParser; - - internal ParamCompiler(ReSettings reSettings, RuleExpressionParser ruleExpressionParser) - { - _reSettings = reSettings; - _ruleExpressionParser = ruleExpressionParser; - } - - /// - /// Compiles the and evaluate parameter expression. - /// - /// The rule. - /// The rule parameters. - /// - /// IEnumerable<RuleParameter>. - /// - public IEnumerable CompileParamsExpression(Rule rule, IEnumerable ruleParams) - { - - if (rule.LocalParams == null) return null; - - var compiledParameters = new List(); - var evaluatedParameters = new List(); - foreach (var param in rule.LocalParams) - { - var compiledParamDelegate = GetDelegateForRuleParam(param, ruleParams.ToArray()); - var evaluatedParam = EvaluateCompiledParam(param.Name, compiledParamDelegate, ruleParams); - compiledParameters.Add(new CompiledParam { Name = param.Name, Value = compiledParamDelegate, ReturnType = evaluatedParam.Type }); - ruleParams = ruleParams.Append(evaluatedParam); - evaluatedParameters.Add(evaluatedParam); - } - - return compiledParameters; - } - - /// Evaluates the compiled parameter. - /// Name of the parameter. - /// The compiled parameter. - /// The rule parameters. - /// RuleParameter. - public RuleParameter EvaluateCompiledParam(string paramName, Func compiledParam, IEnumerable inputs) - { - var result = compiledParam(inputs.Select(c => c.Value).ToArray()); - return new RuleParameter(paramName, result); - } - - - /// - /// Gets the expression for rule. - /// - /// The rule. - /// The type parameter expressions. - /// The rule input exp. - /// - private Func GetDelegateForRuleParam(LocalParam param, RuleParameter[] ruleParameters) - { - return _ruleExpressionParser.Compile(param.Expression, ruleParameters); - } - } -} diff --git a/src/RulesEngine/RuleCompiler.cs b/src/RulesEngine/RuleCompiler.cs index 39f620a..a22dc5e 100644 --- a/src/RulesEngine/RuleCompiler.cs +++ b/src/RulesEngine/RuleCompiler.cs @@ -2,12 +2,14 @@ // Licensed under the MIT License. using Microsoft.Extensions.Logging; +using RulesEngine.ExpressionBuilders; using RulesEngine.HelperFunctions; using RulesEngine.Models; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Runtime.InteropServices; namespace RulesEngine { @@ -25,6 +27,7 @@ namespace RulesEngine /// The expression builder factory /// private readonly RuleExpressionBuilderFactory _expressionBuilderFactory; + private readonly ReSettings _reSettings; /// /// The logger @@ -36,10 +39,12 @@ namespace RulesEngine /// /// The expression builder factory. /// expressionBuilderFactory - internal RuleCompiler(RuleExpressionBuilderFactory expressionBuilderFactory, ILogger logger) + internal RuleCompiler(RuleExpressionBuilderFactory expressionBuilderFactory, ReSettings reSettings, ILogger logger) { _logger = logger ?? throw new ArgumentNullException($"{nameof(logger)} can't be null."); + _expressionBuilderFactory = expressionBuilderFactory ?? throw new ArgumentNullException($"{nameof(expressionBuilderFactory)} can't be null."); + _reSettings = reSettings; } /// @@ -50,7 +55,7 @@ namespace RulesEngine /// /// /// Compiled func delegate - internal RuleFunc CompileRule(Rule rule, params RuleParameter[] ruleParams) + internal RuleFunc CompileRule(Rule rule, RuleParameter[] ruleParams, ScopedParam[] globalParams) { try { @@ -58,8 +63,11 @@ namespace RulesEngine { throw new ArgumentNullException(nameof(rule)); } - var ruleExpression = GetDelegateForRule(rule, ruleParams); - return ruleExpression; + var globalParamExp = GetRuleExpressionParameters(rule.RuleExpressionType,globalParams, ruleParams); + var extendedRuleParams = ruleParams.Concat(globalParamExp.Select(c => new RuleParameter(c.ParameterExpression.Name,c.ParameterExpression.Type))) + .ToArray(); + var ruleExpression = GetDelegateForRule(rule, extendedRuleParams); + return GetWrappedRuleFunc(RuleExpressionType.LambdaExpression,ruleExpression,ruleParams,globalParamExp); } catch (Exception ex) { @@ -79,15 +87,55 @@ namespace RulesEngine /// private RuleFunc GetDelegateForRule(Rule rule, RuleParameter[] ruleParams) { + var scopedParamList = GetRuleExpressionParameters(rule.RuleExpressionType, rule?.LocalParams, ruleParams); + + var extendedRuleParams = ruleParams.Concat(scopedParamList.Select(c => new RuleParameter(c.ParameterExpression.Name, c.ParameterExpression.Type))) + .ToArray(); + + RuleFunc ruleFn; + if (Enum.TryParse(rule.Operator, out ExpressionType nestedOperator) && nestedOperators.Contains(nestedOperator) && rule.Rules != null && rule.Rules.Any()) { - return BuildNestedRuleFunc(rule, nestedOperator, ruleParams); + ruleFn = BuildNestedRuleFunc(rule, nestedOperator, extendedRuleParams); } else { - return BuildRuleFunc(rule, ruleParams); + ruleFn = BuildRuleFunc(rule, extendedRuleParams); } + + return GetWrappedRuleFunc(rule.RuleExpressionType, ruleFn, ruleParams, scopedParamList); + } + + private RuleExpressionParameter[] GetRuleExpressionParameters(RuleExpressionType ruleExpressionType,IEnumerable localParams, RuleParameter[] ruleParams) + { + if(!_reSettings.EnableScopedParams) + { + return new RuleExpressionParameter[] { }; + } + var ruleExpParams = new List(); + + if (localParams?.Any() == true) + { + + var parameters = ruleParams.Select(c => c.ParameterExpression) + .ToList(); + + var expressionBuilder = GetExpressionBuilder(ruleExpressionType); + + foreach (var lp in localParams) + { + var lpExpression = expressionBuilder.Parse(lp.Expression, parameters.ToArray(), null).Body; + var ruleExpParam = new RuleExpressionParameter() { + ParameterExpression = Expression.Parameter(lpExpression.Type, lp.Name), + ValueExpression = lpExpression + }; + parameters.Add(ruleExpParam.ParameterExpression); + ruleExpParams.Add(ruleExpParam); + } + } + return ruleExpParams.ToArray(); + } /// @@ -100,12 +148,7 @@ namespace RulesEngine /// private RuleFunc BuildRuleFunc(Rule rule, RuleParameter[] ruleParams) { - if (!rule.RuleExpressionType.HasValue) - { - throw new InvalidOperationException($"RuleExpressionType can not be null for leaf level expressions."); - } - - var ruleExpressionBuilder = _expressionBuilderFactory.RuleGetExpressionBuilder(rule.RuleExpressionType.Value); + var ruleExpressionBuilder = GetExpressionBuilder(rule.RuleExpressionType); var ruleFunc = ruleExpressionBuilder.BuildDelegateForRule(rule, ruleParams); @@ -125,13 +168,13 @@ namespace RulesEngine private RuleFunc BuildNestedRuleFunc(Rule parentRule, ExpressionType operation, RuleParameter[] ruleParams) { var ruleFuncList = new List>(); - foreach (var r in parentRule.Rules) + foreach (var r in parentRule.Rules.Where(c => c.Enabled)) { ruleFuncList.Add(GetDelegateForRule(r, ruleParams)); } return (paramArray) => { - var resultList = ruleFuncList.Select(fn => fn(paramArray)); + var resultList = ruleFuncList.Select(fn => fn(paramArray)).ToList(); Func isSuccess = (p) => ApplyOperation(resultList, operation); var result = Helpers.ToResultTree(parentRule, resultList, isSuccess); return result(paramArray); @@ -141,6 +184,11 @@ namespace RulesEngine private bool ApplyOperation(IEnumerable ruleResults, ExpressionType operation) { + if (ruleResults?.Any() != true) + { + return false; + } + switch (operation) { case ExpressionType.And: @@ -154,5 +202,36 @@ namespace RulesEngine return false; } } + + private RuleFunc GetWrappedRuleFunc(RuleExpressionType ruleExpressionType, RuleFunc ruleFunc,RuleParameter[] ruleParameters,RuleExpressionParameter[] ruleExpParams) + { + if(ruleExpParams.Length == 0) + { + return ruleFunc; + } + var paramDelegate = GetExpressionBuilder(ruleExpressionType).CompileScopedParams(ruleParameters, ruleExpParams); + + return (ruleParams) => { + var inputs = ruleParams.Select(c => c.Value).ToArray(); + var scopedParamsDict = paramDelegate(inputs); + var scopedParams = scopedParamsDict.Select(c => new RuleParameter(c.Key, c.Value)).ToList(); + var extendedInputs = ruleParams.Concat(scopedParams); + var result = ruleFunc(extendedInputs.ToArray()); + // To be removed in next major release +#pragma warning disable CS0618 // Type or member is obsolete + if(result.RuleEvaluatedParams == null) + { + result.RuleEvaluatedParams = scopedParams; + } +#pragma warning restore CS0618 // Type or member is obsolete + + return result; + }; + } + + private RuleExpressionBuilderBase GetExpressionBuilder(RuleExpressionType expressionType) + { + return _expressionBuilderFactory.RuleGetExpressionBuilder(expressionType); + } } } diff --git a/src/RulesEngine/RulesCache.cs b/src/RulesEngine/RulesCache.cs index 713ce1c..85fa7be 100644 --- a/src/RulesEngine/RulesCache.cs +++ b/src/RulesEngine/RulesCache.cs @@ -27,6 +27,11 @@ namespace RulesEngine return _workflowRules.ContainsKey(workflowName); } + public List GetAllWorkflowNames() + { + return _workflowRules.Keys.ToList(); + } + /// Determines whether [contains compiled rules] [the specified workflow name]. /// Name of the workflow. /// @@ -59,16 +64,6 @@ namespace RulesEngine _compileRules.Clear(); } - /// Gets the rules. - /// Name of the workflow. - /// IEnumerable<Rule>. - public IEnumerable GetRules(string workflowName) - { - if (!ContainsWorkflowRules(workflowName)) - throw new ArgumentException($"workflow `{workflowName}` was not found"); - return _workflowRules[workflowName].Rules; - } - /// Gets the work flow rules. /// Name of the workflow. /// WorkflowRules. diff --git a/src/RulesEngine/RulesEngine.cs b/src/RulesEngine/RulesEngine.cs index 33f9099..597860b 100644 --- a/src/RulesEngine/RulesEngine.cs +++ b/src/RulesEngine/RulesEngine.cs @@ -32,8 +32,6 @@ namespace RulesEngine private readonly ILogger _logger; private readonly ReSettings _reSettings; private readonly RulesCache _rulesCache = new RulesCache(); - private MemoryCache _compiledParamsCache = new MemoryCache(new MemoryCacheOptions()); - private readonly ParamCompiler _ruleParamCompiler; private readonly RuleExpressionParser _ruleExpressionParser; private readonly RuleCompiler _ruleCompiler; private readonly ActionFactory _actionFactory; @@ -57,8 +55,7 @@ namespace RulesEngine _logger = logger ?? new NullLogger(); _reSettings = reSettings ?? new ReSettings(); _ruleExpressionParser = new RuleExpressionParser(_reSettings); - _ruleParamCompiler = new ParamCompiler(_reSettings, _ruleExpressionParser); - _ruleCompiler = new RuleCompiler(new RuleExpressionBuilderFactory(_reSettings, _ruleExpressionParser), _logger); + _ruleCompiler = new RuleCompiler(new RuleExpressionBuilderFactory(_reSettings, _ruleExpressionParser),_reSettings, _logger); _actionFactory = new ActionFactory(GetActionRegistry(_reSettings)); } @@ -173,13 +170,17 @@ namespace RulesEngine } } + public List GetAllRegisteredWorkflowNames() + { + return _rulesCache.GetAllWorkflowNames(); + } + /// /// Clears the workflows. /// public void ClearWorkflows() { _rulesCache.Clear(); - ClearCompiledParamCache(); } /// @@ -192,7 +193,6 @@ namespace RulesEngine { _rulesCache.Remove(workflowName); } - ClearCompiledParamCache(); } /// @@ -239,9 +239,9 @@ namespace RulesEngine if (workflowRules != null) { var dictFunc = new Dictionary>(); - foreach (var rule in workflowRules.Rules) + foreach (var rule in workflowRules.Rules.Where(c => c.Enabled)) { - dictFunc.Add(rule.RuleName, CompileRule(workflowName, ruleParams, rule)); + dictFunc.Add(rule.RuleName, CompileRule(rule, ruleParams, workflowRules.GlobalParams?.ToArray())); } _rulesCache.AddOrUpdateCompiledRule(compileRulesKey, dictFunc); @@ -257,42 +257,22 @@ namespace RulesEngine private RuleFunc CompileRule(string workflowName, string ruleName, RuleParameter[] ruleParameters) { - var rules = _rulesCache.GetRules(workflowName); - var currentRule = rules?.SingleOrDefault(c => c.RuleName == ruleName); + var workflow = _rulesCache.GetWorkFlowRules(workflowName); + if(workflow == null) + { + throw new ArgumentException($"Workflow `{workflowName}` is not found"); + } + var currentRule = workflow.Rules?.SingleOrDefault(c => c.RuleName == ruleName && c.Enabled); if (currentRule == null) { throw new ArgumentException($"Workflow `{workflowName}` does not contain any rule named `{ruleName}`"); } - return CompileRule(workflowName, ruleParameters, currentRule); + return CompileRule(currentRule, ruleParameters, workflow.GlobalParams?.ToArray()); } - private RuleFunc CompileRule(string workflowName, RuleParameter[] ruleParams, Rule rule) + private RuleFunc CompileRule(Rule rule, RuleParameter[] ruleParams, ScopedParam[] scopedParams) { - if (!_reSettings.EnableLocalParams) - { - return _ruleCompiler.CompileRule(rule, ruleParams); - } - var compiledParamsKey = GetCompiledParamsCacheKey(workflowName, rule.RuleName, ruleParams); - var compiledParamList = _compiledParamsCache.GetOrCreate(compiledParamsKey, (entry) => _ruleParamCompiler.CompileParamsExpression(rule, ruleParams)); - var compiledRuleParameters = compiledParamList?.Select(c => c.AsRuleParameter()) ?? new List(); - var updatedRuleParams = ruleParams?.Concat(compiledRuleParameters); - var compiledRule = _ruleCompiler.CompileRule(rule, updatedRuleParams?.ToArray()); - - RuleFunc updatedRule = (RuleParameter[] paramList) => { - var inputs = paramList.AsEnumerable(); - var localParams = compiledParamList ?? new List(); - var evaluatedParamList = new List(); - foreach (var localParam in localParams) - { - var evaluatedLocalParam = _ruleParamCompiler.EvaluateCompiledParam(localParam.Name, localParam.Value, inputs); - inputs = inputs.Append(evaluatedLocalParam); - evaluatedParamList.Add(evaluatedLocalParam); - } - var result = compiledRule(inputs.ToArray()); - result.RuleEvaluatedParams = evaluatedParamList; - return result; - }; - return updatedRule; + return _ruleCompiler.CompileRule(rule, ruleParams, scopedParams); } @@ -325,12 +305,6 @@ namespace RulesEngine return key; } - private string GetCompiledParamsCacheKey(string workflowName, string ruleName, RuleParameter[] ruleParams) - { - var key = $"compiledparams-{workflowName}-{ruleName}" + string.Join("-", ruleParams.Select(c => c.Type.Name)); - return key; - } - private IDictionary> GetDefaultActionRegistry() { return new Dictionary>{ @@ -351,7 +325,7 @@ namespace RulesEngine foreach (var ruleResult in ruleResultList?.Where(r => !r.IsSuccess)) { var errorMessage = ruleResult?.Rule?.ErrorMessage; - if (errorMessage != null) + if (string.IsNullOrWhiteSpace(ruleResult.ExceptionMessage) && errorMessage != null) { var errorParameters = Regex.Matches(errorMessage, ParamParseRegex); @@ -406,16 +380,6 @@ namespace RulesEngine return errorMessage; } - - /// - /// Clears all compiledParams - /// - private void ClearCompiledParamCache() - { - _compiledParamsCache.Dispose(); - _compiledParamsCache = new MemoryCache(new MemoryCacheOptions()); - } - #endregion } } diff --git a/src/RulesEngine/RulesEngine.csproj b/src/RulesEngine/RulesEngine.csproj index a90d428..27519a5 100644 --- a/src/RulesEngine/RulesEngine.csproj +++ b/src/RulesEngine/RulesEngine.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 3.0.2 + 3.1.0-preview.1 Copyright (c) Microsoft Corporation. LICENSE https://github.com/microsoft/RulesEngine @@ -27,12 +27,12 @@ - + - + diff --git a/src/RulesEngine/Validators/RuleValidator.cs b/src/RulesEngine/Validators/RuleValidator.cs index 23f5a09..cc76d7f 100644 --- a/src/RulesEngine/Validators/RuleValidator.cs +++ b/src/RulesEngine/Validators/RuleValidator.cs @@ -19,7 +19,7 @@ namespace RulesEngine.Validators RuleFor(c => c.RuleName).NotEmpty().WithMessage(Constants.RULE_NAME_NULL_ERRMSG); //Nested expression check - When(c => c.RuleExpressionType == null, () => { + When(c => c.Operator != null, () => { RuleFor(c => c.Operator) .NotNull().WithMessage(Constants.OPERATOR_NULL_ERRMSG) .Must(op => _nestedOperators.Any(x => x.ToString().Equals(op, StringComparison.OrdinalIgnoreCase))) @@ -37,9 +37,8 @@ namespace RulesEngine.Validators private void RegisterExpressionTypeRules() { - When(c => c.RuleExpressionType == RuleExpressionType.LambdaExpression, () => { + When(c => c.Operator == null && c.RuleExpressionType == RuleExpressionType.LambdaExpression, () => { RuleFor(c => c.Expression).NotEmpty().WithMessage(Constants.LAMBDA_EXPRESSION_EXPRESSION_NULL_ERRMSG); - RuleFor(c => c.Operator).Null().WithMessage(Constants.LAMBDA_EXPRESSION_OPERATOR_ERRMSG); RuleFor(c => c.Rules).Null().WithMessage(Constants.LAMBDA_EXPRESSION_RULES_ERRMSG); }); } diff --git a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs index 165850c..f635efd 100644 --- a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs +++ b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs @@ -62,6 +62,29 @@ namespace RulesEngine.UnitTest Assert.Contains(result, c => c.IsSuccess); } + + [Theory] + [InlineData("rules2.json")] + public void GetAllRegisteredWorkflows_ReturnsListOfAllWorkflows(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + var workflows = re.GetAllRegisteredWorkflowNames(); + + Assert.NotNull(workflows); + Assert.Equal(2, workflows.Count); + Assert.Contains("inputWorkflow", workflows); + } + + [Fact] + public void GetAllRegisteredWorkflows_NoWorkflow_ReturnsEmptyList() + { + var re = new RulesEngine(); + var workflows = re.GetAllRegisteredWorkflowNames(); + + Assert.NotNull(workflows); + Assert.Empty(workflows); + } + [Theory] [InlineData("rules2.json")] public async Task ExecuteRule_ManyInputs_ReturnsListOfRuleResultTree(string ruleFileName) @@ -160,6 +183,7 @@ namespace RulesEngine.UnitTest [Theory] [InlineData("rules2.json")] + [Obsolete] public async Task ExecuteRule_ReturnsListOfRuleResultTree_ResultMessage(string ruleFileName) { var re = GetRulesEngine(ruleFileName); @@ -400,20 +424,36 @@ namespace RulesEngine.UnitTest [Theory] [InlineData("rules9.json")] - public async Task ExecuteRule_MissingMethodInExpression_DefaultParameter(string ruleFileName) + public async Task ExecuteRule_CompilationException_ReturnsAsErrorMessage(string ruleFileName) { - var re = GetRulesEngine(ruleFileName); + var re = GetRulesEngine(ruleFileName, new ReSettings() { EnableExceptionAsErrorMessage = true }); dynamic input1 = new ExpandoObject(); input1.Data = new { TestProperty = "" }; input1.Boolean = false; var utils = new TestInstanceUtils(); - var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); + Assert.NotNull(result); - Assert.IsType>(result); - Assert.All(result, c => Assert.False(c.IsSuccess)); + Assert.StartsWith("Exception while parsing expression", result[1].ExceptionMessage); + } + + [Theory] + [InlineData("rules9.json")] + public async Task ExecuteRuleWithIgnoreException_CompilationException_DoesNotReturnsAsErrorMessage(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName, new ReSettings() { EnableExceptionAsErrorMessage = true , IgnoreException = true}); + + dynamic input1 = new ExpandoObject(); + input1.Data = new { TestProperty = "" }; + input1.Boolean = false; + + var utils = new TestInstanceUtils(); + var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); + + Assert.NotNull(result); + Assert.False(result[1].ExceptionMessage.StartsWith("Exception while parsing expression")); } [Fact] @@ -424,7 +464,7 @@ namespace RulesEngine.UnitTest Rules = new Rule[]{ new Rule { RuleName = "RuleWithLocalParam", - LocalParams = new LocalParam[] { + LocalParams = new List { new LocalParam { Name = "lp1", Expression = "true" diff --git a/test/RulesEngine.UnitTest/RuleCompilerTest.cs b/test/RulesEngine.UnitTest/RuleCompilerTest.cs index a77e8f4..5164b4a 100644 --- a/test/RulesEngine.UnitTest/RuleCompilerTest.cs +++ b/test/RulesEngine.UnitTest/RuleCompilerTest.cs @@ -17,10 +17,10 @@ namespace RulesEngine.UnitTest [Fact] public void RuleCompiler_NullCheck() { - Assert.Throws(() => new RuleCompiler(null, null)); + Assert.Throws(() => new RuleCompiler(null, null,null)); var reSettings = new ReSettings(); var parser = new RuleExpressionParser(reSettings); - Assert.Throws(() => new RuleCompiler(new RuleExpressionBuilderFactory(reSettings, parser), null)); + Assert.Throws(() => new RuleCompiler(new RuleExpressionBuilderFactory(reSettings, parser), null,null)); } [Fact] @@ -28,9 +28,9 @@ namespace RulesEngine.UnitTest { var reSettings = new ReSettings(); var parser = new RuleExpressionParser(reSettings); - var compiler = new RuleCompiler(new RuleExpressionBuilderFactory(reSettings, parser), new NullLogger()); - Assert.Throws(() => compiler.CompileRule(null, null)); - Assert.Throws(() => compiler.CompileRule(null, new RuleParameter[] { null })); + var compiler = new RuleCompiler(new RuleExpressionBuilderFactory(reSettings, parser),null, new NullLogger()); + Assert.Throws(() => compiler.CompileRule(null, null,null)); + Assert.Throws(() => compiler.CompileRule(null, new RuleParameter[] { null },null)); } diff --git a/test/RulesEngine.UnitTest/RulesEnabledTests.cs b/test/RulesEngine.UnitTest/RulesEnabledTests.cs new file mode 100644 index 0000000..1b865ea --- /dev/null +++ b/test/RulesEngine.UnitTest/RulesEnabledTests.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using RulesEngine.Models; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace RulesEngine.UnitTest +{ + [ExcludeFromCodeCoverage] + public class RulesEnabledTests + { + public RulesEnabledTests() + { + + } + + [Theory] + [InlineData("RuleEnabledFeatureTest", new bool[] { true, true })] + [InlineData("RuleEnabledNestedFeatureTest", new bool[] { true, true, false })] + public async Task RulesEngine_ShouldOnlyExecuteEnabledRules(string workflowName, bool[] expectedRuleResults) + { + var workflows = GetWorkflows(); + var rulesEngine = new RulesEngine(workflows); + var input1 = new { + TrueValue = true + }; + var result = await rulesEngine.ExecuteAllRulesAsync(workflowName, input1); + Assert.NotNull(result); + Assert.True(NestedEnabledCheck(result)); + + Assert.Equal(expectedRuleResults.Length, result.Count); + for (var i = 0; i < expectedRuleResults.Length; i++) + { + Assert.Equal(expectedRuleResults[i], result[i].IsSuccess); + } + } + + + [Theory] + [InlineData("RuleEnabledFeatureTest", new bool[] { true, true })] + [InlineData("RuleEnabledNestedFeatureTest", new bool[] { true, true, false })] + public async Task WorkflowUpdatedRuleEnabled_ShouldReflect(string workflowName, bool[] expectedRuleResults) + { + var workflow = GetWorkflows().Single(c => c.WorkflowName == workflowName); + var rulesEngine = new RulesEngine(); + rulesEngine.AddWorkflow(workflow); + var input1 = new { + TrueValue = true + }; + var result = await rulesEngine.ExecuteAllRulesAsync(workflowName, input1); + Assert.NotNull(result); + Assert.True(NestedEnabledCheck(result)); + + Assert.Equal(expectedRuleResults.Length, result.Count); + for (var i = 0; i < expectedRuleResults.Length; i++) + { + Assert.Equal(expectedRuleResults[i], result[i].IsSuccess); + } + + rulesEngine.RemoveWorkflow(workflowName); + + var firstRule = workflow.Rules.First(); + + firstRule.Enabled = false; + rulesEngine.AddWorkflow(workflow); + + var expectedLength = workflow.Rules.Count(c => c.Enabled); + + var result2 = await rulesEngine.ExecuteAllRulesAsync(workflowName, input1); + Assert.Equal(expectedLength, result2.Count); + + Assert.DoesNotContain(result2, c => c.Rule.RuleName == firstRule.RuleName); + } + + private bool NestedEnabledCheck(IEnumerable ruleResults) + { + var areAllRulesEnabled = ruleResults.All(c => c.Rule.Enabled); + if (areAllRulesEnabled) + { + foreach (var ruleResult in ruleResults) + { + if (ruleResult.ChildResults?.Any() == true) + { + var areAllChildRulesEnabled = NestedEnabledCheck(ruleResult.ChildResults); + if (areAllChildRulesEnabled == false) + { + return false; + } + } + } + } + return areAllRulesEnabled; + } + + private WorkflowRules[] GetWorkflows() + { + return new[] { + new WorkflowRules { + WorkflowName = "RuleEnabledFeatureTest", + Rules = new List { + new Rule { + RuleName = "RuleWithoutEnabledFieldMentioned", + Expression = "input1.TrueValue == true" + }, + new Rule { + RuleName = "RuleWithEnabledSetToTrue", + Expression = "input1.TrueValue == true", + Enabled = true + }, + new Rule { + RuleName = "RuleWithEnabledSetToFalse", + Expression = "input1.TrueValue == true", + Enabled = false + } + + } + }, + new WorkflowRules { + WorkflowName = "RuleEnabledNestedFeatureTest", + Rules = new List { + new Rule { + RuleName = "RuleWithoutEnabledFieldMentioned", + Operator = "And", + Rules = new List { + new Rule { + RuleName = "RuleWithoutEnabledField", + Expression = "input1.TrueValue" + } + } + }, + new Rule { + RuleName = "RuleWithOneChildSetToFalse", + Expression = "input1.TrueValue == true", + Operator = "And", + Rules = new List{ + new Rule { + RuleName = "RuleWithEnabledFalse", + Expression = "input1.TrueValue", + Enabled = false, + }, + new Rule { + RuleName = "RuleWithEnabledTrue", + Expression = "input1.TrueValue", + Enabled = true + } + } + + }, + new Rule { + RuleName = "RuleWithParentSetToFalse", + Operator = "And", + Enabled = false, + Rules = new List{ + new Rule { + RuleName = "RuleWithEnabledTrue", + Expression = "input1.TrueValue", + Enabled = true + } + } + }, + new Rule { + RuleName = "RuleWithAllChildSetToFalse", + Operator = "And", + Enabled = true, + Rules = new List{ + new Rule { + RuleName = "ChildRuleWithEnabledFalse", + Expression = "input1.TrueValue", + Enabled = false + } + } + } + + } + } + }; + } + + } +} diff --git a/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj b/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj index bb4627f..568b46f 100644 --- a/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj +++ b/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj @@ -3,15 +3,18 @@ netcoreapp3.1 - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/test/RulesEngine.UnitTest/ScopedParamsTest.cs b/test/RulesEngine.UnitTest/ScopedParamsTest.cs new file mode 100644 index 0000000..1ffbe7d --- /dev/null +++ b/test/RulesEngine.UnitTest/ScopedParamsTest.cs @@ -0,0 +1,308 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine.Models; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace RulesEngine.UnitTest +{ + [ExcludeFromCodeCoverage] + public class ScopedParamsTest + { + [Theory] + [InlineData("NoLocalAndGlobalParams")] + [InlineData("LocalParamsOnly")] + [InlineData("GlobalParamsOnly")] + [InlineData("GlobalAndLocalParams")] + [InlineData("GlobalParamReferencedInLocalParams")] + [InlineData("GlobalParamReferencedInNextGlobalParams")] + [InlineData("LocalParamReferencedInNextLocalParams")] + [InlineData("GlobalParamAndLocalParamsInNestedRules")] + public async Task BasicWorkflowRules_ReturnsTrue(string workflowName) + { + var workflows = GetWorkflowRulesList(); + + var engine = new RulesEngine(null, null); + engine.AddWorkflow(workflows); + + var input1 = new { + trueValue = true, + falseValue = false + }; + + var result = await engine.ExecuteAllRulesAsync(workflowName, input1); + Assert.True(result.All(c => c.IsSuccess)); + + CheckResultTreeContainsAllInputs(workflowName, result); + + } + + [Theory] + [InlineData("GlobalAndLocalParams")] + public async Task WorkflowUpdate_GlobalParam_ShouldReflect(string workflowName) + { + var workflows = GetWorkflowRulesList(); + + var engine = new RulesEngine(null, null); + engine.AddWorkflow(workflows); + + var input1 = new { + trueValue = true, + falseValue = false + }; + + var result = await engine.ExecuteAllRulesAsync(workflowName, input1); + Assert.True(result.All(c => c.IsSuccess)); + + var workflowToUpdate = workflows.Single(c => c.WorkflowName == workflowName); + engine.RemoveWorkflow(workflowName); + workflowToUpdate.GlobalParams.First().Expression = "true == false"; + engine.AddWorkflow(workflowToUpdate); + + var result2 = await engine.ExecuteAllRulesAsync(workflowName, input1); + + Assert.True(result2.All(c => c.IsSuccess == false)); + } + + + [Theory] + [InlineData("GlobalParamsOnly",new []{ false })] + [InlineData("LocalParamsOnly", new[] { false, true })] + [InlineData("GlobalAndLocalParams", new[] { false })] + public async Task DisabledScopedParam_ShouldReflect(string workflowName, bool[] outputs) + { + var workflows = GetWorkflowRulesList(); + + var engine = new RulesEngine(new string[] { }, null, new ReSettings { + EnableScopedParams = false + }); + engine.AddWorkflow(workflows); + + var input1 = new { + trueValue = true, + falseValue = false + }; + + var result = await engine.ExecuteAllRulesAsync(workflowName, input1); + 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); + } + } + } + + private void CheckResultTreeContainsAllInputs(string workflowName, List result) + { + var workflow = GetWorkflowRulesList().Single(c => c.WorkflowName == workflowName); + var expectedInputs = new List() { "input1" }; + expectedInputs.AddRange(workflow.GlobalParams?.Select(c => c.Name) ?? new List()); + + + foreach (var resultTree in result) + { + CheckInputs(expectedInputs, resultTree); + } + + } + + private static void CheckInputs(IEnumerable expectedInputs, RuleResultTree resultTree) + { + Assert.All(expectedInputs, input => Assert.True(resultTree.Inputs.ContainsKey(input))); + + var localParamNames = resultTree.Rule.LocalParams?.Select(c => c.Name) ?? new List(); + Assert.All(localParamNames, input => Assert.True(resultTree.Inputs.ContainsKey(input))); + +#pragma warning disable CS0618 // Type or member is obsolete + Assert.All(localParamNames, lp => Assert.Contains(resultTree.RuleEvaluatedParams, c => c.Name == lp)); +#pragma warning restore CS0618 // Type or member is obsolete + + if (resultTree.ChildResults?.Any() == true) + { + foreach (var childResultTree in resultTree.ChildResults) + { + CheckInputs(expectedInputs.Concat(localParamNames), childResultTree); + } + + } + + } + private WorkflowRules[] GetWorkflowRulesList() + { + return new WorkflowRules[] { + new WorkflowRules { + WorkflowName = "NoLocalAndGlobalParams", + Rules = new List { + new Rule { + RuleName = "TruthTest", + Expression = "input1.trueValue" + } + } + }, + new WorkflowRules { + WorkflowName = "LocalParamsOnly", + Rules = new List { + new Rule { + + RuleName = "WithLocalParam", + LocalParams = new List { + new ScopedParam { + Name = "localParam1", + Expression = "input1.trueValue" + } + }, + Expression = "localParam1 == true" + }, + new Rule { + + RuleName = "WithoutLocalParam", + Expression = "input1.falseValue == false" + }, + } + }, + new WorkflowRules { + WorkflowName = "GlobalParamsOnly", + GlobalParams = new List { + new ScopedParam { + Name = "globalParam1", + Expression = "input1.falseValue == false" + } + }, + Rules = new List { + new Rule { + RuleName = "TrueTest", + Expression = "globalParam1 == true" + } + } + }, + new WorkflowRules { + WorkflowName = "GlobalAndLocalParams", + GlobalParams = new List { + new ScopedParam { + Name = "globalParam1", + Expression = "input1.falseValue == false" + } + }, + Rules = new List { + new Rule { + RuleName = "WithLocalParam", + LocalParams = new List { + new ScopedParam { + Name = "localParam1", + Expression = "input1.trueValue" + } + }, + Expression = "globalParam1 == true && localParam1 == true" + }, + } + + }, + new WorkflowRules { + WorkflowName = "GlobalParamReferencedInLocalParams", + GlobalParams = new List { + new ScopedParam { + Name = "globalParam1", + Expression = "\"testString\"" + } + }, + Rules = new List { + new Rule { + + RuleName = "WithLocalParam", + LocalParams = new List { + new ScopedParam { + Name = "localParam1", + Expression = "globalParam1.ToUpper()" + } + }, + Expression = "globalParam1 == \"testString\" && localParam1 == \"TESTSTRING\"" + }, + } + }, + new WorkflowRules { + WorkflowName = "GlobalParamReferencedInNextGlobalParams", + GlobalParams = new List { + new ScopedParam { + Name = "globalParam1", + Expression = "\"testString\"" + }, + new ScopedParam { + Name = "globalParam2", + Expression = "globalParam1.ToUpper()" + } + }, + Rules = new List { + new Rule { + RuleName = "WithLocalParam", + Expression = "globalParam1 == \"testString\" && globalParam2 == \"TESTSTRING\"" + }, + } + }, + new WorkflowRules { + WorkflowName = "LocalParamReferencedInNextLocalParams", + Rules = new List { + new Rule { + LocalParams = new List { + new ScopedParam { + Name = "localParam1", + Expression = "\"testString\"" + }, + new ScopedParam { + Name = "localParam2", + Expression = "localParam1.ToUpper()" + } + }, + RuleName = "WithLocalParam", + Expression = "localParam1 == \"testString\" && localParam2 == \"TESTSTRING\"" + }, + } + }, + new WorkflowRules { + WorkflowName = "GlobalParamAndLocalParamsInNestedRules", + GlobalParams = new List { + new ScopedParam { + Name = "globalParam1", + Expression = @"""hello""" + } + }, + Rules = new List { + new Rule { + RuleName = "NestedRuleTest", + Operator = "And", + LocalParams = new List { + new ScopedParam { + Name = "localParam1", + Expression = @"""world""" + } + }, + Rules = new List{ + new Rule{ + RuleName = "NestedRule1", + Expression = "globalParam1 == \"hello\" && localParam1 == \"world\"" + }, + new Rule { + RuleName = "NestedRule2", + LocalParams = new List { + new ScopedParam { + Name = "nestedLocalParam1", + Expression = "globalParam1 + \" \" + localParam1" + } + }, + Expression = "nestedLocalParam1 == \"hello world\"" + } + + } + + } + } + } + }; + } + } +}