Rules Engine param Feature (#24)

* Feature Description
Rules Engine has a param (like ‘var’ in c#) feature support now, it makes authoring and troubleshooting of issues very easy. Now you can breakdown your bigger statements into smaller logical expressions as parameters within a rule definition.

* renaming param to localParam

* adding change log for local param

Co-authored-by: Deepak Joshi <dejosh@microsoft.com>
pull/41/head
joshidp 2020-07-22 21:04:15 +05:30 committed by GitHub
parent 170b494b66
commit a0a8938892
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 857 additions and 36 deletions

View File

@ -2,6 +2,9 @@
All notable changes to this project will be documented in this file.
## [2.1.0] - 18-05-2020
- Adding local param support to make expression authroing more intuitive.
## [2.0.0] - 18-05-2020
### Changed
- Interface simplified by removing redundant parameters in the IRulesEngine.

View File

@ -38,7 +38,20 @@ namespace RulesEngine.ExpressionBuilders
var binaryExpression = Expression.And(Expression.Constant(true), Expression.Constant(false));
var exceptionMessage = ex.Message;
return Helpers.ToResultTreeExpression(rule, null, binaryExpression, typeParamExpressions, ruleInputExp, exceptionMessage);
}
}
}
/// <summary>Builds the expression for rule parameter.</summary>
/// <param name="param">The parameter.</param>
/// <param name="typeParamExpressions">The type parameter expressions.</param>
/// <param name="ruleInputExp">The rule input exp.</param>
/// <returns>Expression.</returns>
internal override Expression BuildExpressionForRuleParam(LocalParam param, IEnumerable<ParameterExpression> typeParamExpressions, ParameterExpression ruleInputExp)
{
var config = new ParsingConfig { CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes) };
var e = DynamicExpressionParser.ParseLambda(config, typeParamExpressions.ToArray(), null, param.Expression);
return e.Body;
}
}
}

View File

@ -21,5 +21,12 @@ namespace RulesEngine.ExpressionBuilders
/// <param name="ruleInputExp">The rule input exp.</param>
/// <returns>Expression type</returns>
internal abstract Expression<Func<RuleInput, RuleResultTree>> BuildExpressionForRule(Rule rule, IEnumerable<ParameterExpression> typeParamExpressions, ParameterExpression ruleInputExp);
/// <summary>Builds the expression for rule parameter.</summary>
/// <param name="rule">The rule.</param>
/// <param name="typeParamExpressions">The type parameter expressions.</param>
/// <param name="ruleInputExp">The rule input exp.</param>
/// <returns>Expression.</returns>
internal abstract Expression BuildExpressionForRuleParam(LocalParam rule, IEnumerable<ParameterExpression> typeParamExpressions, ParameterExpression ruleInputExp);
}
}

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace RulesEngine.Models
{
/// <summary>
/// CompiledParam class.
/// </summary>
internal class CompiledParam
{
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>
/// The name.
/// </value>
internal string Name { get; set; }
/// <summary>
/// Gets or sets the value.
/// </summary>
/// <value>
/// The value.
/// </value>
internal Delegate Value { get; set; }
/// <summary>
/// Gets or sets the parameters.
/// </summary>
/// <value>
/// The parameters.
/// </value>
internal IEnumerable<RuleParameter> Parameters { get; set; }
}
}

View File

@ -16,7 +16,16 @@ namespace RulesEngine.Models
/// <value>
/// The compiled rules.
/// </value>
internal List<Delegate> CompiledRules { get; set; }
internal Delegate Rule { get; set; }
/// <summary>
/// Gets or sets the rule parameters.
/// </summary>
/// <value>
/// The rule parameters.
/// </value>
internal CompiledRuleParam CompiledParameters { get; set; }
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace RulesEngine.Models
{
/// <summary>Class CompiledRule.</summary>
internal class CompiledRuleParam
{
/// <summary>
/// Gets or sets the compiled rules.
/// </summary>
/// <value>
/// The compiled rules.
/// </value>
internal string Name { get; set; }
/// <summary>Gets or sets the rule parameters.</summary>
/// <value>The rule parameters.</value>
internal IEnumerable<CompiledParam> CompiledParameters { get; set; }
/// <summary>
/// Gets or sets the rule parameters.
/// </summary>
/// <value>
/// The rule parameters.
/// </value>
internal IEnumerable<RuleParameter> RuleParameters { get; set; }
}
}

View File

@ -0,0 +1,25 @@
using Newtonsoft.Json;
namespace RulesEngine.Models
{
/// <summary>Class Param.
/// Implements the <see cref="RulesEngine.Models.Rule" /></summary>
public class LocalParam
{
/// <summary>
/// Gets or sets the name of the rule.
/// </summary>
/// <value>
/// The name of the rule.
/// </value>
[JsonProperty, JsonRequired]
public string Name { get; private set; }
/// <summary>
/// Gets or Sets the lambda expression.
/// </summary>
[JsonProperty, JsonRequired]
public string Expression { get; private set; }
}
}

View File

@ -5,6 +5,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace RulesEngine.Models
{
@ -70,12 +71,27 @@ namespace RulesEngine.Models
/// </value>
public List<Rule> Rules { get; set; }
/// <summary>
/// Gets the parameters.
/// </summary>
/// <value>
/// The parameters.
/// </value>
[JsonProperty]
public IEnumerable<LocalParam> LocalParams { get; private set; }
/// <summary>
/// Gets or Sets the lambda expression.
/// </summary>
public string Expression { get; set; }
/// <summary>
/// Gets or sets the success event.
/// </summary>
/// <value>
/// The success event.
/// </value>
public string SuccessEvent { get; set; }
}

View File

@ -47,6 +47,14 @@ namespace RulesEngine.Models
/// </summary>
public string ExceptionMessage { get; set; }
/// <summary>
/// Gets or sets the rule evaluated parameters.
/// </summary>
/// <value>
/// The rule evaluated parameters.
/// </value>
public IEnumerable<RuleParameter> RuleEvaluatedParams { get; set; }
/// <summary>
/// This method will return all the error and warning messages to caller
/// </summary>

View File

@ -17,10 +17,13 @@ namespace RulesEngine.Models
/// </summary>
public string WorkflowName { get; set; }
public List<string> WorkflowRulesToInject { get; set; }
/// <summary>Gets or sets the workflow rules to inject.</summary>
/// <value>The workflow rules to inject.</value>
public IEnumerable<string> WorkflowRulesToInject { get; set; }
/// <summary>
/// list of rules.
/// </summary>
public List<Rule> Rules { get; set; }
public IEnumerable<Rule> Rules { get; set; }
}
}

View File

@ -0,0 +1,86 @@
using RulesEngine.Models;
using System;
using System.Collections.Concurrent;
using System.Linq;
namespace RulesEngine
{
/// <summary>Maintains the cache of evaludated param.</summary>
internal class ParamCache<T> where T : class
{
/// <summary>
/// The compile rules
/// </summary>
private readonly ConcurrentDictionary<string, T> _evaluatedParams = new ConcurrentDictionary<string, T>();
/// <summary>
/// <para></para>
/// <para>Determines whether the specified parameter key name contains parameters.
/// </para>
/// </summary>
/// <param name="paramKeyName">Name of the parameter key.</param>
/// <returns>
/// <c>true</c> if the specified parameter key name contains parameters; otherwise, <c>false</c>.</returns>
public bool ContainsParams(string paramKeyName)
{
return _evaluatedParams.ContainsKey(paramKeyName);
}
/// <summary>Adds the or update evaluated parameter.</summary>
/// <param name="paramKeyName">Name of the parameter key.</param>
/// <param name="ruleParameters">The rule parameters.</param>
public void AddOrUpdateParams(string paramKeyName, T ruleParameters)
{
_evaluatedParams.AddOrUpdate(paramKeyName, ruleParameters, (k, v) => v);
}
/// <summary>Clears this instance.</summary>
public void Clear()
{
_evaluatedParams.Clear();
}
/// <summary>Gets the evaluated parameters.</summary>
/// <param name="paramKeyName">Name of the parameter key.</param>
/// <returns>Delegate[].</returns>
public T GetParams(string paramKeyName)
{
return _evaluatedParams[paramKeyName];
}
/// <summary>Gets the evaluated parameters cache key.</summary>
/// <param name="workflowName">Name of the workflow.</param>
/// <param name="rule">The rule.</param>
/// <returns>Cache key.</returns>
public string GetCompiledParamsCacheKey(string workflowName, Rule rule)
{
if (rule == null)
{
return string.Empty;
}
else
{
if (rule?.LocalParams == null)
{
return $"Compiled_{workflowName}_{rule.RuleName}";
}
return $"Compiled_{workflowName}_{rule.RuleName}_{string.Join("_", rule?.LocalParams.Select(r => r?.Name))}";
}
}
/// <summary>Removes the specified workflow name.</summary>
/// <param name="workflowName">Name of the workflow.</param>
public void RemoveCompiledParams(string paramKeyName)
{
if (_evaluatedParams.TryRemove(paramKeyName, out T ruleParameters))
{
var compiledKeysToRemove = _evaluatedParams.Keys.Where(key => key.StartsWith(paramKeyName));
foreach (var key in compiledKeysToRemove)
{
_evaluatedParams.TryRemove(key, out T val);
}
}
}
}
}

View File

@ -0,0 +1,148 @@
using Microsoft.Extensions.Logging;
using RulesEngine.Models;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
using System.Linq;
namespace RulesEngine
{
/// <summary>
/// Rule param compilers
/// </summary>
internal class ParamCompiler
{
/// <summary>
/// The expression builder factory
/// </summary>
private readonly RuleExpressionBuilderFactory _expressionBuilderFactory;
/// <summary>
/// The logger
/// </summary>
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="ParamCompiler"/> class.
/// </summary>
/// <param name="expressionBuilderFactory">The expression builder factory.</param>
/// <exception cref="ArgumentNullException">expressionBuilderFactory</exception>
internal ParamCompiler(RuleExpressionBuilderFactory expressionBuilderFactory, ILogger logger)
{
if (expressionBuilderFactory == null)
{
throw new ArgumentNullException($"{nameof(expressionBuilderFactory)} can't be null.");
}
if (logger == null)
{
throw new ArgumentNullException($"{nameof(logger)} can't be null.");
}
_logger = logger;
_expressionBuilderFactory = expressionBuilderFactory;
}
/// <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 CompiledRuleParam CompileParamsExpression(Rule rule, IEnumerable<RuleParameter> ruleParams)
{
CompiledRuleParam compiledRuleParam = null;
if (rule.LocalParams != null)
{
var compiledParameters = new List<CompiledParam>();
var evaluatedParameters = new List<RuleParameter>();
foreach (var param in rule.LocalParams)
{
IEnumerable<ParameterExpression> typeParameterExpressions = GetParameterExpression(ruleParams.ToArray()).ToList(); // calling ToList to avoid multiple calls this the method for nested rule scenario.
ParameterExpression ruleInputExp = Expression.Parameter(typeof(RuleInput), nameof(RuleInput));
var ruleParamExpression = GetExpressionForRuleParam(param, typeParameterExpressions, ruleInputExp);
var lambdaParameterExps = new List<ParameterExpression>(typeParameterExpressions) { ruleInputExp };
var expression = Expression.Lambda(ruleParamExpression, lambdaParameterExps);
var compiledParam = expression.Compile();
compiledParameters.Add(new CompiledParam { Name = param.Name, Value = compiledParam, Parameters = evaluatedParameters });
var evaluatedParam = this.EvaluateCompiledParam(param.Name, compiledParam, ruleParams);
ruleParams = ruleParams.Concat(new List<RuleParameter> { evaluatedParam });
evaluatedParameters.Add(evaluatedParam);
}
compiledRuleParam = new CompiledRuleParam { Name = rule.RuleName, CompiledParameters = compiledParameters, RuleParameters = evaluatedParameters };
}
return compiledRuleParam;
}
/// <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, Delegate compiledParam, IEnumerable<RuleParameter> ruleParams)
{
var inputs = ruleParams.Select(c => c.Value);
var result = compiledParam.DynamicInvoke(new List<object>(inputs) { new RuleInput() }.ToArray());
return new RuleParameter(paramName, result);
}
// <summary>
/// Gets the parameter expression.
/// </summary>
/// <param name="ruleParams">The types.</param>
/// <returns></returns>
/// <exception cref="ArgumentException">
/// types
/// or
/// type
/// </exception>
private IEnumerable<ParameterExpression> GetParameterExpression(params RuleParameter[] ruleParams)
{
foreach (var ruleParam in ruleParams)
{
if (ruleParam == null)
{
throw new ArgumentException($"{nameof(ruleParam)} can't be null.");
}
yield return Expression.Parameter(ruleParam.Type, ruleParam.Name);
}
}
/// <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 Expression GetExpressionForRuleParam(LocalParam param, IEnumerable<ParameterExpression> typeParameterExpressions, ParameterExpression ruleInputExp)
{
return BuildExpression(param, typeParameterExpressions, ruleInputExp);
}
/// <summary>
/// Builds the expression.
/// </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>
/// <exception cref="InvalidOperationException"></exception>
private Expression BuildExpression(LocalParam param, IEnumerable<ParameterExpression> typeParameterExpressions, ParameterExpression ruleInputExp)
{
var ruleExpressionBuilder = _expressionBuilderFactory.RuleGetExpressionBuilder(RuleExpressionType.LambdaExpression);
var expression = ruleExpressionBuilder.BuildExpressionForRuleParam(param, typeParameterExpressions, ruleInputExp);
return expression;
}
}
}

View File

@ -26,6 +26,9 @@ namespace RulesEngine
/// </summary>
private readonly RuleExpressionBuilderFactory _expressionBuilderFactory;
/// <summary>
/// The logger
/// </summary>
private readonly ILogger _logger;
/// <summary>

View File

@ -9,42 +9,68 @@ using System.Linq;
namespace RulesEngine
{
/// <summary>Class RulesCache.</summary>
internal class RulesCache
{
private ConcurrentDictionary<string, CompiledRule> _compileRules = new ConcurrentDictionary<string, CompiledRule>();
/// <summary>The compile rules</summary>
private ConcurrentDictionary<string, IEnumerable<CompiledRule>> _compileRules = new ConcurrentDictionary<string, IEnumerable<CompiledRule>>();
/// <summary>The workflow rules</summary>
private ConcurrentDictionary<string, WorkflowRules> _workflowRules = new ConcurrentDictionary<string, WorkflowRules>();
/// <summary>Determines whether [contains workflow rules] [the specified workflow name].</summary>
/// <param name="workflowName">Name of the workflow.</param>
/// <returns>
/// <c>true</c> if [contains workflow rules] [the specified workflow name]; otherwise, <c>false</c>.</returns>
public bool ContainsWorkflowRules(string workflowName)
{
return _workflowRules.ContainsKey(workflowName);
}
/// <summary>Determines whether [contains compiled rules] [the specified workflow name].</summary>
/// <param name="workflowName">Name of the workflow.</param>
/// <returns>
/// <c>true</c> if [contains compiled rules] [the specified workflow name]; otherwise, <c>false</c>.</returns>
public bool ContainsCompiledRules(string workflowName)
{
return _compileRules.ContainsKey(workflowName);
}
/// <summary>Adds the or update workflow rules.</summary>
/// <param name="workflowName">Name of the workflow.</param>
/// <param name="rules">The rules.</param>
public void AddOrUpdateWorkflowRules(string workflowName, WorkflowRules rules)
{
_workflowRules.AddOrUpdate(workflowName, rules, (k, v) => rules);
}
public void AddOrUpdateCompiledRule(string compiledRuleKey, CompiledRule compiledRule)
/// <summary>Adds the or update compiled rule.</summary>
/// <param name="compiledRuleKey">The compiled rule key.</param>
/// <param name="compiledRule">The compiled rule.</param>
public void AddOrUpdateCompiledRule(string compiledRuleKey, IEnumerable<CompiledRule> compiledRule)
{
_compileRules.AddOrUpdate(compiledRuleKey, compiledRule, (k, v) => compiledRule);
}
/// <summary>Clears this instance.</summary>
public void Clear()
{
_workflowRules.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)
{
return _workflowRules[workflowName].Rules;
}
/// <summary>Gets the work flow rules.</summary>
/// <param name="workflowName">Name of the workflow.</param>
/// <returns>WorkflowRules.</returns>
/// <exception cref="Exception">Could not find injected Workflow: {wfname}</exception>
public WorkflowRules GetWorkFlowRules(string workflowName)
{
_workflowRules.TryGetValue(workflowName, out var workflowRules);
@ -64,7 +90,8 @@ namespace RulesEngine
{
throw new Exception($"Could not find injected Workflow: {wfname}");
}
workflowRules.Rules.AddRange(injectedWorkflow.Rules);
workflowRules.Rules.ToList().AddRange(injectedWorkflow.Rules);
}
}
@ -72,11 +99,57 @@ namespace RulesEngine
}
}
public CompiledRule GetCompiledRules(string compiledRulesKey)
/// <summary>Gets the rules cache key.</summary>
/// <param name="workflowName">Name of the workflow.</param>
/// <returns>System.String.</returns>
/// <exception cref="Exception">Could not find injected Workflow: {wfname}</exception>
public string GetRulesCacheKey(string workflowName)
{
_workflowRules.TryGetValue(workflowName, out var workflowRules);
if (workflowRules == null) return string.Empty;
else
{
var ruleCacheKey = workflowName + "_";
if (workflowRules.WorkflowRulesToInject?.Any() == true)
{
if (workflowRules.Rules == null)
{
workflowRules.Rules = new List<Rule>();
}
foreach (string wfname in workflowRules.WorkflowRulesToInject)
{
var injectedWorkflow = GetWorkFlowRules(wfname);
if (injectedWorkflow == null)
{
throw new Exception($"Could not find injected Workflow: {wfname}");
}
var lstRuleNames = injectedWorkflow.Rules.Select(s => s.RuleName)?.ToList();
lstRuleNames?.Add(workflowName);
ruleCacheKey += string.Join("_", lstRuleNames);
}
}
if (workflowRules.Rules != null)
{
var lstRuleNames = workflowRules.Rules.Select(s => s.RuleName)?.ToList();
ruleCacheKey += string.Join("_", lstRuleNames);
}
return ruleCacheKey;
}
}
/// <summary>Gets the compiled rules.</summary>
/// <param name="compiledRulesKey">The compiled rules key.</param>
/// <returns>CompiledRule.</returns>
public IEnumerable<CompiledRule> GetCompiledRules(string compiledRulesKey)
{
return _compileRules[compiledRulesKey];
}
/// <summary>Removes the specified workflow name.</summary>
/// <param name="workflowName">Name of the workflow.</param>
public void Remove(string workflowName)
{
if (_workflowRules.TryRemove(workflowName, out WorkflowRules workflowObj))
@ -84,7 +157,7 @@ namespace RulesEngine
var compiledKeysToRemove = _compileRules.Keys.Where(key => key.StartsWith(workflowName));
foreach (var key in compiledKeysToRemove)
{
_compileRules.TryRemove(key, out CompiledRule val);
_compileRules.TryRemove(key, out IEnumerable<CompiledRule> val);
}
}
}

View File

@ -1,27 +1,60 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using FluentValidation;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using RulesEngine.Exceptions;
using RulesEngine.HelperFunctions;
using RulesEngine.Interfaces;
using RulesEngine.Models;
using RulesEngine.Validators;
using RulesEngine.Exceptions;
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using FluentValidation;
using System.Text.RegularExpressions;
namespace RulesEngine
{
/// <summary>
///
/// </summary>
/// <seealso cref="RulesEngine.Interfaces.IRulesEngine" />
public class RulesEngine : IRulesEngine
{
#region Variables
/// <summary>
/// The logger
/// </summary>
private readonly ILogger _logger;
/// <summary>
/// The re settings
/// </summary>
private readonly ReSettings _reSettings;
/// <summary>
/// The rules cache
/// </summary>
private readonly RulesCache _rulesCache = new RulesCache();
/// <summary>
/// The parameters cache
/// </summary>
private readonly ParamCache<CompiledRuleParam> _compiledParamsCache = new ParamCache<CompiledRuleParam>();
/// <summary>
/// The rule parameter compiler
/// </summary>
private readonly ParamCompiler ruleParamCompiler;
/// <summary>
/// The parameter parse regex
/// </summary>
private const string ParamParseRegex = "(\\$\\(.*?\\))";
#endregion
#region Constructor
@ -40,6 +73,7 @@ namespace RulesEngine
{
_logger = logger ?? new NullLogger<RulesEngine>();
_reSettings = reSettings ?? new ReSettings();
ruleParamCompiler = new ParamCompiler(new RuleExpressionBuilderFactory(_reSettings), _logger);
}
#endregion
@ -81,6 +115,11 @@ namespace RulesEngine
#region Private Methods
/// <summary>
/// Adds the workflow.
/// </summary>
/// <param name="workflowRules">The workflow rules.</param>
/// <exception cref="RuleValidationException"></exception>
public void AddWorkflow(params WorkflowRules[] workflowRules)
{
try
@ -98,11 +137,18 @@ namespace RulesEngine
}
}
/// <summary>
/// Clears the workflows.
/// </summary>
public void ClearWorkflows()
{
_rulesCache.Clear();
}
/// <summary>
/// Removes the workflow.
/// </summary>
/// <param name="workflowNames">The workflow names.</param>
public void RemoveWorkflow(params string[] workflowNames)
{
foreach (var workflowName in workflowNames)
@ -122,7 +168,7 @@ namespace RulesEngine
{
List<RuleResultTree> result;
if (RegisterRule(workflowName, ruleParams))
if (RegisterCompiledRule(workflowName, ruleParams))
{
result = ExecuteRuleByWorkflow(workflowName, ruleParams);
}
@ -135,31 +181,45 @@ namespace RulesEngine
return result;
}
/// <summary>
/// This will compile the rules and store them to dictionary
/// </summary>
/// <typeparam name="T">type of entity</typeparam>
/// <param name="workflowName">workflow name</param>
/// <returns>bool result</returns>
private bool RegisterRule(string workflowName, params RuleParameter[] ruleParams)
/// <param name="ruleParams">The rule parameters.</param>
/// <returns>
/// bool result
/// </returns>
private bool RegisterCompiledRule(string workflowName, params RuleParameter[] ruleParams)
{
string compileRulesKey = GetCompileRulesKey(workflowName, ruleParams);
string compileRulesKey = _rulesCache.GetRulesCacheKey(workflowName);
if (_rulesCache.ContainsCompiledRules(compileRulesKey))
return true;
var workflowRules = _rulesCache.GetWorkFlowRules(workflowName);
if (workflowRules != null)
{
var lstFunc = new List<Delegate>();
var lstFunc = new List<CompiledRule>();
var ruleCompiler = new RuleCompiler(new RuleExpressionBuilderFactory(_reSettings), _logger);
foreach (var rule in _rulesCache.GetRules(workflowName))
{
RuleCompiler ruleCompiler = new RuleCompiler(new RuleExpressionBuilderFactory(_reSettings), _logger);
lstFunc.Add(ruleCompiler.CompileRule(rule, ruleParams));
var compiledParamsKey = _compiledParamsCache.GetCompiledParamsCacheKey(workflowName, rule);
CompiledRuleParam compiledRuleParam;
if (_compiledParamsCache.ContainsParams(compiledParamsKey))
{
compiledRuleParam = _compiledParamsCache.GetParams(compiledParamsKey);
}
else
{
compiledRuleParam = ruleParamCompiler.CompileParamsExpression(rule, ruleParams);
_compiledParamsCache.AddOrUpdateParams(compiledParamsKey, compiledRuleParam);
}
var updatedRuleParams = compiledRuleParam != null ? ruleParams?.Concat(compiledRuleParam?.RuleParameters) : ruleParams;
var compiledRule = ruleCompiler.CompileRule(rule, updatedRuleParams?.ToArray());
lstFunc.Add(new CompiledRule { Rule = compiledRule, CompiledParameters = compiledRuleParam });
}
_rulesCache.AddOrUpdateCompiledRule(compileRulesKey, new CompiledRule() { CompiledRules = lstFunc });
_rulesCache.AddOrUpdateCompiledRule(compileRulesKey, lstFunc);
_logger.LogTrace($"Rules has been compiled for the {workflowName} workflow and added to dictionary");
return true;
}
@ -169,6 +229,7 @@ namespace RulesEngine
}
}
private static string GetCompileRulesKey(string workflowName, RuleParameter[] ruleParams)
{
return $"{workflowName}-" + String.Join("-", ruleParams.Select(c => c.Type.Name));
@ -180,20 +241,95 @@ namespace RulesEngine
/// <param name="workflowName"></param>
/// <param name="ruleParams"></param>
/// <returns>list of rule result set</returns>
private List<RuleResultTree> ExecuteRuleByWorkflow(string workflowName, RuleParameter[] ruleParams)
private List<RuleResultTree> ExecuteRuleByWorkflow(string workflowName, RuleParameter[] ruleParameters)
{
_logger.LogTrace($"Compiled rules found for {workflowName} workflow and executed");
List<RuleResultTree> result = new List<RuleResultTree>();
var compileRulesKey = GetCompileRulesKey(workflowName, ruleParams);
var inputs = ruleParams.Select(c => c.Value);
foreach (var compiledRule in _rulesCache.GetCompiledRules(compileRulesKey).CompiledRules)
string compileRulesKey = _rulesCache.GetRulesCacheKey(workflowName);
foreach (var compiledRule in _rulesCache.GetCompiledRules(compileRulesKey))
{
result.Add(compiledRule.DynamicInvoke(new List<object>(inputs) { new RuleInput() }.ToArray()) as RuleResultTree);
IEnumerable<RuleParameter> evaluatedRuleParams = new List<RuleParameter>(ruleParameters);
if (compiledRule?.CompiledParameters?.CompiledParameters != null)
{
foreach (var compiledParam in compiledRule?.CompiledParameters?.CompiledParameters)
{
var evaluatedParam = ruleParamCompiler.EvaluateCompiledParam(compiledParam.Name, compiledParam.Value, evaluatedRuleParams);
evaluatedRuleParams = evaluatedRuleParams.Concat(new List<RuleParameter> { evaluatedParam });
}
}
var inputs = evaluatedRuleParams.Select(c => c.Value);
var resultTree = compiledRule.Rule.DynamicInvoke(new List<object>(inputs) { new RuleInput() }.ToArray()) as RuleResultTree;
resultTree.RuleEvaluatedParams = evaluatedRuleParams;
result.Add(resultTree);
}
FormatErrorMessages(result?.Where(r => !r.IsSuccess));
return result;
}
/// <summary>
/// The result
/// </summary>
/// <param name="result">The result.</param>
/// <returns>Updated error message.</returns>
private IEnumerable<RuleResultTree> FormatErrorMessages(IEnumerable<RuleResultTree> result)
{
foreach (var error in result)
{
var errorParameters = Regex.Matches(error?.Rule?.ErrorMessage, ParamParseRegex);
var errorMessage = error?.Rule?.ErrorMessage;
var evaluatedParams = error?.RuleEvaluatedParams;
foreach (var param in errorParameters)
{
var paramVal = param?.ToString();
var property = paramVal?.Substring(2, paramVal.Length - 3);
if (property?.Split('.')?.Count() > 1)
{
var typeName = property?.Split('.')?[0];
var propertyName = property?.Split('.')?[1];
errorMessage = UpdateErrorMessage(errorMessage, evaluatedParams, property, typeName, propertyName);
}
else
{
var arrParams = evaluatedParams?.Select(c => new { c.Name, c.Value });
var model = arrParams?.Where(a => string.Equals(a.Name, property))?.FirstOrDefault();
var value = model?.Value != null ? JsonConvert.SerializeObject(model?.Value) : null;
errorMessage = errorMessage?.Replace($"$({property})", value ?? $"$({property})");
}
}
error.Rule.ErrorMessage = errorMessage;
}
return result;
}
/// <summary>
/// Updates the error message.
/// </summary>
/// <param name="errorMessage">The error message.</param>
/// <param name="evaluatedParams">The evaluated parameters.</param>
/// <param name="property">The property.</param>
/// <param name="typeName">Name of the type.</param>
/// <param name="propertyName">Name of the property.</param>
/// <returns>Updated error message.</returns>
private static string UpdateErrorMessage(string errorMessage, IEnumerable<RuleParameter> evaluatedParams, string property, string typeName, string propertyName)
{
var arrParams = evaluatedParams?.Select(c => new { c.Name, c.Value });
var model = arrParams?.Where(a => string.Equals(a.Name, typeName))?.FirstOrDefault();
if (model != null)
{
var modelJson = JsonConvert.SerializeObject(model?.Value);
var jObj = JObject.Parse(modelJson);
JToken jToken = null;
var val = jObj?.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out jToken);
errorMessage = errorMessage.Replace($"$({property})", jToken != null ? jToken?.ToString() : $"({property})");
}
return errorMessage;
}
#endregion
}
}

View File

@ -2,22 +2,35 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Version>2.0.1</Version>
<Version>2.1.0</Version>
<Copyright>Copyright (c) Microsoft Corporation.</Copyright>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://github.com/microsoft/RulesEngine</PackageProjectUrl>
<Authors>Dishant Munjal, Abbas Cyclewala, Yogesh Prajapati</Authors>
<Authors>Dishant Munjal, Abbas Cyclewala, Yogesh Prajapati, Deepak Joshi, Pritam Deshmukh, Chetanya Kumar</Authors>
<Description>Rules Engine is a package for abstracting business logic/rules/policies out of the system. This works in a very simple way by giving you an ability to put your rules in a store outside the core logic of the system thus ensuring that any change in rules doesn't affect the core system.</Description>
<PackageTags>BRE, Rules Engine, Abstraction</PackageTags>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<Optimize>true</Optimize>
</PropertyGroup>
<PropertyGroup Label="SourceLink">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="8.4.0" />
<PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.0.18" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.1.2" />
</ItemGroup>
<ItemGroup>

View File

@ -1,5 +1,5 @@
{
"sdk": {
"version": "3.1.101"
"version": "3.1.301"
}
}

View File

@ -13,6 +13,7 @@ using System.IO;
using System.Linq;
using Xunit;
using Newtonsoft.Json.Converters;
using RulesEngine.HelperFunctions;
namespace RulesEngine.UnitTest
{
@ -32,7 +33,7 @@ namespace RulesEngine.UnitTest
public void RulesEngine_InjectedRules_ReturnsListOfRuleResultTree(string ruleFileName)
{
var re = GetRulesEngine(ruleFileName);
dynamic input1 = GetInput1();
dynamic input2 = GetInput2();
dynamic input3 = GetInput3();
@ -67,7 +68,7 @@ namespace RulesEngine.UnitTest
dynamic input2 = GetInput2();
dynamic input3 = GetInput3();
var result = re.ExecuteRule("inputWorkflow",input1);
var result = re.ExecuteRule("inputWorkflow", input1);
Assert.NotNull(result);
Assert.IsType<List<RuleResultTree>>(result);
}
@ -153,6 +154,37 @@ namespace RulesEngine.UnitTest
Assert.IsType<List<RuleResultTree>>(result);
}
/// <summary>
/// Ruleses the engine execute rule for nested rull parameters returns success.
/// </summary>
/// <param name="ruleFileName">Name of the rule file.</param>
/// <exception cref="Exception">Rules not found.</exception>
[Theory]
[InlineData("rules4.json")]
public void RulesEngine_Execute_Rule_For_Nested_Rull_Params_Returns_Success(string ruleFileName)
{
dynamic[] inputs = GetInputs4();
var ruleParams = new List<RuleParameter>();
for (int i = 0; i < inputs.Length; i++)
{
var input = inputs[i];
var obj = Utils.GetTypedObject(input);
ruleParams.Add(new RuleParameter($"input{i + 1}", obj));
}
var files = Directory.GetFiles(Directory.GetCurrentDirectory(), ruleFileName, SearchOption.AllDirectories);
if (files == null || files.Length == 0)
throw new Exception("Rules not found.");
var fileData = File.ReadAllText(files[0]);
var bre = new RulesEngine(JsonConvert.DeserializeObject<WorkflowRules[]>(fileData), null);
var result = bre.ExecuteRule("inputWorkflow", ruleParams?.ToArray()); ;
var ruleResult = result?.FirstOrDefault(r => string.Equals(r.Rule.RuleName, "GiveDiscount10", StringComparison.OrdinalIgnoreCase));
Assert.True(ruleResult.IsSuccess);
}
private RulesEngine CreateRulesEngine(WorkflowRules workflow)
{
var json = JsonConvert.SerializeObject(workflow);
@ -172,7 +204,7 @@ namespace RulesEngine.UnitTest
var injectWorkflowStr = JsonConvert.SerializeObject(injectWorkflow);
var mockLogger = new Mock<ILogger>();
return new RulesEngine(new string[] { data, injectWorkflowStr}, mockLogger.Object);
return new RulesEngine(new string[] { data, injectWorkflowStr }, mockLogger.Object);
}
@ -196,6 +228,44 @@ namespace RulesEngine.UnitTest
var telemetryInfo = "{\"noOfVisitsPerMonth\": 10,\"percentageOfBuyingToVisit\": 15}";
return JsonConvert.DeserializeObject<ExpandoObject>(telemetryInfo, converter);
}
/// <summary>
/// Gets the inputs.
/// </summary>
/// <returns>
/// The inputs.
/// </returns>
private static dynamic[] GetInputs4()
{
var basicInfo = "{\"name\": \"Dishant\",\"email\": \"dishantmunjal@live.com\",\"creditHistory\": \"good\",\"country\": \"canada\",\"loyalityFactor\": 3,\"totalPurchasesToDate\": 70000}";
var orderInfo = "{\"totalOrders\": 50,\"recurringItems\": 2}";
var telemetryInfo = "{\"noOfVisitsPerMonth\": 10,\"percentageOfBuyingToVisit\": 15}";
var laborCategoriesInput = "[{\"country\": \"india\", \"loyalityFactor\": 2, \"totalPurchasesToDate\": 20000}]";
var currentLaborCategoryInput = "{\"CurrentLaborCategoryProp\":\"TestVal2\"}";
var converter = new ExpandoObjectConverter();
var settings = new JsonSerializerSettings
{
ContractResolver = new PrivateSetterContractResolver()
};
dynamic input1 = JsonConvert.DeserializeObject<List<RuleTestClass>>(laborCategoriesInput, settings);
dynamic input2 = JsonConvert.DeserializeObject<ExpandoObject>(currentLaborCategoryInput, converter);
dynamic input3 = JsonConvert.DeserializeObject<ExpandoObject>(telemetryInfo, converter);
dynamic input4 = JsonConvert.DeserializeObject<ExpandoObject>(basicInfo, converter);
dynamic input5 = JsonConvert.DeserializeObject<ExpandoObject>(orderInfo, converter);
var inputs = new dynamic[]
{
input1,
input2,
input3,
input4,
input5
};
return inputs;
}
}
}

View File

@ -0,0 +1,32 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
namespace RulesEngine.UnitTest
{
/// <summary>Class PrivateSetterContractResolver.
/// Implements the <see cref="Newtonsoft.Json.Serialization.DefaultContractResolver" /></summary>
public class PrivateSetterContractResolver : DefaultContractResolver
{
/// <summary>Creates a <see cref="T:Newtonsoft.Json.Serialization.JsonProperty" /> for the given <see cref="T:System.Reflection.MemberInfo">MemberInfo</see>.</summary>
/// <param name="member">The member to create a <see cref="T:Newtonsoft.Json.Serialization.JsonProperty" /> for.</param>
/// <param name="memberSerialization">The member's parent <see cref="T:Newtonsoft.Json.MemberSerialization" />.</param>
/// <returns>A created <see cref="T:Newtonsoft.Json.Serialization.JsonProperty" /> for the given <see cref="T:System.Reflection.MemberInfo">MemberInfo</see>.</returns>
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var jsonProperty = base.CreateProperty(member, memberSerialization);
if (!jsonProperty.Writable)
{
if (member is PropertyInfo propertyInfo)
{
jsonProperty.Writable = propertyInfo.GetSetMethod(true) != null;
}
}
return jsonProperty;
}
}
}

View File

@ -0,0 +1,40 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace RulesEngine.UnitTest
{
/// <summary>
/// Class RuleTestClass.
/// </summary>
public class RuleTestClass
{
/// <summary>
/// Gets the country.
/// </summary>
/// <value>
/// The country.
/// </value>
[JsonProperty("country")]
public string Country { get; private set; }
/// <summary>
/// Gets the loyality factor.
/// </summary>
/// <value>
/// The loyality factor.
/// </value>
[JsonProperty("loyalityFactor")]
public int LoyalityFactor { get; private set; }
/// <summary>
/// Gets the total purchases to date.
/// </summary>
/// <value>
/// The total purchases to date.
/// </value>
[JsonProperty("totalPurchasesToDate")]
public int TotalPurchasesToDate { get; private set; }
}
}

View File

@ -24,6 +24,9 @@
<None Update="TestData\rules1.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestData\rules4.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestData\rules3.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

View File

@ -0,0 +1,67 @@
[
{
"WorkflowName": "inputWorkflow",
"Rules": [
{
"RuleName": "GiveDiscount10",
"SuccessEvent": "10",
"ErrorMessage": "One or more adjust rules failed, with loyalityFactor : $(model1.loyalityFactor), country : $(model1.country), totalPurchasesToDate : $(model1.totalPurchasesToDate), model2 : $(model2)",
"ErrorType": "Error",
"localParams": [
{
"Name": "model1",
"Expression": "input1.FirstOrDefault(country.Equals(\"india\", StringComparison.OrdinalIgnoreCase))"
},
{
"Name": "model2",
"Expression": "model1.country == \"india\""
}
],
"RuleExpressionType": "LambdaExpression",
"Expression": "model1.country == \"india\" AND model1.loyalityFactor <= 2 AND model1.totalPurchasesToDate >= 5000 AND model2"
},
{
"RuleName": "GiveDiscount100",
"SuccessEvent": "10",
"ErrorMessage": "One or more adjust rules failed, with loyalityFactor : $(model1.loyalityFactor), country : $(model1.country), totalPurchasesToDate : $(model1.totalPurchasesToDate), model2 : $(model2)",
"ErrorType": "Error",
"localParams": [
{
"Name": "model1",
"Expression": "input1.FirstOrDefault(country.Equals(\"india\", StringComparison.OrdinalIgnoreCase))"
},
{
"Name": "model2",
"Expression": "model1.country == \"india\""
}
],
"RuleExpressionType": "LambdaExpression",
"Expression": "model1.country == \"india\" AND model1.loyalityFactor < 0 AND model1.totalPurchasesToDate >= 5000 AND model2"
},
{
"RuleName": "GiveDiscount25",
"SuccessEvent": "25",
"ErrorMessage": "One or more adjust rules failed, country : $(input4.country), loyalityFactor : $(input4.loyalityFactor), totalPurchasesToDate : $(input4.totalPurchasesToDate), totalOrders : $(input5.totalOrders), noOfVisitsPerMonth : $(input30.noOfVisitsPerMonth)",
"ErrorType": "Error",
"RuleExpressionType": "LambdaExpression",
"Expression": "input4.country == \"india\" AND input4.loyalityFactor >= 2 AND input4.totalPurchasesToDate <= 10 AND input5.totalOrders > 2 AND input3.noOfVisitsPerMonth > 5"
},
{
"RuleName": "GiveDiscount30",
"SuccessEvent": "30",
"ErrorMessage": "One or more adjust rules failed.",
"ErrorType": "Error",
"RuleExpressionType": "LambdaExpression",
"Expression": "input4.loyalityFactor > 30 AND input4.totalPurchasesToDate >= 50000 AND input4.totalPurchasesToDate <= 100000 AND input5.totalOrders > 5 AND input3.noOfVisitsPerMonth > 15"
},
{
"RuleName": "GiveDiscount35",
"SuccessEvent": "35",
"ErrorMessage": "One or more adjust rules failed, totalPurchasesToDate : $(input4.totalPurchasesToDate), totalOrders : $(input5.totalOrders)",
"ErrorType": "Error",
"RuleExpressionType": "LambdaExpression",
"Expression": "input4.loyalityFactor > 30 AND input4.totalPurchasesToDate >= 100000 AND input5.totalOrders > 15 AND input3.noOfVisitsPerMonth > 25"
}
]
}
]