// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using FluentValidation; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using RulesEngine.Actions; using RulesEngine.Exceptions; using RulesEngine.ExpressionBuilders; using RulesEngine.Extensions; using RulesEngine.HelperFunctions; using RulesEngine.Interfaces; using RulesEngine.Models; using RulesEngine.Validators; using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace RulesEngine { /// /// /// /// public class RulesEngine : IRulesEngine { #region Variables private readonly ReSettings _reSettings; private readonly RulesCache _rulesCache; private readonly RuleExpressionParser _ruleExpressionParser; private readonly RuleCompiler _ruleCompiler; private readonly ActionFactory _actionFactory; private const string ParamParseRegex = "(\\$\\(.*?\\))"; #endregion #region Constructor public RulesEngine(string[] jsonConfig, ReSettings reSettings = null) : this(reSettings) { var workflow = jsonConfig.Select(item => JsonConvert.DeserializeObject(item)).ToArray(); AddWorkflow(workflow); } public RulesEngine(Workflow[] Workflows, ReSettings reSettings = null) : this(reSettings) { AddWorkflow(Workflows); } public RulesEngine(ReSettings reSettings = null) { _reSettings = reSettings == null ? new ReSettings(): new ReSettings(reSettings); if(_reSettings.CacheConfig == null) { _reSettings.CacheConfig = new MemCacheConfig(); } _rulesCache = new RulesCache(_reSettings); _ruleExpressionParser = new RuleExpressionParser(_reSettings); _ruleCompiler = new RuleCompiler(new RuleExpressionBuilderFactory(_reSettings, _ruleExpressionParser),_reSettings); _actionFactory = new ActionFactory(GetActionRegistry(_reSettings)); } private IDictionary> GetActionRegistry(ReSettings reSettings) { var actionDictionary = GetDefaultActionRegistry(); var customActions = reSettings.CustomActions ?? new Dictionary>(); foreach (var customAction in customActions) { actionDictionary.Add(customAction); } return actionDictionary; } #endregion #region Public Methods /// /// This will execute all the rules of the specified workflow /// /// The name of the workflow with rules to execute against the inputs /// A variable number of inputs /// List of rule results public async ValueTask> ExecuteAllRulesAsync(string workflowName, params object[] inputs) { var ruleParams = new List(); for (var i = 0; i < inputs.Length; i++) { var input = inputs[i]; ruleParams.Add(new RuleParameter($"input{i + 1}", input)); } return await ExecuteAllRulesAsync(workflowName, ruleParams.ToArray()); } /// /// This will execute all the rules of the specified workflow /// /// The name of the workflow with rules to execute against the inputs /// A variable number of rule parameters /// List of rule results public async ValueTask> ExecuteAllRulesAsync(string workflowName, params RuleParameter[] ruleParams) { var sortedRuleParams = ruleParams.ToList(); sortedRuleParams.Sort((RuleParameter a, RuleParameter b) => string.Compare(a.Name, b.Name)); var ruleResultList = ValidateWorkflowAndExecuteRule(workflowName, sortedRuleParams.ToArray()); await ExecuteActionAsync(ruleResultList); return ruleResultList; } private async ValueTask ExecuteActionAsync(IEnumerable ruleResultList) { foreach (var ruleResult in ruleResultList) { if(ruleResult.ChildResults != null) { await ExecuteActionAsync(ruleResult.ChildResults); } var actionResult = await ExecuteActionForRuleResult(ruleResult, false); ruleResult.ActionResult = new ActionResult { Output = actionResult.Output, Exception = actionResult.Exception }; } } public async ValueTask ExecuteActionWorkflowAsync(string workflowName, string ruleName, RuleParameter[] ruleParameters) { var compiledRule = CompileRule(workflowName, ruleName, ruleParameters); var resultTree = compiledRule(ruleParameters); return await ExecuteActionForRuleResult(resultTree, true); } private async ValueTask ExecuteActionForRuleResult(RuleResultTree resultTree, bool includeRuleResults = false) { var ruleActions = resultTree?.Rule?.Actions; var actionInfo = resultTree?.IsSuccess == true ? ruleActions?.OnSuccess : ruleActions?.OnFailure; if (actionInfo != null) { var action = _actionFactory.Get(actionInfo.Name); var ruleParameters = resultTree.Inputs.Select(kv => new RuleParameter(kv.Key, kv.Value)).ToArray(); return await action.ExecuteAndReturnResultAsync(new ActionContext(actionInfo.Context, resultTree), ruleParameters, includeRuleResults); } else { //If there is no action,return output as null and return the result for rule return new ActionRuleResult { Output = null, Results = includeRuleResults ? new List() { resultTree } : null }; } } #endregion #region Private Methods /// /// Adds the workflow if the workflow name is not already added. Ignores the rest. /// /// The workflow rules. /// public void AddWorkflow(params Workflow[] workflows) { try { foreach (var workflow in workflows) { var validator = new WorkflowsValidator(); validator.ValidateAndThrow(workflow); if (!_rulesCache.ContainsWorkflows(workflow.WorkflowName)) { _rulesCache.AddOrUpdateWorkflows(workflow.WorkflowName, workflow); } else { throw new ValidationException($"Cannot add workflow `{workflow.WorkflowName}` as it already exists. Use `AddOrUpdateWorkflow` to update existing workflow"); } } } catch (ValidationException ex) { throw new RuleValidationException(ex.Message, ex.Errors); } } /// /// Adds new workflow rules if not previously added. /// Or updates the rules for an existing workflow. /// /// The workflow rules. /// public void AddOrUpdateWorkflow(params Workflow[] workflows) { try { foreach (var workflow in workflows) { var validator = new WorkflowsValidator(); validator.ValidateAndThrow(workflow); _rulesCache.AddOrUpdateWorkflows(workflow.WorkflowName, workflow); } } catch (ValidationException ex) { throw new RuleValidationException(ex.Message, ex.Errors); } } public List GetAllRegisteredWorkflowNames() { return _rulesCache.GetAllWorkflowNames(); } /// /// Checks is workflow exist. /// /// The workflow name. /// true if contains the specified workflow name; otherwise, false. public bool ContainsWorkflow(string workflowName) { return _rulesCache.ContainsWorkflows(workflowName); } /// /// Clears the workflow. /// public void ClearWorkflows() { _rulesCache.Clear(); } /// /// Removes the workflows. /// /// The workflow names. public void RemoveWorkflow(params string[] workflowNames) { foreach (var workflowName in workflowNames) { _rulesCache.Remove(workflowName); } } /// /// This will validate workflow rules then call execute method /// /// type of entity /// input /// workflow name /// list of rule result set private List ValidateWorkflowAndExecuteRule(string workflowName, RuleParameter[] ruleParams) { List result; if (RegisterRule(workflowName, ruleParams)) { result = ExecuteAllRuleByWorkflow(workflowName, ruleParams); } else { // if rules are not registered with Rules Engine throw new ArgumentException($"Rule config file is not present for the {workflowName} workflow"); } return result; } /// /// This will compile the rules and store them to dictionary /// /// workflow name /// The rule parameters. /// /// bool result /// private bool RegisterRule(string workflowName, params RuleParameter[] ruleParams) { var compileRulesKey = GetCompiledRulesKey(workflowName, ruleParams); if (_rulesCache.AreCompiledRulesUpToDate(compileRulesKey, workflowName)) { return true; } var workflow = _rulesCache.GetWorkflow(workflowName); if (workflow != null) { var dictFunc = new Dictionary>(); if (_reSettings.AutoRegisterInputType) { _reSettings.CustomTypes = _reSettings.CustomTypes.Safe().Union(ruleParams.Select(c => c.Type)).ToArray(); } // add separate compilation for global params var globalParamExp = new Lazy( () => _ruleCompiler.GetRuleExpressionParameters(workflow.RuleExpressionType, workflow.GlobalParams, ruleParams) ); foreach (var rule in workflow.Rules.Where(c => c.Enabled)) { dictFunc.Add(rule.RuleName, CompileRule(rule,workflow.RuleExpressionType, ruleParams, globalParamExp)); } _rulesCache.AddOrUpdateCompiledRule(compileRulesKey, dictFunc); return true; } else { return false; } } private RuleFunc CompileRule(string workflowName, string ruleName, RuleParameter[] ruleParameters) { var workflow = _rulesCache.GetWorkflow(workflowName); if(workflow == null) { throw new ArgumentException($"Workflow `{workflowName}` is not found"); } var currentRule = workflow.Rules?.SingleOrDefault(c => c.RuleName == ruleName && c.Enabled); if (currentRule == null) { throw new ArgumentException($"Workflow `{workflowName}` does not contain any rule named `{ruleName}`"); } var globalParamExp = new Lazy( () => _ruleCompiler.GetRuleExpressionParameters(workflow.RuleExpressionType, workflow.GlobalParams, ruleParameters) ); return CompileRule(currentRule,workflow.RuleExpressionType, ruleParameters, globalParamExp); } private RuleFunc CompileRule(Rule rule, RuleExpressionType ruleExpressionType, RuleParameter[] ruleParams, Lazy scopedParams) { return _ruleCompiler.CompileRule(rule, ruleExpressionType, ruleParams, scopedParams); } /// /// This will execute the compiled rules /// /// /// /// list of rule result set private List ExecuteAllRuleByWorkflow(string workflowName, RuleParameter[] ruleParameters) { var result = new List(); var compiledRulesCacheKey = GetCompiledRulesKey(workflowName, ruleParameters); foreach (var compiledRule in _rulesCache.GetCompiledRules(compiledRulesCacheKey)?.Values) { var resultTree = compiledRule(ruleParameters); result.Add(resultTree); } FormatErrorMessages(result); return result; } private string GetCompiledRulesKey(string workflowName, RuleParameter[] ruleParams) { var ruleParamsKey = string.Join("-", ruleParams.Select(c => $"{c.Name}_{c.Type.Name}")); var key = $"{workflowName}-" + ruleParamsKey; return key; } private IDictionary> GetDefaultActionRegistry() { return new Dictionary>{ {"OutputExpression",() => new OutputExpressionAction(_ruleExpressionParser) }, {"EvaluateRule", () => new EvaluateRuleAction(this,_ruleExpressionParser) } }; } /// /// The result /// /// The result. /// Updated error message. private IEnumerable FormatErrorMessages(IEnumerable ruleResultList) { if (_reSettings.EnableFormattedErrorMessage) { foreach (var ruleResult in ruleResultList?.Where(r => !r.IsSuccess)) { var errorMessage = ruleResult?.Rule?.ErrorMessage; if (string.IsNullOrWhiteSpace(ruleResult.ExceptionMessage) && errorMessage != null) { var errorParameters = Regex.Matches(errorMessage, ParamParseRegex); var inputs = ruleResult.Inputs; 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, inputs, property, typeName, propertyName); } else { var arrParams = inputs?.Select(c => new { Name = c.Key, 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})"); } } ruleResult.ExceptionMessage = errorMessage; } } } return ruleResultList; } /// /// 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, IDictionary inputs, string property, string typeName, string propertyName) { var arrParams = inputs?.Select(c => new { Name = c.Key, 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 } }