diff --git a/CHANGELOG.md b/CHANGELOG.md index 7659711..b9deb2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/RulesEngine/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs b/src/RulesEngine/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs index ce13397..3a93b78 100644 --- a/src/RulesEngine/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs +++ b/src/RulesEngine/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs @@ -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); - } + } } + + /// Builds the expression for rule parameter. + /// The parameter. + /// The type parameter expressions. + /// The rule input exp. + /// Expression. + internal override Expression BuildExpressionForRuleParam(LocalParam param, IEnumerable 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; + } + } } diff --git a/src/RulesEngine/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs b/src/RulesEngine/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs index 0130138..88a6da7 100644 --- a/src/RulesEngine/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs +++ b/src/RulesEngine/RulesEngine/ExpressionBuilders/RuleExpressionBuilderBase.cs @@ -21,5 +21,12 @@ namespace RulesEngine.ExpressionBuilders /// The rule input exp. /// Expression type internal abstract Expression> BuildExpressionForRule(Rule rule, IEnumerable typeParamExpressions, ParameterExpression ruleInputExp); + + /// Builds the expression for rule parameter. + /// The rule. + /// The type parameter expressions. + /// The rule input exp. + /// Expression. + internal abstract Expression BuildExpressionForRuleParam(LocalParam rule, IEnumerable typeParamExpressions, ParameterExpression ruleInputExp); } } diff --git a/src/RulesEngine/RulesEngine/Models/CompiledParam.cs b/src/RulesEngine/RulesEngine/Models/CompiledParam.cs new file mode 100644 index 0000000..c05b968 --- /dev/null +++ b/src/RulesEngine/RulesEngine/Models/CompiledParam.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace RulesEngine.Models +{ + /// + /// CompiledParam class. + /// + internal class CompiledParam + { + /// + /// Gets or sets the name. + /// + /// + /// The name. + /// + internal string Name { get; set; } + + /// + /// Gets or sets the value. + /// + /// + /// The value. + /// + internal Delegate Value { get; set; } + + /// + /// Gets or sets the parameters. + /// + /// + /// The parameters. + /// + internal IEnumerable Parameters { get; set; } + } +} diff --git a/src/RulesEngine/RulesEngine/Models/CompiledRule.cs b/src/RulesEngine/RulesEngine/Models/CompiledRule.cs index 6d18a71..f1d7959 100644 --- a/src/RulesEngine/RulesEngine/Models/CompiledRule.cs +++ b/src/RulesEngine/RulesEngine/Models/CompiledRule.cs @@ -16,7 +16,16 @@ namespace RulesEngine.Models /// /// The compiled rules. /// - internal List CompiledRules { get; set; } + internal Delegate Rule { get; set; } + + + /// + /// Gets or sets the rule parameters. + /// + /// + /// The rule parameters. + /// + internal CompiledRuleParam CompiledParameters { get; set; } } } diff --git a/src/RulesEngine/RulesEngine/Models/CompiledRuleParam.cs b/src/RulesEngine/RulesEngine/Models/CompiledRuleParam.cs new file mode 100644 index 0000000..cecd2d5 --- /dev/null +++ b/src/RulesEngine/RulesEngine/Models/CompiledRuleParam.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace RulesEngine.Models +{ + /// Class CompiledRule. + internal class CompiledRuleParam + { + /// + /// Gets or sets the compiled rules. + /// + /// + /// The compiled rules. + /// + internal string Name { get; set; } + + /// Gets or sets the rule parameters. + /// The rule parameters. + internal IEnumerable CompiledParameters { get; set; } + + /// + /// Gets or sets the rule parameters. + /// + /// + /// The rule parameters. + /// + internal IEnumerable RuleParameters { get; set; } + } +} diff --git a/src/RulesEngine/RulesEngine/Models/LocalParam.cs b/src/RulesEngine/RulesEngine/Models/LocalParam.cs new file mode 100644 index 0000000..754e0be --- /dev/null +++ b/src/RulesEngine/RulesEngine/Models/LocalParam.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; + +namespace RulesEngine.Models +{ + /// Class Param. + /// Implements the + public class LocalParam + { + + /// + /// Gets or sets the name of the rule. + /// + /// + /// The name of the rule. + /// + [JsonProperty, JsonRequired] + public string Name { get; private set; } + + /// + /// Gets or Sets the lambda expression. + /// + [JsonProperty, JsonRequired] + public string Expression { get; private set; } + } +} diff --git a/src/RulesEngine/RulesEngine/Models/Rule.cs b/src/RulesEngine/RulesEngine/Models/Rule.cs index db1aa9f..f0178fa 100644 --- a/src/RulesEngine/RulesEngine/Models/Rule.cs +++ b/src/RulesEngine/RulesEngine/Models/Rule.cs @@ -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 /// public List Rules { get; set; } + /// + /// Gets the parameters. + /// + /// + /// The parameters. + /// + [JsonProperty] + public IEnumerable LocalParams { get; private set; } + /// /// Gets or Sets the lambda expression. /// public string Expression { get; set; } + /// + /// Gets or sets the success event. + /// + /// + /// The success event. + /// public string SuccessEvent { get; set; } } diff --git a/src/RulesEngine/RulesEngine/Models/RuleResultTree.cs b/src/RulesEngine/RulesEngine/Models/RuleResultTree.cs index 2649031..fab28d1 100644 --- a/src/RulesEngine/RulesEngine/Models/RuleResultTree.cs +++ b/src/RulesEngine/RulesEngine/Models/RuleResultTree.cs @@ -47,6 +47,14 @@ namespace RulesEngine.Models /// public string ExceptionMessage { get; set; } + /// + /// Gets or sets the rule evaluated parameters. + /// + /// + /// The rule evaluated parameters. + /// + public IEnumerable RuleEvaluatedParams { get; set; } + /// /// This method will return all the error and warning messages to caller /// diff --git a/src/RulesEngine/RulesEngine/Models/WorkflowRules.cs b/src/RulesEngine/RulesEngine/Models/WorkflowRules.cs index f6db30f..0364719 100644 --- a/src/RulesEngine/RulesEngine/Models/WorkflowRules.cs +++ b/src/RulesEngine/RulesEngine/Models/WorkflowRules.cs @@ -17,10 +17,13 @@ namespace RulesEngine.Models /// public string WorkflowName { get; set; } - public List WorkflowRulesToInject { get; set; } + /// Gets or sets the workflow rules to inject. + /// The workflow rules to inject. + public IEnumerable WorkflowRulesToInject { get; set; } + /// /// list of rules. /// - public List Rules { get; set; } + public IEnumerable Rules { get; set; } } } diff --git a/src/RulesEngine/RulesEngine/ParamCache.cs b/src/RulesEngine/RulesEngine/ParamCache.cs new file mode 100644 index 0000000..84c7e97 --- /dev/null +++ b/src/RulesEngine/RulesEngine/ParamCache.cs @@ -0,0 +1,86 @@ +using RulesEngine.Models; +using System; +using System.Collections.Concurrent; +using System.Linq; + +namespace RulesEngine +{ + /// Maintains the cache of evaludated param. + internal class ParamCache where T : class + { + /// + /// The compile rules + /// + private readonly ConcurrentDictionary _evaluatedParams = new ConcurrentDictionary(); + + /// + /// + /// Determines whether the specified parameter key name contains parameters. + /// + /// + /// Name of the parameter key. + /// + /// true if the specified parameter key name contains parameters; otherwise, false. + public bool ContainsParams(string paramKeyName) + { + return _evaluatedParams.ContainsKey(paramKeyName); + } + + /// Adds the or update evaluated parameter. + /// Name of the parameter key. + /// The rule parameters. + public void AddOrUpdateParams(string paramKeyName, T ruleParameters) + { + _evaluatedParams.AddOrUpdate(paramKeyName, ruleParameters, (k, v) => v); + } + + /// Clears this instance. + public void Clear() + { + _evaluatedParams.Clear(); + } + + /// Gets the evaluated parameters. + /// Name of the parameter key. + /// Delegate[]. + public T GetParams(string paramKeyName) + { + return _evaluatedParams[paramKeyName]; + } + + /// Gets the evaluated parameters cache key. + /// Name of the workflow. + /// The rule. + /// Cache key. + 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))}"; + } + } + + /// Removes the specified workflow name. + /// Name of the workflow. + 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); + } + } + } + } +} diff --git a/src/RulesEngine/RulesEngine/ParamCompiler.cs b/src/RulesEngine/RulesEngine/ParamCompiler.cs new file mode 100644 index 0000000..03e7689 --- /dev/null +++ b/src/RulesEngine/RulesEngine/ParamCompiler.cs @@ -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 +{ + /// + /// Rule param compilers + /// + internal class ParamCompiler + { + /// + /// The expression builder factory + /// + private readonly RuleExpressionBuilderFactory _expressionBuilderFactory; + + /// + /// The logger + /// + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The expression builder factory. + /// expressionBuilderFactory + 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; + } + + /// + /// Compiles the and evaluate parameter expression. + /// + /// The rule. + /// The rule parameters. + /// + /// IEnumerable<RuleParameter>. + /// + public CompiledRuleParam CompileParamsExpression(Rule rule, IEnumerable ruleParams) + { + + CompiledRuleParam compiledRuleParam = null; + + if (rule.LocalParams != null) + { + var compiledParameters = new List(); + var evaluatedParameters = new List(); + foreach (var param in rule.LocalParams) + { + IEnumerable 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(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 { evaluatedParam }); + evaluatedParameters.Add(evaluatedParam); + } + + compiledRuleParam = new CompiledRuleParam { Name = rule.RuleName, CompiledParameters = compiledParameters, RuleParameters = evaluatedParameters }; + } + + return compiledRuleParam; + } + + /// Evaluates the compiled parameter. + /// Name of the parameter. + /// The compiled parameter. + /// The rule parameters. + /// RuleParameter. + public RuleParameter EvaluateCompiledParam(string paramName, Delegate compiledParam, IEnumerable ruleParams) + { + var inputs = ruleParams.Select(c => c.Value); + var result = compiledParam.DynamicInvoke(new List(inputs) { new RuleInput() }.ToArray()); + return new RuleParameter(paramName, result); + } + + // + /// Gets the parameter expression. + /// + /// The types. + /// + /// + /// types + /// or + /// type + /// + private IEnumerable 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); + } + } + + /// + /// Gets the expression for rule. + /// + /// The rule. + /// The type parameter expressions. + /// The rule input exp. + /// + private Expression GetExpressionForRuleParam(LocalParam param, IEnumerable typeParameterExpressions, ParameterExpression ruleInputExp) + { + return BuildExpression(param, typeParameterExpressions, ruleInputExp); + } + + /// + /// Builds the expression. + /// + /// The rule. + /// The type parameter expressions. + /// The rule input exp. + /// + /// + private Expression BuildExpression(LocalParam param, IEnumerable typeParameterExpressions, ParameterExpression ruleInputExp) + { + var ruleExpressionBuilder = _expressionBuilderFactory.RuleGetExpressionBuilder(RuleExpressionType.LambdaExpression); + + var expression = ruleExpressionBuilder.BuildExpressionForRuleParam(param, typeParameterExpressions, ruleInputExp); + + return expression; + } + } +} diff --git a/src/RulesEngine/RulesEngine/RuleCompiler.cs b/src/RulesEngine/RulesEngine/RuleCompiler.cs index 519dd29..5912647 100644 --- a/src/RulesEngine/RulesEngine/RuleCompiler.cs +++ b/src/RulesEngine/RulesEngine/RuleCompiler.cs @@ -26,6 +26,9 @@ namespace RulesEngine /// private readonly RuleExpressionBuilderFactory _expressionBuilderFactory; + /// + /// The logger + /// private readonly ILogger _logger; /// diff --git a/src/RulesEngine/RulesEngine/RulesCache.cs b/src/RulesEngine/RulesEngine/RulesCache.cs index 0b0cfe0..4b8b1ba 100644 --- a/src/RulesEngine/RulesEngine/RulesCache.cs +++ b/src/RulesEngine/RulesEngine/RulesCache.cs @@ -9,42 +9,68 @@ using System.Linq; namespace RulesEngine { + /// Class RulesCache. internal class RulesCache { - private ConcurrentDictionary _compileRules = new ConcurrentDictionary(); + /// The compile rules + private ConcurrentDictionary> _compileRules = new ConcurrentDictionary>(); + + /// The workflow rules private ConcurrentDictionary _workflowRules = new ConcurrentDictionary(); + /// Determines whether [contains workflow rules] [the specified workflow name]. + /// Name of the workflow. + /// + /// true if [contains workflow rules] [the specified workflow name]; otherwise, false. public bool ContainsWorkflowRules(string workflowName) { return _workflowRules.ContainsKey(workflowName); } + /// Determines whether [contains compiled rules] [the specified workflow name]. + /// Name of the workflow. + /// + /// true if [contains compiled rules] [the specified workflow name]; otherwise, false. public bool ContainsCompiledRules(string workflowName) { return _compileRules.ContainsKey(workflowName); } + /// Adds the or update workflow rules. + /// Name of the workflow. + /// The rules. public void AddOrUpdateWorkflowRules(string workflowName, WorkflowRules rules) { _workflowRules.AddOrUpdate(workflowName, rules, (k, v) => rules); } - public void AddOrUpdateCompiledRule(string compiledRuleKey, CompiledRule compiledRule) + /// Adds the or update compiled rule. + /// The compiled rule key. + /// The compiled rule. + public void AddOrUpdateCompiledRule(string compiledRuleKey, IEnumerable compiledRule) { _compileRules.AddOrUpdate(compiledRuleKey, compiledRule, (k, v) => compiledRule); } + /// Clears this instance. public void Clear() { _workflowRules.Clear(); _compileRules.Clear(); } + /// Gets the rules. + /// Name of the workflow. + /// IEnumerable<Rule>. public IEnumerable GetRules(string workflowName) { return _workflowRules[workflowName].Rules; } + /// Gets the work flow rules. + /// Name of the workflow. + /// WorkflowRules. + /// Could not find injected Workflow: {wfname} 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) + /// Gets the rules cache key. + /// Name of the workflow. + /// System.String. + /// Could not find injected Workflow: {wfname} + 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(); + } + 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; + } + } + + /// Gets the compiled rules. + /// The compiled rules key. + /// CompiledRule. + public IEnumerable GetCompiledRules(string compiledRulesKey) { return _compileRules[compiledRulesKey]; } + /// Removes the specified workflow name. + /// Name of the workflow. 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 val); } } } diff --git a/src/RulesEngine/RulesEngine/RulesEngine.cs b/src/RulesEngine/RulesEngine/RulesEngine.cs index 1422086..19dc2a2 100644 --- a/src/RulesEngine/RulesEngine/RulesEngine.cs +++ b/src/RulesEngine/RulesEngine/RulesEngine.cs @@ -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 { + /// + /// + /// + /// public class RulesEngine : IRulesEngine { #region Variables + + /// + /// The logger + /// private readonly ILogger _logger; + + /// + /// The re settings + /// private readonly ReSettings _reSettings; + + /// + /// The rules cache + /// private readonly RulesCache _rulesCache = new RulesCache(); + + /// + /// The parameters cache + /// + private readonly ParamCache _compiledParamsCache = new ParamCache(); + + /// + /// The rule parameter compiler + /// + private readonly ParamCompiler ruleParamCompiler; + + /// + /// The parameter parse regex + /// + private const string ParamParseRegex = "(\\$\\(.*?\\))"; #endregion #region Constructor @@ -40,6 +73,7 @@ namespace RulesEngine { _logger = logger ?? new NullLogger(); _reSettings = reSettings ?? new ReSettings(); + ruleParamCompiler = new ParamCompiler(new RuleExpressionBuilderFactory(_reSettings), _logger); } #endregion @@ -81,6 +115,11 @@ namespace RulesEngine #region Private Methods + /// + /// Adds the workflow. + /// + /// The workflow rules. + /// public void AddWorkflow(params WorkflowRules[] workflowRules) { try @@ -98,11 +137,18 @@ namespace RulesEngine } } + /// + /// Clears the workflows. + /// public void ClearWorkflows() { _rulesCache.Clear(); } + /// + /// Removes the workflow. + /// + /// The workflow names. public void RemoveWorkflow(params string[] workflowNames) { foreach (var workflowName in workflowNames) @@ -122,7 +168,7 @@ namespace RulesEngine { List result; - if (RegisterRule(workflowName, ruleParams)) + if (RegisterCompiledRule(workflowName, ruleParams)) { result = ExecuteRuleByWorkflow(workflowName, ruleParams); } @@ -135,31 +181,45 @@ namespace RulesEngine return result; } - /// /// This will compile the rules and store them to dictionary /// - /// type of entity /// workflow name - /// bool result - private bool RegisterRule(string workflowName, params RuleParameter[] ruleParams) + /// The rule parameters. + /// + /// bool result + /// + 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(); + var lstFunc = new List(); + 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 /// /// /// list of rule result set - private List ExecuteRuleByWorkflow(string workflowName, RuleParameter[] ruleParams) + private List ExecuteRuleByWorkflow(string workflowName, RuleParameter[] ruleParameters) { _logger.LogTrace($"Compiled rules found for {workflowName} workflow and executed"); List result = new List(); - 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(inputs) { new RuleInput() }.ToArray()) as RuleResultTree); + IEnumerable evaluatedRuleParams = new List(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 { evaluatedParam }); + } + } + + var inputs = evaluatedRuleParams.Select(c => c.Value); + var resultTree = compiledRule.Rule.DynamicInvoke(new List(inputs) { new RuleInput() }.ToArray()) as RuleResultTree; + resultTree.RuleEvaluatedParams = evaluatedRuleParams; + result.Add(resultTree); + } + + FormatErrorMessages(result?.Where(r => !r.IsSuccess)); + return result; + } + + /// + /// The result + /// + /// The result. + /// Updated error message. + private IEnumerable FormatErrorMessages(IEnumerable 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; } + + /// + /// Updates the error message. + /// + /// The error message. + /// The evaluated parameters. + /// The property. + /// Name of the type. + /// Name of the property. + /// Updated error message. + private static string UpdateErrorMessage(string errorMessage, IEnumerable 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 } } diff --git a/src/RulesEngine/RulesEngine/RulesEngine.csproj b/src/RulesEngine/RulesEngine/RulesEngine.csproj index 4a367e2..17617ee 100644 --- a/src/RulesEngine/RulesEngine/RulesEngine.csproj +++ b/src/RulesEngine/RulesEngine/RulesEngine.csproj @@ -2,22 +2,35 @@ netstandard2.0 - 2.0.1 + 2.1.0 Copyright (c) Microsoft Corporation. LICENSE https://github.com/microsoft/RulesEngine - Dishant Munjal, Abbas Cyclewala, Yogesh Prajapati + Dishant Munjal, Abbas Cyclewala, Yogesh Prajapati, Deepak Joshi, Pritam Deshmukh, Chetanya Kumar 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. BRE, Rules Engine, Abstraction + + true + + + + true + true + snupkg + + + + + - + diff --git a/src/RulesEngine/global.json b/src/RulesEngine/global.json index f1c2b2b..c120c81 100644 --- a/src/RulesEngine/global.json +++ b/src/RulesEngine/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "3.1.101" + "version": "3.1.301" } } \ No newline at end of file diff --git a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs index dd2b1a7..9f0f29b 100644 --- a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs +++ b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs @@ -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>(result); } @@ -153,6 +154,37 @@ namespace RulesEngine.UnitTest Assert.IsType>(result); } + /// + /// Ruleses the engine execute rule for nested rull parameters returns success. + /// + /// Name of the rule file. + /// Rules not found. + [Theory] + [InlineData("rules4.json")] + public void RulesEngine_Execute_Rule_For_Nested_Rull_Params_Returns_Success(string ruleFileName) + { + dynamic[] inputs = GetInputs4(); + + var ruleParams = new List(); + + 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(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(); - 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(telemetryInfo, converter); } - + + /// + /// Gets the inputs. + /// + /// + /// The inputs. + /// + 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>(laborCategoriesInput, settings); + dynamic input2 = JsonConvert.DeserializeObject(currentLaborCategoryInput, converter); + dynamic input3 = JsonConvert.DeserializeObject(telemetryInfo, converter); + dynamic input4 = JsonConvert.DeserializeObject(basicInfo, converter); + dynamic input5 = JsonConvert.DeserializeObject(orderInfo, converter); + + var inputs = new dynamic[] + { + input1, + input2, + input3, + input4, + input5 + }; + + return inputs; + } + } } \ No newline at end of file diff --git a/test/RulesEngine.UnitTest/PrivateSetterContractResolver.cs b/test/RulesEngine.UnitTest/PrivateSetterContractResolver.cs new file mode 100644 index 0000000..3782a28 --- /dev/null +++ b/test/RulesEngine.UnitTest/PrivateSetterContractResolver.cs @@ -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 +{ + /// Class PrivateSetterContractResolver. + /// Implements the + public class PrivateSetterContractResolver : DefaultContractResolver + { + /// Creates a for the given MemberInfo. + /// The member to create a for. + /// The member's parent . + /// A created for the given MemberInfo. + 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; + } + } +} diff --git a/test/RulesEngine.UnitTest/RuleTestClass.cs b/test/RulesEngine.UnitTest/RuleTestClass.cs new file mode 100644 index 0000000..77407f3 --- /dev/null +++ b/test/RulesEngine.UnitTest/RuleTestClass.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace RulesEngine.UnitTest +{ + /// + /// Class RuleTestClass. + /// + public class RuleTestClass + { + /// + /// Gets the country. + /// + /// + /// The country. + /// + [JsonProperty("country")] + public string Country { get; private set; } + + /// + /// Gets the loyality factor. + /// + /// + /// The loyality factor. + /// + [JsonProperty("loyalityFactor")] + public int LoyalityFactor { get; private set; } + + /// + /// Gets the total purchases to date. + /// + /// + /// The total purchases to date. + /// + [JsonProperty("totalPurchasesToDate")] + public int TotalPurchasesToDate { get; private set; } + } +} diff --git a/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj b/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj index 21d87ee..9d95788 100644 --- a/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj +++ b/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj @@ -24,6 +24,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/test/RulesEngine.UnitTest/TestData/rules4.json b/test/RulesEngine.UnitTest/TestData/rules4.json new file mode 100644 index 0000000..1137c0a --- /dev/null +++ b/test/RulesEngine.UnitTest/TestData/rules4.json @@ -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" + } + ] + } +] \ No newline at end of file