Abbasc52/nested fix (#96)

- Added GlobalParams support #97
- LocalParams now work at all nested levels #98
- Added Enabled field to Rule to enable/disable a Rule #99
- Fixed Rule compilation error not appearing as error message in certain cases #95
pull/107/head
Abbas Cyclewala 2021-02-02 10:29:21 +05:30 committed by GitHub
parent 04060e7159
commit b49ffd207d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 926 additions and 258 deletions

View File

@ -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

View File

@ -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.

View File

@ -38,7 +38,7 @@ namespace RulesEngineBenchmark
rulesEngine = new RulesEngine.RulesEngine(workflows.ToArray(), null, new ReSettings {
EnableFormattedErrorMessage = false,
EnableLocalParams = false
EnableScopedParams = false
});
ruleInput = new {

View File

@ -7,11 +7,11 @@
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
<!--<PackageReference Include="RulesEngine" Version="3.0.0-preview.2" />-->
<!--<PackageReference Include="RulesEngine" Version="3.0.2" />-->
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\RulesEngine\RulesEngine.csproj" />
<ProjectReference Include="..\..\src\RulesEngine\RulesEngine.csproj" />
</ItemGroup>
<ItemGroup>

View File

@ -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
{
/// <summary>
/// This class will build the list expression
/// </summary>
internal sealed class LambdaExpressionBuilder : RuleExpressionBuilderBase
{
private readonly ReSettings _reSettings;
@ -26,19 +26,32 @@ namespace RulesEngine.ExpressionBuilders
try
{
var ruleDelegate = _ruleExpressionParser.Compile<bool>(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<object[],Dictionary<string,object>> CompileScopedParams(RuleParameter[] ruleParameters, RuleExpressionParameter[] scopedParameters)
{
return _ruleExpressionParser.CompileRuleExpressionParameters(ruleParameters, scopedParameters);
}
}
}

View File

@ -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
/// <param name="ruleInputExp">The rule input exp.</param>
/// <returns>Expression type</returns>
internal abstract RuleFunc<RuleResultTree> BuildDelegateForRule(Rule rule, RuleParameter[] ruleParams);
internal abstract LambdaExpression Parse(string expression, ParameterExpression[] parameters, Type returnType);
internal abstract Func<object[], Dictionary<string, object>> CompileScopedParams(RuleParameter[] ruleParameters, RuleExpressionParameter[] scopedParameters);
}
}

View File

@ -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<object[], T> Compile<T>(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<object[], T> Compile<T>(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<T>(e, typeParamExpressions);
return wrappedExpression.CompileFast<Func<object[], T>>();
var parameterExpressions = GetParameterExpression(ruleParams).ToArray();
var e = Parse(expression, parameterExpressions, typeof(T));
var expressionBody = new List<Expression>() { e.Body };
var wrappedExpression = WrapExpression<T>(expressionBody, parameterExpressions, new ParameterExpression[] { });
return wrappedExpression.CompileFast();
});
}
private Expression<Func<object[], T>> WrapExpression<T>(LambdaExpression expression, ParameterExpression[] parameters)
private Expression<Func<object[], T>> WrapExpression<T>(List<Expression> 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> { expression.Body });
var blockExp = Expression.Block(parameters, blockExpSteps);
var blockExpSteps = paramExps.Concat(expressionList);
var blockExp = Expression.Block(parameters.Concat(variables), blockExpSteps);
return Expression.Lambda<Func<object[], T>>(blockExp, argExp);
}
internal Func<object[],Dictionary<string,object>> CompileRuleExpressionParameters(RuleParameter[] ruleParams, RuleExpressionParameter[] ruleExpParams = null)
{
ruleExpParams = ruleExpParams ?? new RuleExpressionParameter[] { };
var expression = CreateDictionaryExpression(ruleParams, ruleExpParams);
return expression.CompileFast();
}
public T Evaluate<T>(string expression, RuleParameter[] ruleParams)
{
@ -58,6 +72,13 @@ namespace RulesEngine.ExpressionBuilders
return func(ruleParams.Select(c => c.Value).ToArray());
}
private IEnumerable<Expression> CreateAssignedParameterExpression(RuleExpressionParameter[] ruleExpParams)
{
return ruleExpParams.Select((c, i) => {
return Expression.Assign(c.ParameterExpression, c.ValueExpression);
});
}
// <summary>
/// Gets the parameter expression.
/// </summary>
@ -68,7 +89,7 @@ namespace RulesEngine.ExpressionBuilders
/// or
/// type
/// </exception>
private IEnumerable<ParameterExpression> GetParameterExpression(params RuleParameter[] ruleParams)
private IEnumerable<ParameterExpression> 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<Func<object[],Dictionary<string,object>>> CreateDictionaryExpression(RuleParameter[] ruleParams, RuleExpressionParameter[] ruleExpParams)
{
var body = new List<Expression>();
var paramExp = new List<ParameterExpression>();
var variableExp = new List<ParameterExpression>();
var variableExpressions = CreateAssignedParameterExpression(ruleExpParams);
body.AddRange(variableExpressions);
var dict = Expression.Variable(typeof(Dictionary<string, object>));
var add = typeof(Dictionary<string, object>).GetMethod("Add", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(string), typeof(object) }, null);
body.Add(Expression.Assign(dict, Expression.New(typeof(Dictionary<string, object>))));
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<Dictionary<string,object>>(body, paramExp.ToArray(), variableExp.ToArray());
}
private string GetCacheKey(string expression, RuleParameter[] ruleParameters, Type returnType)
{
var paramKey = string.Join("|", ruleParameters.Select(c => c.Type.ToString()));

View File

@ -13,6 +13,13 @@ namespace RulesEngine.Extensions
public delegate void OnSuccessFunc(string eventName);
public delegate void OnFailureFunc();
/// <summary>
/// Calls the Success Func for the first rule which succeeded among the the ruleReults
/// </summary>
/// <param name="ruleResultTrees"></param>
/// <param name="onSuccessFunc"></param>
/// <returns></returns>
public static List<RuleResultTree> OnSuccess(this List<RuleResultTree> ruleResultTrees, OnSuccessFunc onSuccessFunc)
{
var successfulRuleResult = ruleResultTrees.FirstOrDefault(ruleResult => ruleResult.IsSuccess == true);
@ -25,6 +32,12 @@ namespace RulesEngine.Extensions
return ruleResultTrees;
}
/// <summary>
/// Calls the Failure Func if all rules failed in the ruleReults
/// </summary>
/// <param name="ruleResultTrees"></param>
/// <param name="onSuccessFunc"></param>
/// <returns></returns>
public static List<RuleResultTree> OnFail(this List<RuleResultTree> ruleResultTrees, OnFailureFunc onFailureFunc)
{
bool allFailure = ruleResultTrees.All(ruleResult => ruleResult.IsSuccess == false);

View File

@ -29,6 +29,7 @@ namespace RulesEngine.HelperFunctions
/// </summary>
/// <param name="ruleResultTree">ruleResultTree</param>
/// <param name="ruleResultMessage">ruleResultMessage</param>
[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
/// </summary>
/// <param name="childResultTree">childResultTree</param>
/// <param name="ruleResultMessage">ruleResultMessage</param>
[Obsolete]
private static void GetChildRuleMessages(IEnumerable<RuleResultTree> childResultTree, ref RuleResultMessage ruleResultMessage)
{
foreach (var item in childResultTree)

View File

@ -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<string, object>)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<object>();
var newList = new List<object>().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;

View File

@ -25,8 +25,28 @@ namespace RulesEngine.Interfaces
/// <returns>List of rule results</returns>
ValueTask<List<RuleResultTree>> ExecuteAllRulesAsync(string workflowName, params RuleParameter[] ruleParams);
ValueTask<ActionRuleResult> ExecuteActionWorkflowAsync(string workflowName, string ruleName, RuleParameter[] ruleParameters);
/// <summary>
/// Adds new workflows to RulesEngine
/// </summary>
/// <param name="workflowRules"></param>
void AddWorkflow(params WorkflowRules[] workflowRules);
/// <summary>
/// Removes all registered workflows from RulesEngine
/// </summary>
void ClearWorkflows();
/// <summary>
/// Removes the workflow from RulesEngine
/// </summary>
/// <param name="workflowNames"></param>
void RemoveWorkflow(params string[] workflowNames);
/// <summary>
/// Returns the list of all registered workflow names
/// </summary>
/// <returns></returns>
List<string> GetAllRegisteredWorkflowNames();
}
}

View File

@ -1,23 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Diagnostics.CodeAnalysis;
namespace RulesEngine.Models
{
/// <summary>
/// CompiledParam class.
/// </summary>
[ExcludeFromCodeCoverage]
internal class CompiledParam
{
internal string Name { get; set; }
internal Type ReturnType { get; set; }
internal Func<object[], object> Value { get; set; }
internal RuleParameter AsRuleParameter()
{
return new RuleParameter(Name, ReturnType);
}
}
}

View File

@ -11,10 +11,46 @@ namespace RulesEngine.Models
[ExcludeFromCodeCoverage]
public class ReSettings
{
/// <summary>
/// Get/Set the custom types to be used in Rule expressions
/// </summary>
public Type[] CustomTypes { get; set; }
/// <summary>
/// Get/Set the custom actions that can be used in the Rules
/// </summary>
public Dictionary<string, Func<ActionBase>> CustomActions { get; set; }
/// <summary>
/// When set to true, returns any exception occurred
/// while rule execution as ErrorMessage
/// otherwise throws an exception
/// </summary>
/// <remarks>This setting is only applicable if IgnoreException is set to false</remarks>
public bool EnableExceptionAsErrorMessage { get; set; } = true;
/// <summary>
/// When set to true, it will ignore any exception thrown with rule compilation/execution
/// </summary>
public bool IgnoreException { get; set; } = false;
/// <summary>
/// Enables ErrorMessage Formatting
/// </summary>
public bool EnableFormattedErrorMessage { get; set; } = true;
public bool EnableLocalParams { get; set; } = true;
/// <summary>
/// Enables Global params and local params for rules
/// </summary>
public bool EnableScopedParams { get; set; } = true;
/// <summary>
/// Enables Local params for rules
/// </summary>
[Obsolete("Use 'EnableScopedParams' instead. This will be removed in next major version")]
public bool EnableLocalParams {
get { return EnableScopedParams; }
set { EnableScopedParams = value; }
}
}
}

View File

@ -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
{
/// <summary>
/// Rule name for the Rule
/// </summary>
public string RuleName { get; set; }
/// <summary>
/// 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; }
/// <summary>
/// Gets or sets whether the rule is enabled.
/// </summary>
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<string> WorkflowRulesToInject { get; set; }
public List<Rule> Rules { get; set; }
[JsonProperty]
public IEnumerable<LocalParam> LocalParams { get; set; }
public IEnumerable<ScopedParam> LocalParams { get; set; }
public string Expression { get; set; }
public Dictionary<ActionTriggerType, ActionInfo> Actions { get; set; }
public string SuccessEvent { get; set; }

View File

@ -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
{
/// <summary>
/// CompiledParam class.
/// </summary>
[ExcludeFromCodeCoverage]
public class RuleExpressionParameter
{
public ParameterExpression ParameterExpression { get; set; }
public Expression ValueExpression { get; set; }
}
}

View File

@ -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; }
}
}

View File

@ -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
/// <value>
/// The rule evaluated parameters.
/// </value>
[Obsolete("Use `Inputs` field to get details of all input, localParams and globalParams")]
public IEnumerable<RuleParameter> RuleEvaluatedParams { get; set; }
/// <summary>
@ -62,9 +64,10 @@ namespace RulesEngine.Models
/// </summary>
/// <returns>RuleResultMessage</returns>
[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);

View File

@ -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
/// <summary>Class LocalParam.
/// </summary>
[ExcludeFromCodeCoverage]
public class LocalParam
public class ScopedParam
{
/// <summary>
/// Gets or sets the name of the rule.
/// Gets or sets the name of the param.
/// </summary>
/// <value>
/// The name of the rule.
/// </value>
[JsonProperty, JsonRequired]
/// </value>]
public string Name { get; set; }
/// <summary>
/// Gets or Sets the lambda expression.
/// Gets or Sets the lambda expression which can be reference in Rule.
/// </summary>
[JsonProperty, JsonRequired]
public string Expression { get; set; }
}
[ExcludeFromCodeCoverage]
public class LocalParam : ScopedParam { }
}

View File

@ -21,6 +21,11 @@ namespace RulesEngine.Models
/// <value>The workflow rules to inject.</value>
public IEnumerable<string> WorkflowRulesToInject { get; set; }
/// <summary>
/// Gets or Sets the global params which will be applicable to all rules
/// </summary>
public IEnumerable<ScopedParam> GlobalParams { get; set; }
/// <summary>
/// list of rules.
/// </summary>

View File

@ -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
{
/// <summary>
/// Rule param compilers
/// </summary>
internal class ParamCompiler
{
private readonly ReSettings _reSettings;
private readonly RuleExpressionParser _ruleExpressionParser;
internal ParamCompiler(ReSettings reSettings, RuleExpressionParser ruleExpressionParser)
{
_reSettings = reSettings;
_ruleExpressionParser = ruleExpressionParser;
}
/// <summary>
/// Compiles the and evaluate parameter expression.
/// </summary>
/// <param name="rule">The rule.</param>
/// <param name="ruleParams">The rule parameters.</param>
/// <returns>
/// IEnumerable&lt;RuleParameter&gt;.
/// </returns>
public IEnumerable<CompiledParam> CompileParamsExpression(Rule rule, IEnumerable<RuleParameter> ruleParams)
{
if (rule.LocalParams == null) return null;
var compiledParameters = new List<CompiledParam>();
var evaluatedParameters = new List<RuleParameter>();
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;
}
/// <summary>Evaluates the compiled parameter.</summary>
/// <param name="paramName">Name of the parameter.</param>
/// <param name="compiledParam">The compiled parameter.</param>
/// <param name="ruleParams">The rule parameters.</param>
/// <returns>RuleParameter.</returns>
public RuleParameter EvaluateCompiledParam(string paramName, Func<object[], object> compiledParam, IEnumerable<RuleParameter> inputs)
{
var result = compiledParam(inputs.Select(c => c.Value).ToArray());
return new RuleParameter(paramName, result);
}
/// <summary>
/// Gets the expression for rule.
/// </summary>
/// <param name="param">The rule.</param>
/// <param name="typeParameterExpressions">The type parameter expressions.</param>
/// <param name="ruleInputExp">The rule input exp.</param>
/// <returns></returns>
private Func<object[], object> GetDelegateForRuleParam(LocalParam param, RuleParameter[] ruleParameters)
{
return _ruleExpressionParser.Compile<object>(param.Expression, ruleParameters);
}
}
}

View File

@ -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
/// </summary>
private readonly RuleExpressionBuilderFactory _expressionBuilderFactory;
private readonly ReSettings _reSettings;
/// <summary>
/// The logger
@ -36,10 +39,12 @@ namespace RulesEngine
/// </summary>
/// <param name="expressionBuilderFactory">The expression builder factory.</param>
/// <exception cref="ArgumentNullException">expressionBuilderFactory</exception>
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;
}
/// <summary>
@ -50,7 +55,7 @@ namespace RulesEngine
/// <param name="input"></param>
/// <param name="ruleParam"></param>
/// <returns>Compiled func delegate</returns>
internal RuleFunc<RuleResultTree> CompileRule(Rule rule, params RuleParameter[] ruleParams)
internal RuleFunc<RuleResultTree> 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
/// <returns></returns>
private RuleFunc<RuleResultTree> 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<RuleResultTree> 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<ScopedParam> localParams, RuleParameter[] ruleParams)
{
if(!_reSettings.EnableScopedParams)
{
return new RuleExpressionParameter[] { };
}
var ruleExpParams = new List<RuleExpressionParameter>();
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();
}
/// <summary>
@ -100,12 +148,7 @@ namespace RulesEngine
/// <exception cref="InvalidOperationException"></exception>
private RuleFunc<RuleResultTree> 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<RuleResultTree> BuildNestedRuleFunc(Rule parentRule, ExpressionType operation, RuleParameter[] ruleParams)
{
var ruleFuncList = new List<RuleFunc<RuleResultTree>>();
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<object[], bool> 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<RuleResultTree> ruleResults, ExpressionType operation)
{
if (ruleResults?.Any() != true)
{
return false;
}
switch (operation)
{
case ExpressionType.And:
@ -154,5 +202,36 @@ namespace RulesEngine
return false;
}
}
private RuleFunc<RuleResultTree> GetWrappedRuleFunc(RuleExpressionType ruleExpressionType, RuleFunc<RuleResultTree> 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);
}
}
}

View File

@ -27,6 +27,11 @@ namespace RulesEngine
return _workflowRules.ContainsKey(workflowName);
}
public List<string> GetAllWorkflowNames()
{
return _workflowRules.Keys.ToList();
}
/// <summary>Determines whether [contains compiled rules] [the specified workflow name].</summary>
/// <param name="workflowName">Name of the workflow.</param>
/// <returns>
@ -59,16 +64,6 @@ namespace RulesEngine
_compileRules.Clear();
}
/// <summary>Gets the rules.</summary>
/// <param name="workflowName">Name of the workflow.</param>
/// <returns>IEnumerable&lt;Rule&gt;.</returns>
public IEnumerable<Rule> GetRules(string workflowName)
{
if (!ContainsWorkflowRules(workflowName))
throw new ArgumentException($"workflow `{workflowName}` was not found");
return _workflowRules[workflowName].Rules;
}
/// <summary>Gets the work flow rules.</summary>
/// <param name="workflowName">Name of the workflow.</param>
/// <returns>WorkflowRules.</returns>

View File

@ -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<RulesEngine>();
_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<string> GetAllRegisteredWorkflowNames()
{
return _rulesCache.GetAllWorkflowNames();
}
/// <summary>
/// Clears the workflows.
/// </summary>
public void ClearWorkflows()
{
_rulesCache.Clear();
ClearCompiledParamCache();
}
/// <summary>
@ -192,7 +193,6 @@ namespace RulesEngine
{
_rulesCache.Remove(workflowName);
}
ClearCompiledParamCache();
}
/// <summary>
@ -239,9 +239,9 @@ namespace RulesEngine
if (workflowRules != null)
{
var dictFunc = new Dictionary<string, RuleFunc<RuleResultTree>>();
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<RuleResultTree> 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<RuleResultTree> CompileRule(string workflowName, RuleParameter[] ruleParams, Rule rule)
private RuleFunc<RuleResultTree> 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<RuleParameter>();
var updatedRuleParams = ruleParams?.Concat(compiledRuleParameters);
var compiledRule = _ruleCompiler.CompileRule(rule, updatedRuleParams?.ToArray());
RuleFunc<RuleResultTree> updatedRule = (RuleParameter[] paramList) => {
var inputs = paramList.AsEnumerable();
var localParams = compiledParamList ?? new List<CompiledParam>();
var evaluatedParamList = new List<RuleParameter>();
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<string, Func<ActionBase>> GetDefaultActionRegistry()
{
return new Dictionary<string, Func<ActionBase>>{
@ -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;
}
/// <summary>
/// Clears all compiledParams
/// </summary>
private void ClearCompiledParamCache()
{
_compiledParamsCache.Dispose();
_compiledParamsCache = new MemoryCache(new MemoryCacheOptions());
}
#endregion
}
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Version>3.0.2</Version>
<Version>3.1.0-preview.1</Version>
<Copyright>Copyright (c) Microsoft Corporation.</Copyright>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://github.com/microsoft/RulesEngine</PackageProjectUrl>
@ -27,12 +27,12 @@
<ItemGroup>
<PackageReference Include="FastExpressionCompiler" Version="2.0.0" />
<PackageReference Include="FluentValidation" Version="9.3.0" />
<PackageReference Include="FluentValidation" Version="9.4.0" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.2.6" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.2.7" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>

View File

@ -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);
});
}

View File

@ -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<List<RuleResultTree>>(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<LocalParam> {
new LocalParam {
Name = "lp1",
Expression = "true"

View File

@ -17,10 +17,10 @@ namespace RulesEngine.UnitTest
[Fact]
public void RuleCompiler_NullCheck()
{
Assert.Throws<ArgumentNullException>(() => new RuleCompiler(null, null));
Assert.Throws<ArgumentNullException>(() => new RuleCompiler(null, null,null));
var reSettings = new ReSettings();
var parser = new RuleExpressionParser(reSettings);
Assert.Throws<ArgumentNullException>(() => new RuleCompiler(new RuleExpressionBuilderFactory(reSettings, parser), null));
Assert.Throws<ArgumentNullException>(() => 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<RuleCompiler>());
Assert.Throws<ArgumentNullException>(() => compiler.CompileRule(null, null));
Assert.Throws<ArgumentNullException>(() => compiler.CompileRule(null, new RuleParameter[] { null }));
var compiler = new RuleCompiler(new RuleExpressionBuilderFactory(reSettings, parser),null, new NullLogger<RuleCompiler>());
Assert.Throws<ArgumentNullException>(() => compiler.CompileRule(null, null,null));
Assert.Throws<ArgumentNullException>(() => compiler.CompileRule(null, new RuleParameter[] { null },null));
}

View File

@ -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<RuleResultTree> 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<Rule> {
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<Rule> {
new Rule {
RuleName = "RuleWithoutEnabledFieldMentioned",
Operator = "And",
Rules = new List<Rule> {
new Rule {
RuleName = "RuleWithoutEnabledField",
Expression = "input1.TrueValue"
}
}
},
new Rule {
RuleName = "RuleWithOneChildSetToFalse",
Expression = "input1.TrueValue == true",
Operator = "And",
Rules = new List<Rule>{
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<Rule>{
new Rule {
RuleName = "RuleWithEnabledTrue",
Expression = "input1.TrueValue",
Enabled = true
}
}
},
new Rule {
RuleName = "RuleWithAllChildSetToFalse",
Operator = "And",
Enabled = true,
Rules = new List<Rule>{
new Rule {
RuleName = "ChildRuleWithEnabledFalse",
Expression = "input1.TrueValue",
Enabled = false
}
}
}
}
}
};
}
}
}

View File

@ -3,15 +3,18 @@
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.14.0" />
<PackageReference Include="AutoFixture" Version="4.15.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="Moq" Version="4.15.2" />
<PackageReference Include="Moq" Version="4.16.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="1.3.0" />
<PackageReference Include="coverlet.collector" Version="3.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\RulesEngine\RulesEngine.csproj" />

View File

@ -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<RuleResultTree> result)
{
var workflow = GetWorkflowRulesList().Single(c => c.WorkflowName == workflowName);
var expectedInputs = new List<string>() { "input1" };
expectedInputs.AddRange(workflow.GlobalParams?.Select(c => c.Name) ?? new List<string>());
foreach (var resultTree in result)
{
CheckInputs(expectedInputs, resultTree);
}
}
private static void CheckInputs(IEnumerable<string> expectedInputs, RuleResultTree resultTree)
{
Assert.All(expectedInputs, input => Assert.True(resultTree.Inputs.ContainsKey(input)));
var localParamNames = resultTree.Rule.LocalParams?.Select(c => c.Name) ?? new List<string>();
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<Rule> {
new Rule {
RuleName = "TruthTest",
Expression = "input1.trueValue"
}
}
},
new WorkflowRules {
WorkflowName = "LocalParamsOnly",
Rules = new List<Rule> {
new Rule {
RuleName = "WithLocalParam",
LocalParams = new List<ScopedParam> {
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<ScopedParam> {
new ScopedParam {
Name = "globalParam1",
Expression = "input1.falseValue == false"
}
},
Rules = new List<Rule> {
new Rule {
RuleName = "TrueTest",
Expression = "globalParam1 == true"
}
}
},
new WorkflowRules {
WorkflowName = "GlobalAndLocalParams",
GlobalParams = new List<ScopedParam> {
new ScopedParam {
Name = "globalParam1",
Expression = "input1.falseValue == false"
}
},
Rules = new List<Rule> {
new Rule {
RuleName = "WithLocalParam",
LocalParams = new List<ScopedParam> {
new ScopedParam {
Name = "localParam1",
Expression = "input1.trueValue"
}
},
Expression = "globalParam1 == true && localParam1 == true"
},
}
},
new WorkflowRules {
WorkflowName = "GlobalParamReferencedInLocalParams",
GlobalParams = new List<ScopedParam> {
new ScopedParam {
Name = "globalParam1",
Expression = "\"testString\""
}
},
Rules = new List<Rule> {
new Rule {
RuleName = "WithLocalParam",
LocalParams = new List<ScopedParam> {
new ScopedParam {
Name = "localParam1",
Expression = "globalParam1.ToUpper()"
}
},
Expression = "globalParam1 == \"testString\" && localParam1 == \"TESTSTRING\""
},
}
},
new WorkflowRules {
WorkflowName = "GlobalParamReferencedInNextGlobalParams",
GlobalParams = new List<ScopedParam> {
new ScopedParam {
Name = "globalParam1",
Expression = "\"testString\""
},
new ScopedParam {
Name = "globalParam2",
Expression = "globalParam1.ToUpper()"
}
},
Rules = new List<Rule> {
new Rule {
RuleName = "WithLocalParam",
Expression = "globalParam1 == \"testString\" && globalParam2 == \"TESTSTRING\""
},
}
},
new WorkflowRules {
WorkflowName = "LocalParamReferencedInNextLocalParams",
Rules = new List<Rule> {
new Rule {
LocalParams = new List<ScopedParam> {
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<ScopedParam> {
new ScopedParam {
Name = "globalParam1",
Expression = @"""hello"""
}
},
Rules = new List<Rule> {
new Rule {
RuleName = "NestedRuleTest",
Operator = "And",
LocalParams = new List<ScopedParam> {
new ScopedParam {
Name = "localParam1",
Expression = @"""world"""
}
},
Rules = new List<Rule>{
new Rule{
RuleName = "NestedRule1",
Expression = "globalParam1 == \"hello\" && localParam1 == \"world\""
},
new Rule {
RuleName = "NestedRule2",
LocalParams = new List<ScopedParam> {
new ScopedParam {
Name = "nestedLocalParam1",
Expression = "globalParam1 + \" \" + localParam1"
}
},
Expression = "nestedLocalParam1 == \"hello world\""
}
}
}
}
}
};
}
}
}