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. 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] ## [3.0.2]
- Fixed LocalParams cache not getting cleaned up when RemoveWorkflow and ClearWorkflows are called - Fixed LocalParams cache not getting cleaned up when RemoveWorkflow and ClearWorkflows are called

View File

@ -1,6 +1,6 @@
# Rules Engine # Rules Engine
![build](https://github.com/microsoft/RulesEngine/workflows/build/badge.svg?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=master)](https://coveralls.io/github/microsoft/RulesEngine?branch=master) [![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] [![Nuget download][download-image]][download-url]
[download-image]: https://img.shields.io/nuget/dt/RulesEngine [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 ## 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 - An example rule could be -
```json ```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)._ _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 ## 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. 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 { rulesEngine = new RulesEngine.RulesEngine(workflows.ToArray(), null, new ReSettings {
EnableFormattedErrorMessage = false, EnableFormattedErrorMessage = false,
EnableLocalParams = false EnableScopedParams = false
}); });
ruleInput = new { ruleInput = new {

View File

@ -7,11 +7,11 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" /> <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>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\RulesEngine\RulesEngine.csproj" /> <ProjectReference Include="..\..\src\RulesEngine\RulesEngine.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -4,12 +4,12 @@
using RulesEngine.HelperFunctions; using RulesEngine.HelperFunctions;
using RulesEngine.Models; using RulesEngine.Models;
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
namespace RulesEngine.ExpressionBuilders namespace RulesEngine.ExpressionBuilders
{ {
/// <summary>
/// This class will build the list expression
/// </summary>
internal sealed class LambdaExpressionBuilder : RuleExpressionBuilderBase internal sealed class LambdaExpressionBuilder : RuleExpressionBuilderBase
{ {
private readonly ReSettings _reSettings; private readonly ReSettings _reSettings;
@ -26,19 +26,32 @@ namespace RulesEngine.ExpressionBuilders
try try
{ {
var ruleDelegate = _ruleExpressionParser.Compile<bool>(rule.Expression, ruleParams); var ruleDelegate = _ruleExpressionParser.Compile<bool>(rule.Expression, ruleParams);
bool func(object[] paramList) => ruleDelegate(paramList); return Helpers.ToResultTree(rule, null, ruleDelegate);
return Helpers.ToResultTree(rule, null, func);
} }
catch (Exception ex) catch (Exception ex)
{ {
ex.Data.Add(nameof(rule.RuleName), rule.RuleName); ex.Data.Add(nameof(rule.RuleName), rule.RuleName);
ex.Data.Add(nameof(rule.Expression), rule.Expression); ex.Data.Add(nameof(rule.Expression), rule.Expression);
if (!_reSettings.EnableExceptionAsErrorMessage) throw; if (!_reSettings.EnableExceptionAsErrorMessage)
{
throw;
}
bool func(object[] param) => false; bool func(object[] param) => false;
var exceptionMessage = $"Exception while parsing expression `{rule?.Expression}` - {ex.Message}"; var exceptionMessage = _reSettings.IgnoreException ? "" : $"Exception while parsing expression `{rule?.Expression}` - {ex.Message}";
return Helpers.ToResultTree(rule, null, func, exceptionMessage); 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. // Licensed under the MIT License.
using RulesEngine.Models; using RulesEngine.Models;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace RulesEngine.ExpressionBuilders namespace RulesEngine.ExpressionBuilders
{ {
@ -18,5 +21,9 @@ namespace RulesEngine.ExpressionBuilders
/// <param name="ruleInputExp">The rule input exp.</param> /// <param name="ruleInputExp">The rule input exp.</param>
/// <returns>Expression type</returns> /// <returns>Expression type</returns>
internal abstract RuleFunc<RuleResultTree> BuildDelegateForRule(Rule rule, RuleParameter[] ruleParams); 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;
using System.Linq.Dynamic.Core; using System.Linq.Dynamic.Core;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection;
namespace RulesEngine.ExpressionBuilders namespace RulesEngine.ExpressionBuilders
{ {
@ -25,32 +26,45 @@ namespace RulesEngine.ExpressionBuilders
}); });
} }
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) public Func<object[], T> Compile<T>(string expression, RuleParameter[] ruleParams)
{ {
var cacheKey = GetCacheKey(expression, ruleParams, typeof(T)); var cacheKey = GetCacheKey(expression, ruleParams, typeof(T));
return _memoryCache.GetOrCreate(cacheKey, (entry) => { return _memoryCache.GetOrCreate(cacheKey, (entry) => {
entry.SetSize(1); entry.SetSize(1);
var config = new ParsingConfig { CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes) }; var parameterExpressions = GetParameterExpression(ruleParams).ToArray();
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 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 argExp = Expression.Parameter(typeof(object[]), "args");
var paramExps = parameters.Select((c, i) => { var paramExps = parameters.Select((c, i) => {
var arg = Expression.ArrayAccess(argExp, Expression.Constant(i)); var arg = Expression.ArrayAccess(argExp, Expression.Constant(i));
return (Expression)Expression.Assign(c, Expression.Convert(arg, c.Type)); return (Expression)Expression.Assign(c, Expression.Convert(arg, c.Type));
}); });
var blockExpSteps = paramExps.Concat(new List<Expression> { expression.Body }); var blockExpSteps = paramExps.Concat(expressionList);
var blockExp = Expression.Block(parameters, blockExpSteps); var blockExp = Expression.Block(parameters.Concat(variables), blockExpSteps);
return Expression.Lambda<Func<object[], T>>(blockExp, argExp); 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) public T Evaluate<T>(string expression, RuleParameter[] ruleParams)
{ {
@ -58,6 +72,13 @@ namespace RulesEngine.ExpressionBuilders
return func(ruleParams.Select(c => c.Value).ToArray()); 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> // <summary>
/// Gets the parameter expression. /// Gets the parameter expression.
/// </summary> /// </summary>
@ -68,7 +89,7 @@ namespace RulesEngine.ExpressionBuilders
/// or /// or
/// type /// type
/// </exception> /// </exception>
private IEnumerable<ParameterExpression> GetParameterExpression(params RuleParameter[] ruleParams) private IEnumerable<ParameterExpression> GetParameterExpression(RuleParameter[] ruleParams)
{ {
foreach (var ruleParam in ruleParams) foreach (var ruleParam in ruleParams)
{ {
@ -77,10 +98,45 @@ namespace RulesEngine.ExpressionBuilders
throw new ArgumentException($"{nameof(ruleParam)} can't be null."); 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) private string GetCacheKey(string expression, RuleParameter[] ruleParameters, Type returnType)
{ {
var paramKey = string.Join("|", ruleParameters.Select(c => c.Type.ToString())); 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 OnSuccessFunc(string eventName);
public delegate void OnFailureFunc(); 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) public static List<RuleResultTree> OnSuccess(this List<RuleResultTree> ruleResultTrees, OnSuccessFunc onSuccessFunc)
{ {
var successfulRuleResult = ruleResultTrees.FirstOrDefault(ruleResult => ruleResult.IsSuccess == true); var successfulRuleResult = ruleResultTrees.FirstOrDefault(ruleResult => ruleResult.IsSuccess == true);
@ -25,6 +32,12 @@ namespace RulesEngine.Extensions
return ruleResultTrees; 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) public static List<RuleResultTree> OnFail(this List<RuleResultTree> ruleResultTrees, OnFailureFunc onFailureFunc)
{ {
bool allFailure = ruleResultTrees.All(ruleResult => ruleResult.IsSuccess == false); bool allFailure = ruleResultTrees.All(ruleResult => ruleResult.IsSuccess == false);

View File

@ -29,6 +29,7 @@ namespace RulesEngine.HelperFunctions
/// </summary> /// </summary>
/// <param name="ruleResultTree">ruleResultTree</param> /// <param name="ruleResultTree">ruleResultTree</param>
/// <param name="ruleResultMessage">ruleResultMessage</param> /// <param name="ruleResultMessage">ruleResultMessage</param>
[Obsolete]
internal static void ToResultTreeMessages(RuleResultTree ruleResultTree, ref RuleResultMessage ruleResultMessage) internal static void ToResultTreeMessages(RuleResultTree ruleResultTree, ref RuleResultMessage ruleResultMessage)
{ {
if (ruleResultTree.ChildResults != null) if (ruleResultTree.ChildResults != null)
@ -40,7 +41,7 @@ namespace RulesEngine.HelperFunctions
if (!ruleResultTree.IsSuccess) if (!ruleResultTree.IsSuccess)
{ {
string errMsg = ruleResultTree.Rule.ErrorMessage; 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)) if (ruleResultTree.Rule.ErrorType == ErrorType.Error && !ruleResultMessage.ErrorMessages.Contains(errMsg))
{ {
@ -59,6 +60,7 @@ namespace RulesEngine.HelperFunctions
/// </summary> /// </summary>
/// <param name="childResultTree">childResultTree</param> /// <param name="childResultTree">childResultTree</param>
/// <param name="ruleResultMessage">ruleResultMessage</param> /// <param name="ruleResultMessage">ruleResultMessage</param>
[Obsolete]
private static void GetChildRuleMessages(IEnumerable<RuleResultTree> childResultTree, ref RuleResultMessage ruleResultMessage) private static void GetChildRuleMessages(IEnumerable<RuleResultTree> childResultTree, ref RuleResultMessage ruleResultMessage)
{ {
foreach (var item in childResultTree) foreach (var item in childResultTree)

View File

@ -73,36 +73,38 @@ namespace RulesEngine.HelperFunctions
} }
object obj = Activator.CreateInstance(type); object obj = Activator.CreateInstance(type);
var typeProps = type.GetProperties().ToDictionary(c => c.Name);
foreach (var expando in (IDictionary<string, object>)input) 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)) expando.Value != null && (expando.Value.GetType().Name != "DBNull" || expando.Value != DBNull.Value))
{ {
object val; object val;
var propInfo = typeProps[expando.Key];
if (expando.Value is ExpandoObject) if (expando.Value is ExpandoObject)
{ {
var propType = type.GetProperty(expando.Key).PropertyType; var propType = propInfo.PropertyType;
val = CreateObject(propType, expando.Value); val = CreateObject(propType, expando.Value);
} }
else if (expando.Value is IList) 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 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++) for (int i = 0; i < temp.Count; i++)
{ {
var child = CreateObject(internalType, temp[i]); var child = CreateObject(internalType, temp[i]);
newList.Add(child); newList.Add(child);
}; };
val = newList.Cast(internalType).ToList(internalType); val = newList;
} }
else else
{ {
val = expando.Value; val = expando.Value;
} }
type.GetProperty(expando.Key).SetValue(obj, val, null); propInfo.SetValue(obj, val, null);
} }
} }
return obj; return obj;

View File

@ -25,8 +25,28 @@ namespace RulesEngine.Interfaces
/// <returns>List of rule results</returns> /// <returns>List of rule results</returns>
ValueTask<List<RuleResultTree>> ExecuteAllRulesAsync(string workflowName, params RuleParameter[] ruleParams); ValueTask<List<RuleResultTree>> ExecuteAllRulesAsync(string workflowName, params RuleParameter[] ruleParams);
ValueTask<ActionRuleResult> ExecuteActionWorkflowAsync(string workflowName, string ruleName, RuleParameter[] ruleParameters); 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); void AddWorkflow(params WorkflowRules[] workflowRules);
/// <summary>
/// Removes all registered workflows from RulesEngine
/// </summary>
void ClearWorkflows(); void ClearWorkflows();
/// <summary>
/// Removes the workflow from RulesEngine
/// </summary>
/// <param name="workflowNames"></param>
void RemoveWorkflow(params string[] workflowNames); 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] [ExcludeFromCodeCoverage]
public class ReSettings public class ReSettings
{ {
/// <summary>
/// Get/Set the custom types to be used in Rule expressions
/// </summary>
public Type[] CustomTypes { get; set; } 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; } 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; 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 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;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using RulesEngine.Enums; using RulesEngine.Enums;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@ -15,6 +16,9 @@ namespace RulesEngine.Models
[ExcludeFromCodeCoverage] [ExcludeFromCodeCoverage]
public class Rule public class Rule
{ {
/// <summary>
/// Rule name for the Rule
/// </summary>
public string RuleName { get; set; } public string RuleName { get; set; }
/// <summary> /// <summary>
/// Gets or sets the custom property or tags of the rule. /// 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 Operator { get; set; }
public string ErrorMessage { 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))] [JsonConverter(typeof(StringEnumConverter))]
public ErrorType ErrorType { get; set; } public ErrorType ErrorType { get; set; } = ErrorType.Warning;
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
public RuleExpressionType? RuleExpressionType { get; set; } public RuleExpressionType RuleExpressionType { get; set; } = RuleExpressionType.LambdaExpression;
public List<string> WorkflowRulesToInject { get; set; } public List<string> WorkflowRulesToInject { get; set; }
public List<Rule> Rules { get; set; } public List<Rule> Rules { get; set; }
public IEnumerable<ScopedParam> LocalParams { get; set; }
[JsonProperty]
public IEnumerable<LocalParam> LocalParams { get; set; }
public string Expression { get; set; } public string Expression { get; set; }
public Dictionary<ActionTriggerType, ActionInfo> Actions { get; set; } public Dictionary<ActionTriggerType, ActionInfo> Actions { get; set; }
public string SuccessEvent { 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 RulesEngine.HelperFunctions;
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
namespace RulesEngine.Models namespace RulesEngine.Models
{ {
@ -13,18 +14,24 @@ namespace RulesEngine.Models
public RuleParameter(string name, object value) public RuleParameter(string name, object value)
{ {
Value = Utils.GetTypedObject(value); Value = Utils.GetTypedObject(value);
Type = Value.GetType(); Init(name, Value.GetType());
Name = name;
} }
internal RuleParameter(string name, Type type) 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; Name = name;
Type = type; 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. // Licensed under the MIT License.
using RulesEngine.HelperFunctions; using RulesEngine.HelperFunctions;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@ -55,6 +56,7 @@ namespace RulesEngine.Models
/// <value> /// <value>
/// The rule evaluated parameters. /// The rule evaluated parameters.
/// </value> /// </value>
[Obsolete("Use `Inputs` field to get details of all input, localParams and globalParams")]
public IEnumerable<RuleParameter> RuleEvaluatedParams { get; set; } public IEnumerable<RuleParameter> RuleEvaluatedParams { get; set; }
/// <summary> /// <summary>
@ -62,9 +64,10 @@ namespace RulesEngine.Models
/// </summary> /// </summary>
/// <returns>RuleResultMessage</returns> /// <returns>RuleResultMessage</returns>
[ExcludeFromCodeCoverage] [ExcludeFromCodeCoverage]
[Obsolete("will be removed in next major version")]
public RuleResultMessage GetMessages() public RuleResultMessage GetMessages()
{ {
RuleResultMessage ruleResultMessage = new RuleResultMessage(); var ruleResultMessage = new RuleResultMessage();
Helpers.ToResultTreeMessages(this, ref ruleResultMessage); Helpers.ToResultTreeMessages(this, ref ruleResultMessage);

View File

@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation. // Copyright (c) Microsoft Corporation.
// Licensed under the MIT License. // Licensed under the MIT License.
using Newtonsoft.Json;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
namespace RulesEngine.Models namespace RulesEngine.Models
@ -9,22 +8,23 @@ namespace RulesEngine.Models
/// <summary>Class LocalParam. /// <summary>Class LocalParam.
/// </summary> /// </summary>
[ExcludeFromCodeCoverage] [ExcludeFromCodeCoverage]
public class LocalParam public class ScopedParam
{ {
/// <summary> /// <summary>
/// Gets or sets the name of the rule. /// Gets or sets the name of the param.
/// </summary> /// </summary>
/// <value> /// <value>
/// The name of the rule. /// The name of the rule.
/// </value> /// </value>]
[JsonProperty, JsonRequired]
public string Name { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
/// Gets or Sets the lambda expression. /// Gets or Sets the lambda expression which can be reference in Rule.
/// </summary> /// </summary>
[JsonProperty, JsonRequired]
public string Expression { get; set; } 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> /// <value>The workflow rules to inject.</value>
public IEnumerable<string> WorkflowRulesToInject { get; set; } 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> /// <summary>
/// list of rules. /// list of rules.
/// </summary> /// </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. // Licensed under the MIT License.
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using RulesEngine.ExpressionBuilders;
using RulesEngine.HelperFunctions; using RulesEngine.HelperFunctions;
using RulesEngine.Models; using RulesEngine.Models;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Runtime.InteropServices;
namespace RulesEngine namespace RulesEngine
{ {
@ -25,6 +27,7 @@ namespace RulesEngine
/// The expression builder factory /// The expression builder factory
/// </summary> /// </summary>
private readonly RuleExpressionBuilderFactory _expressionBuilderFactory; private readonly RuleExpressionBuilderFactory _expressionBuilderFactory;
private readonly ReSettings _reSettings;
/// <summary> /// <summary>
/// The logger /// The logger
@ -36,10 +39,12 @@ namespace RulesEngine
/// </summary> /// </summary>
/// <param name="expressionBuilderFactory">The expression builder factory.</param> /// <param name="expressionBuilderFactory">The expression builder factory.</param>
/// <exception cref="ArgumentNullException">expressionBuilderFactory</exception> /// <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."); _logger = logger ?? throw new ArgumentNullException($"{nameof(logger)} can't be null.");
_expressionBuilderFactory = expressionBuilderFactory ?? throw new ArgumentNullException($"{nameof(expressionBuilderFactory)} can't be null."); _expressionBuilderFactory = expressionBuilderFactory ?? throw new ArgumentNullException($"{nameof(expressionBuilderFactory)} can't be null.");
_reSettings = reSettings;
} }
/// <summary> /// <summary>
@ -50,7 +55,7 @@ namespace RulesEngine
/// <param name="input"></param> /// <param name="input"></param>
/// <param name="ruleParam"></param> /// <param name="ruleParam"></param>
/// <returns>Compiled func delegate</returns> /// <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 try
{ {
@ -58,8 +63,11 @@ namespace RulesEngine
{ {
throw new ArgumentNullException(nameof(rule)); throw new ArgumentNullException(nameof(rule));
} }
var ruleExpression = GetDelegateForRule(rule, ruleParams); var globalParamExp = GetRuleExpressionParameters(rule.RuleExpressionType,globalParams, ruleParams);
return ruleExpression; 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) catch (Exception ex)
{ {
@ -79,15 +87,55 @@ namespace RulesEngine
/// <returns></returns> /// <returns></returns>
private RuleFunc<RuleResultTree> GetDelegateForRule(Rule rule, RuleParameter[] ruleParams) 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) && if (Enum.TryParse(rule.Operator, out ExpressionType nestedOperator) && nestedOperators.Contains(nestedOperator) &&
rule.Rules != null && rule.Rules.Any()) rule.Rules != null && rule.Rules.Any())
{ {
return BuildNestedRuleFunc(rule, nestedOperator, ruleParams); ruleFn = BuildNestedRuleFunc(rule, nestedOperator, extendedRuleParams);
} }
else 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> /// <summary>
@ -100,12 +148,7 @@ namespace RulesEngine
/// <exception cref="InvalidOperationException"></exception> /// <exception cref="InvalidOperationException"></exception>
private RuleFunc<RuleResultTree> BuildRuleFunc(Rule rule, RuleParameter[] ruleParams) private RuleFunc<RuleResultTree> BuildRuleFunc(Rule rule, RuleParameter[] ruleParams)
{ {
if (!rule.RuleExpressionType.HasValue) var ruleExpressionBuilder = GetExpressionBuilder(rule.RuleExpressionType);
{
throw new InvalidOperationException($"RuleExpressionType can not be null for leaf level expressions.");
}
var ruleExpressionBuilder = _expressionBuilderFactory.RuleGetExpressionBuilder(rule.RuleExpressionType.Value);
var ruleFunc = ruleExpressionBuilder.BuildDelegateForRule(rule, ruleParams); var ruleFunc = ruleExpressionBuilder.BuildDelegateForRule(rule, ruleParams);
@ -125,13 +168,13 @@ namespace RulesEngine
private RuleFunc<RuleResultTree> BuildNestedRuleFunc(Rule parentRule, ExpressionType operation, RuleParameter[] ruleParams) private RuleFunc<RuleResultTree> BuildNestedRuleFunc(Rule parentRule, ExpressionType operation, RuleParameter[] ruleParams)
{ {
var ruleFuncList = new List<RuleFunc<RuleResultTree>>(); 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)); ruleFuncList.Add(GetDelegateForRule(r, ruleParams));
} }
return (paramArray) => { 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); Func<object[], bool> isSuccess = (p) => ApplyOperation(resultList, operation);
var result = Helpers.ToResultTree(parentRule, resultList, isSuccess); var result = Helpers.ToResultTree(parentRule, resultList, isSuccess);
return result(paramArray); return result(paramArray);
@ -141,6 +184,11 @@ namespace RulesEngine
private bool ApplyOperation(IEnumerable<RuleResultTree> ruleResults, ExpressionType operation) private bool ApplyOperation(IEnumerable<RuleResultTree> ruleResults, ExpressionType operation)
{ {
if (ruleResults?.Any() != true)
{
return false;
}
switch (operation) switch (operation)
{ {
case ExpressionType.And: case ExpressionType.And:
@ -154,5 +202,36 @@ namespace RulesEngine
return false; 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); return _workflowRules.ContainsKey(workflowName);
} }
public List<string> GetAllWorkflowNames()
{
return _workflowRules.Keys.ToList();
}
/// <summary>Determines whether [contains compiled rules] [the specified workflow name].</summary> /// <summary>Determines whether [contains compiled rules] [the specified workflow name].</summary>
/// <param name="workflowName">Name of the workflow.</param> /// <param name="workflowName">Name of the workflow.</param>
/// <returns> /// <returns>
@ -59,16 +64,6 @@ namespace RulesEngine
_compileRules.Clear(); _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> /// <summary>Gets the work flow rules.</summary>
/// <param name="workflowName">Name of the workflow.</param> /// <param name="workflowName">Name of the workflow.</param>
/// <returns>WorkflowRules.</returns> /// <returns>WorkflowRules.</returns>

View File

@ -32,8 +32,6 @@ namespace RulesEngine
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ReSettings _reSettings; private readonly ReSettings _reSettings;
private readonly RulesCache _rulesCache = new RulesCache(); private readonly RulesCache _rulesCache = new RulesCache();
private MemoryCache _compiledParamsCache = new MemoryCache(new MemoryCacheOptions());
private readonly ParamCompiler _ruleParamCompiler;
private readonly RuleExpressionParser _ruleExpressionParser; private readonly RuleExpressionParser _ruleExpressionParser;
private readonly RuleCompiler _ruleCompiler; private readonly RuleCompiler _ruleCompiler;
private readonly ActionFactory _actionFactory; private readonly ActionFactory _actionFactory;
@ -57,8 +55,7 @@ namespace RulesEngine
_logger = logger ?? new NullLogger<RulesEngine>(); _logger = logger ?? new NullLogger<RulesEngine>();
_reSettings = reSettings ?? new ReSettings(); _reSettings = reSettings ?? new ReSettings();
_ruleExpressionParser = new RuleExpressionParser(_reSettings); _ruleExpressionParser = new RuleExpressionParser(_reSettings);
_ruleParamCompiler = new ParamCompiler(_reSettings, _ruleExpressionParser); _ruleCompiler = new RuleCompiler(new RuleExpressionBuilderFactory(_reSettings, _ruleExpressionParser),_reSettings, _logger);
_ruleCompiler = new RuleCompiler(new RuleExpressionBuilderFactory(_reSettings, _ruleExpressionParser), _logger);
_actionFactory = new ActionFactory(GetActionRegistry(_reSettings)); _actionFactory = new ActionFactory(GetActionRegistry(_reSettings));
} }
@ -173,13 +170,17 @@ namespace RulesEngine
} }
} }
public List<string> GetAllRegisteredWorkflowNames()
{
return _rulesCache.GetAllWorkflowNames();
}
/// <summary> /// <summary>
/// Clears the workflows. /// Clears the workflows.
/// </summary> /// </summary>
public void ClearWorkflows() public void ClearWorkflows()
{ {
_rulesCache.Clear(); _rulesCache.Clear();
ClearCompiledParamCache();
} }
/// <summary> /// <summary>
@ -192,7 +193,6 @@ namespace RulesEngine
{ {
_rulesCache.Remove(workflowName); _rulesCache.Remove(workflowName);
} }
ClearCompiledParamCache();
} }
/// <summary> /// <summary>
@ -239,9 +239,9 @@ namespace RulesEngine
if (workflowRules != null) if (workflowRules != null)
{ {
var dictFunc = new Dictionary<string, RuleFunc<RuleResultTree>>(); 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); _rulesCache.AddOrUpdateCompiledRule(compileRulesKey, dictFunc);
@ -257,42 +257,22 @@ namespace RulesEngine
private RuleFunc<RuleResultTree> CompileRule(string workflowName, string ruleName, RuleParameter[] ruleParameters) private RuleFunc<RuleResultTree> CompileRule(string workflowName, string ruleName, RuleParameter[] ruleParameters)
{ {
var rules = _rulesCache.GetRules(workflowName); var workflow = _rulesCache.GetWorkFlowRules(workflowName);
var currentRule = rules?.SingleOrDefault(c => c.RuleName == ruleName); 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) if (currentRule == null)
{ {
throw new ArgumentException($"Workflow `{workflowName}` does not contain any rule named `{ruleName}`"); 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, scopedParams);
{
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;
} }
@ -325,12 +305,6 @@ namespace RulesEngine
return key; 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() private IDictionary<string, Func<ActionBase>> GetDefaultActionRegistry()
{ {
return new Dictionary<string, Func<ActionBase>>{ return new Dictionary<string, Func<ActionBase>>{
@ -351,7 +325,7 @@ namespace RulesEngine
foreach (var ruleResult in ruleResultList?.Where(r => !r.IsSuccess)) foreach (var ruleResult in ruleResultList?.Where(r => !r.IsSuccess))
{ {
var errorMessage = ruleResult?.Rule?.ErrorMessage; var errorMessage = ruleResult?.Rule?.ErrorMessage;
if (errorMessage != null) if (string.IsNullOrWhiteSpace(ruleResult.ExceptionMessage) && errorMessage != null)
{ {
var errorParameters = Regex.Matches(errorMessage, ParamParseRegex); var errorParameters = Regex.Matches(errorMessage, ParamParseRegex);
@ -406,16 +380,6 @@ namespace RulesEngine
return errorMessage; return errorMessage;
} }
/// <summary>
/// Clears all compiledParams
/// </summary>
private void ClearCompiledParamCache()
{
_compiledParamsCache.Dispose();
_compiledParamsCache = new MemoryCache(new MemoryCacheOptions());
}
#endregion #endregion
} }
} }

View File

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

View File

@ -19,7 +19,7 @@ namespace RulesEngine.Validators
RuleFor(c => c.RuleName).NotEmpty().WithMessage(Constants.RULE_NAME_NULL_ERRMSG); RuleFor(c => c.RuleName).NotEmpty().WithMessage(Constants.RULE_NAME_NULL_ERRMSG);
//Nested expression check //Nested expression check
When(c => c.RuleExpressionType == null, () => { When(c => c.Operator != null, () => {
RuleFor(c => c.Operator) RuleFor(c => c.Operator)
.NotNull().WithMessage(Constants.OPERATOR_NULL_ERRMSG) .NotNull().WithMessage(Constants.OPERATOR_NULL_ERRMSG)
.Must(op => _nestedOperators.Any(x => x.ToString().Equals(op, StringComparison.OrdinalIgnoreCase))) .Must(op => _nestedOperators.Any(x => x.ToString().Equals(op, StringComparison.OrdinalIgnoreCase)))
@ -37,9 +37,8 @@ namespace RulesEngine.Validators
private void RegisterExpressionTypeRules() 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.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); 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); 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] [Theory]
[InlineData("rules2.json")] [InlineData("rules2.json")]
public async Task ExecuteRule_ManyInputs_ReturnsListOfRuleResultTree(string ruleFileName) public async Task ExecuteRule_ManyInputs_ReturnsListOfRuleResultTree(string ruleFileName)
@ -160,6 +183,7 @@ namespace RulesEngine.UnitTest
[Theory] [Theory]
[InlineData("rules2.json")] [InlineData("rules2.json")]
[Obsolete]
public async Task ExecuteRule_ReturnsListOfRuleResultTree_ResultMessage(string ruleFileName) public async Task ExecuteRule_ReturnsListOfRuleResultTree_ResultMessage(string ruleFileName)
{ {
var re = GetRulesEngine(ruleFileName); var re = GetRulesEngine(ruleFileName);
@ -400,20 +424,36 @@ namespace RulesEngine.UnitTest
[Theory] [Theory]
[InlineData("rules9.json")] [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(); dynamic input1 = new ExpandoObject();
input1.Data = new { TestProperty = "" }; input1.Data = new { TestProperty = "" };
input1.Boolean = false; input1.Boolean = false;
var utils = new TestInstanceUtils(); var utils = new TestInstanceUtils();
var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1)); var result = await re.ExecuteAllRulesAsync("inputWorkflow", new RuleParameter("input1", input1));
Assert.NotNull(result); Assert.NotNull(result);
Assert.IsType<List<RuleResultTree>>(result); Assert.StartsWith("Exception while parsing expression", result[1].ExceptionMessage);
Assert.All(result, c => Assert.False(c.IsSuccess)); }
[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] [Fact]
@ -424,7 +464,7 @@ namespace RulesEngine.UnitTest
Rules = new Rule[]{ Rules = new Rule[]{
new Rule { new Rule {
RuleName = "RuleWithLocalParam", RuleName = "RuleWithLocalParam",
LocalParams = new LocalParam[] { LocalParams = new List<LocalParam> {
new LocalParam { new LocalParam {
Name = "lp1", Name = "lp1",
Expression = "true" Expression = "true"

View File

@ -17,10 +17,10 @@ namespace RulesEngine.UnitTest
[Fact] [Fact]
public void RuleCompiler_NullCheck() public void RuleCompiler_NullCheck()
{ {
Assert.Throws<ArgumentNullException>(() => new RuleCompiler(null, null)); Assert.Throws<ArgumentNullException>(() => new RuleCompiler(null, null,null));
var reSettings = new ReSettings(); var reSettings = new ReSettings();
var parser = new RuleExpressionParser(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] [Fact]
@ -28,9 +28,9 @@ namespace RulesEngine.UnitTest
{ {
var reSettings = new ReSettings(); var reSettings = new ReSettings();
var parser = new RuleExpressionParser(reSettings); var parser = new RuleExpressionParser(reSettings);
var compiler = new RuleCompiler(new RuleExpressionBuilderFactory(reSettings, parser), new NullLogger<RuleCompiler>()); var compiler = new RuleCompiler(new RuleExpressionBuilderFactory(reSettings, parser),null, new NullLogger<RuleCompiler>());
Assert.Throws<ArgumentNullException>(() => compiler.CompileRule(null, null)); Assert.Throws<ArgumentNullException>(() => compiler.CompileRule(null, null,null));
Assert.Throws<ArgumentNullException>(() => compiler.CompileRule(null, new RuleParameter[] { 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> <TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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="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" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </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>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\RulesEngine\RulesEngine.csproj" /> <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\""
}
}
}
}
}
};
}
}
}