2020-11-16 03:36:35 -05:00
// Copyright (c) Microsoft Corporation.
2020-11-01 22:55:43 -05:00
// 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.Actions ;
using RulesEngine.Exceptions ;
2020-12-23 00:34:10 -05:00
using RulesEngine.ExpressionBuilders ;
2020-11-01 22:55:43 -05:00
using RulesEngine.Interfaces ;
using RulesEngine.Models ;
using RulesEngine.Validators ;
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Text.RegularExpressions ;
2020-12-23 00:34:10 -05:00
using System.Threading.Tasks ;
2020-11-01 22:55:43 -05:00
namespace RulesEngine
{
/// <summary>
///
/// </summary>
/// <seealso cref="RulesEngine.Interfaces.IRulesEngine" />
public class RulesEngine : IRulesEngine
{
#region Variables
private readonly ILogger _logger ;
private readonly ReSettings _reSettings ;
private readonly RulesCache _rulesCache = new RulesCache ( ) ;
private readonly RuleExpressionParser _ruleExpressionParser ;
private readonly RuleCompiler _ruleCompiler ;
private readonly ActionFactory _actionFactory ;
private const string ParamParseRegex = "(\\$\\(.*?\\))" ;
# endregion
#region Constructor
2021-04-19 01:51:33 -04:00
public RulesEngine ( string [ ] jsonConfig , ILogger logger = null , ReSettings reSettings = null ) : this ( logger , reSettings )
2020-11-01 22:55:43 -05:00
{
2021-08-13 00:34:47 -04:00
var workflow = jsonConfig . Select ( item = > JsonConvert . DeserializeObject < Workflow > ( item ) ) . ToArray ( ) ;
AddWorkflow ( workflow ) ;
2020-11-01 22:55:43 -05:00
}
2021-08-13 00:34:47 -04:00
public RulesEngine ( Workflow [ ] Workflows , ILogger logger = null , ReSettings reSettings = null ) : this ( logger , reSettings )
2020-11-01 22:55:43 -05:00
{
2021-08-13 00:34:47 -04:00
AddWorkflow ( Workflows ) ;
2020-11-01 22:55:43 -05:00
}
public RulesEngine ( ILogger logger = null , ReSettings reSettings = null )
{
_logger = logger ? ? new NullLogger < RulesEngine > ( ) ;
_reSettings = reSettings ? ? new ReSettings ( ) ;
_ruleExpressionParser = new RuleExpressionParser ( _reSettings ) ;
2021-02-01 23:59:21 -05:00
_ruleCompiler = new RuleCompiler ( new RuleExpressionBuilderFactory ( _reSettings , _ruleExpressionParser ) , _reSettings , _logger ) ;
2020-11-01 22:55:43 -05:00
_actionFactory = new ActionFactory ( GetActionRegistry ( _reSettings ) ) ;
}
2020-12-23 00:34:10 -05:00
private IDictionary < string , Func < ActionBase > > GetActionRegistry ( ReSettings reSettings )
2020-11-01 22:55:43 -05:00
{
var actionDictionary = GetDefaultActionRegistry ( ) ;
var customActions = reSettings . CustomActions ? ? new Dictionary < string , Func < ActionBase > > ( ) ;
2020-12-23 00:34:10 -05:00
foreach ( var customAction in customActions )
{
2020-11-01 22:55:43 -05:00
actionDictionary . Add ( customAction ) ;
}
return actionDictionary ;
}
# endregion
#region Public Methods
/// <summary>
/// This will execute all the rules of the specified workflow
/// </summary>
/// <param name="workflowName">The name of the workflow with rules to execute against the inputs</param>
/// <param name="inputs">A variable number of inputs</param>
/// <returns>List of rule results</returns>
public async ValueTask < List < RuleResultTree > > ExecuteAllRulesAsync ( string workflowName , params object [ ] inputs )
{
_logger . LogTrace ( $"Called {nameof(ExecuteAllRulesAsync)} for workflow {workflowName} and count of input {inputs.Count()}" ) ;
var ruleParams = new List < RuleParameter > ( ) ;
2020-12-23 00:34:10 -05:00
for ( var i = 0 ; i < inputs . Length ; i + + )
2020-11-01 22:55:43 -05:00
{
var input = inputs [ i ] ;
ruleParams . Add ( new RuleParameter ( $"input{i + 1}" , input ) ) ;
}
return await ExecuteAllRulesAsync ( workflowName , ruleParams . ToArray ( ) ) ;
}
/// <summary>
/// This will execute all the rules of the specified workflow
/// </summary>
/// <param name="workflowName">The name of the workflow with rules to execute against the inputs</param>
/// <param name="ruleParams">A variable number of rule parameters</param>
/// <returns>List of rule results</returns>
public async ValueTask < List < RuleResultTree > > ExecuteAllRulesAsync ( string workflowName , params RuleParameter [ ] ruleParams )
{
2020-12-23 00:34:10 -05:00
var ruleResultList = ValidateWorkflowAndExecuteRule ( workflowName , ruleParams ) ;
2021-07-20 07:24:32 -04:00
await ExecuteActionAsync ( ruleResultList ) ;
return ruleResultList ;
}
private async ValueTask ExecuteActionAsync ( IEnumerable < RuleResultTree > ruleResultList )
{
2020-12-23 00:34:10 -05:00
foreach ( var ruleResult in ruleResultList )
{
2021-07-20 07:24:32 -04:00
if ( ruleResult . ChildResults ! = null )
{
await ExecuteActionAsync ( ruleResult . ChildResults ) ;
}
2020-12-23 00:34:10 -05:00
var actionResult = await ExecuteActionForRuleResult ( ruleResult , false ) ;
ruleResult . ActionResult = new ActionResult {
2020-11-01 22:55:43 -05:00
Output = actionResult . Output ,
Exception = actionResult . Exception
} ;
}
}
public async ValueTask < ActionRuleResult > ExecuteActionWorkflowAsync ( string workflowName , string ruleName , RuleParameter [ ] ruleParameters )
{
var compiledRule = CompileRule ( workflowName , ruleName , ruleParameters ) ;
var resultTree = compiledRule ( ruleParameters ) ;
2020-12-23 00:34:10 -05:00
return await ExecuteActionForRuleResult ( resultTree , true ) ;
2020-11-01 22:55:43 -05:00
}
2020-12-23 00:34:10 -05:00
private async ValueTask < ActionRuleResult > ExecuteActionForRuleResult ( RuleResultTree resultTree , bool includeRuleResults = false )
2020-11-01 22:55:43 -05:00
{
2021-07-20 07:24:32 -04:00
var ruleActions = resultTree ? . Rule ? . Actions ;
var actionInfo = resultTree ? . IsSuccess = = true ? ruleActions ? . OnSuccess : ruleActions ? . OnFailure ;
2020-11-01 22:55:43 -05:00
2021-07-20 07:24:32 -04:00
if ( actionInfo ! = null )
2020-11-01 22:55:43 -05:00
{
var action = _actionFactory . Get ( actionInfo . Name ) ;
2020-12-23 00:34:10 -05:00
var ruleParameters = resultTree . Inputs . Select ( kv = > new RuleParameter ( kv . Key , kv . Value ) ) . ToArray ( ) ;
return await action . ExecuteAndReturnResultAsync ( new ActionContext ( actionInfo . Context , resultTree ) , ruleParameters , includeRuleResults ) ;
2020-11-01 22:55:43 -05:00
}
else
{
//If there is no action,return output as null and return the result for rule
2020-12-23 00:34:10 -05:00
return new ActionRuleResult {
2020-11-01 22:55:43 -05:00
Output = null ,
2020-12-23 00:34:10 -05:00
Results = includeRuleResults ? new List < RuleResultTree > ( ) { resultTree } : null
2020-11-01 22:55:43 -05:00
} ;
}
}
# endregion
#region Private Methods
/// <summary>
2021-06-07 01:48:21 -04:00
/// Adds the workflow if the workflow name is not already added. Ignores the rest.
2020-11-01 22:55:43 -05:00
/// </summary>
2021-08-13 00:34:47 -04:00
/// <param name="workflows">The workflow rules.</param>
2020-11-01 22:55:43 -05:00
/// <exception cref="RuleValidationException"></exception>
2021-08-13 00:34:47 -04:00
public void AddWorkflow ( params Workflow [ ] workflows )
2021-06-07 01:48:21 -04:00
{
try
{
2021-08-13 00:34:47 -04:00
foreach ( var workflow in workflows )
2021-06-07 01:48:21 -04:00
{
2021-08-13 00:34:47 -04:00
var validator = new WorkflowsValidator ( ) ;
validator . ValidateAndThrow ( workflow ) ;
if ( ! _rulesCache . ContainsWorkflows ( workflow . WorkflowName ) )
2021-06-07 01:48:21 -04:00
{
2021-08-13 00:34:47 -04:00
_rulesCache . AddOrUpdateWorkflows ( workflow . WorkflowName , workflow ) ;
2021-06-07 01:48:21 -04:00
}
2021-06-07 23:51:02 -04:00
else
{
2021-08-13 00:34:47 -04:00
throw new ValidationException ( $"Cannot add workflow `{workflow.WorkflowName}` as it already exists. Use `AddOrUpdateWorkflow` to update existing workflow" ) ;
2021-06-07 23:51:02 -04:00
}
2021-06-07 01:48:21 -04:00
}
}
catch ( ValidationException ex )
{
throw new RuleValidationException ( ex . Message , ex . Errors ) ;
}
}
/// <summary>
/// Adds new workflow rules if not previously added.
/// Or updates the rules for an existing workflow.
/// </summary>
2021-08-13 00:34:47 -04:00
/// <param name="workflows">The workflow rules.</param>
2021-06-07 01:48:21 -04:00
/// <exception cref="RuleValidationException"></exception>
2021-08-13 00:34:47 -04:00
public void AddOrUpdateWorkflow ( params Workflow [ ] workflows )
2020-11-01 22:55:43 -05:00
{
try
{
2021-08-13 00:34:47 -04:00
foreach ( var workflow in workflows )
2020-11-01 22:55:43 -05:00
{
2021-08-13 00:34:47 -04:00
var validator = new WorkflowsValidator ( ) ;
validator . ValidateAndThrow ( workflow ) ;
_rulesCache . AddOrUpdateWorkflows ( workflow . WorkflowName , workflow ) ;
2020-11-01 22:55:43 -05:00
}
}
catch ( ValidationException ex )
{
throw new RuleValidationException ( ex . Message , ex . Errors ) ;
}
}
2021-02-01 23:59:21 -05:00
public List < string > GetAllRegisteredWorkflowNames ( )
{
return _rulesCache . GetAllWorkflowNames ( ) ;
}
2020-11-01 22:55:43 -05:00
/// <summary>
2021-08-13 00:34:47 -04:00
/// Clears the workflow.
2020-11-01 22:55:43 -05:00
/// </summary>
public void ClearWorkflows ( )
{
_rulesCache . Clear ( ) ;
}
/// <summary>
2021-08-13 00:34:47 -04:00
/// Removes the workflows.
2020-11-01 22:55:43 -05:00
/// </summary>
/// <param name="workflowNames">The workflow names.</param>
public void RemoveWorkflow ( params string [ ] workflowNames )
{
foreach ( var workflowName in workflowNames )
{
_rulesCache . Remove ( workflowName ) ;
}
}
/// <summary>
/// This will validate workflow rules then call execute method
/// </summary>
/// <typeparam name="T">type of entity</typeparam>
/// <param name="input">input</param>
/// <param name="workflowName">workflow name</param>
/// <returns>list of rule result set</returns>
private List < RuleResultTree > ValidateWorkflowAndExecuteRule ( string workflowName , RuleParameter [ ] ruleParams )
{
List < RuleResultTree > result ;
if ( RegisterRule ( workflowName , ruleParams ) )
{
result = ExecuteAllRuleByWorkflow ( workflowName , ruleParams ) ;
}
else
{
_logger . LogTrace ( $"Rule config file is not present for the {workflowName} workflow" ) ;
// if rules are not registered with Rules Engine
throw new ArgumentException ( $"Rule config file is not present for the {workflowName} workflow" ) ;
}
return result ;
}
/// <summary>
/// This will compile the rules and store them to dictionary
/// </summary>
/// <param name="workflowName">workflow name</param>
/// <param name="ruleParams">The rule parameters.</param>
/// <returns>
/// bool result
/// </returns>
private bool RegisterRule ( string workflowName , params RuleParameter [ ] ruleParams )
{
2020-12-23 00:34:10 -05:00
var compileRulesKey = GetCompiledRulesKey ( workflowName , ruleParams ) ;
2021-06-07 01:48:21 -04:00
if ( _rulesCache . AreCompiledRulesUpToDate ( compileRulesKey , workflowName ) )
2020-12-23 00:34:10 -05:00
{
2020-11-01 22:55:43 -05:00
return true ;
2020-12-23 00:34:10 -05:00
}
2020-11-01 22:55:43 -05:00
2021-08-13 00:34:47 -04:00
var workflow = _rulesCache . GetWorkflow ( workflowName ) ;
if ( workflow ! = null )
2020-11-01 22:55:43 -05:00
{
2020-12-23 00:34:10 -05:00
var dictFunc = new Dictionary < string , RuleFunc < RuleResultTree > > ( ) ;
2021-08-13 00:34:47 -04:00
foreach ( var rule in workflow . Rules . Where ( c = > c . Enabled ) )
2020-11-01 22:55:43 -05:00
{
2021-08-13 00:34:47 -04:00
dictFunc . Add ( rule . RuleName , CompileRule ( rule , ruleParams , workflow . GlobalParams ? . ToArray ( ) ) ) ;
2020-11-01 22:55:43 -05:00
}
2020-12-23 00:34:10 -05:00
_rulesCache . AddOrUpdateCompiledRule ( compileRulesKey , dictFunc ) ;
2020-11-01 22:55:43 -05:00
_logger . LogTrace ( $"Rules has been compiled for the {workflowName} workflow and added to dictionary" ) ;
return true ;
}
else
{
return false ;
}
}
2020-12-23 00:34:10 -05:00
private RuleFunc < RuleResultTree > CompileRule ( string workflowName , string ruleName , RuleParameter [ ] ruleParameters )
{
2021-08-13 00:34:47 -04:00
var workflow = _rulesCache . GetWorkflow ( workflowName ) ;
2021-02-01 23:59:21 -05:00
if ( workflow = = null )
{
throw new ArgumentException ( $"Workflow `{workflowName}` is not found" ) ;
}
var currentRule = workflow . Rules ? . SingleOrDefault ( c = > c . RuleName = = ruleName & & c . Enabled ) ;
2020-12-23 00:34:10 -05:00
if ( currentRule = = null )
{
2020-11-01 22:55:43 -05:00
throw new ArgumentException ( $"Workflow `{workflowName}` does not contain any rule named `{ruleName}`" ) ;
}
2021-02-01 23:59:21 -05:00
return CompileRule ( currentRule , ruleParameters , workflow . GlobalParams ? . ToArray ( ) ) ;
2020-11-01 22:55:43 -05:00
}
2021-02-01 23:59:21 -05:00
private RuleFunc < RuleResultTree > CompileRule ( Rule rule , RuleParameter [ ] ruleParams , ScopedParam [ ] scopedParams )
2020-11-01 22:55:43 -05:00
{
2021-02-01 23:59:21 -05:00
return _ruleCompiler . CompileRule ( rule , ruleParams , scopedParams ) ;
2020-11-01 22:55:43 -05:00
}
2020-12-23 00:34:10 -05:00
2020-11-01 22:55:43 -05:00
/// <summary>
/// This will execute the compiled rules
/// </summary>
/// <param name="workflowName"></param>
/// <param name="ruleParams"></param>
/// <returns>list of rule result set</returns>
private List < RuleResultTree > ExecuteAllRuleByWorkflow ( string workflowName , RuleParameter [ ] ruleParameters )
{
_logger . LogTrace ( $"Compiled rules found for {workflowName} workflow and executed" ) ;
2020-12-23 00:34:10 -05:00
var result = new List < RuleResultTree > ( ) ;
var compiledRulesCacheKey = GetCompiledRulesKey ( workflowName , ruleParameters ) ;
2020-11-01 22:55:43 -05:00
foreach ( var compiledRule in _rulesCache . GetCompiledRules ( compiledRulesCacheKey ) ? . Values )
{
var resultTree = compiledRule ( ruleParameters ) ;
result . Add ( resultTree ) ;
}
FormatErrorMessages ( result ) ;
return result ;
}
2020-12-23 00:34:10 -05:00
2020-11-01 22:55:43 -05:00
private string GetCompiledRulesKey ( string workflowName , RuleParameter [ ] ruleParams )
{
2020-12-23 00:34:10 -05:00
var key = $"{workflowName}-" + string . Join ( "-" , ruleParams . Select ( c = > c . Type . Name ) ) ;
2020-11-01 22:55:43 -05:00
return key ;
}
2020-12-23 00:34:10 -05:00
private IDictionary < string , Func < ActionBase > > GetDefaultActionRegistry ( )
{
2020-11-01 22:55:43 -05:00
return new Dictionary < string , Func < ActionBase > > {
{ "OutputExpression" , ( ) = > new OutputExpressionAction ( _ruleExpressionParser ) } ,
2021-11-23 07:17:10 -05:00
{ "EvaluateRule" , ( ) = > new EvaluateRuleAction ( this , _ruleExpressionParser ) }
2020-11-01 22:55:43 -05:00
} ;
}
/// <summary>
/// The result
/// </summary>
/// <param name="ruleResultList">The result.</param>
/// <returns>Updated error message.</returns>
private IEnumerable < RuleResultTree > FormatErrorMessages ( IEnumerable < RuleResultTree > ruleResultList )
{
2020-12-23 00:34:10 -05:00
if ( _reSettings . EnableFormattedErrorMessage )
{
2020-11-16 03:36:35 -05:00
foreach ( var ruleResult in ruleResultList ? . Where ( r = > ! r . IsSuccess ) )
{
var errorMessage = ruleResult ? . Rule ? . ErrorMessage ;
2021-02-01 23:59:21 -05:00
if ( string . IsNullOrWhiteSpace ( ruleResult . ExceptionMessage ) & & errorMessage ! = null )
2020-12-23 00:34:10 -05:00
{
2020-11-16 03:36:35 -05:00
var errorParameters = Regex . Matches ( errorMessage , ParamParseRegex ) ;
2020-11-01 22:55:43 -05:00
2020-11-16 03:36:35 -05:00
var inputs = ruleResult . Inputs ;
foreach ( var param in errorParameters )
2020-11-01 22:55:43 -05:00
{
2020-11-16 03:36:35 -05:00
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})" ) ;
}
2020-11-01 22:55:43 -05:00
}
2020-11-16 03:36:35 -05:00
ruleResult . ExceptionMessage = errorMessage ;
2020-11-01 22:55:43 -05:00
}
2020-12-23 00:34:10 -05:00
2020-11-01 22:55:43 -05:00
}
}
return ruleResultList ;
}
/// <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>
2020-12-23 00:34:10 -05:00
private static string UpdateErrorMessage ( string errorMessage , IDictionary < string , object > inputs , string property , string typeName , string propertyName )
2020-11-01 22:55:43 -05:00
{
2020-12-23 00:34:10 -05:00
var arrParams = inputs ? . Select ( c = > new { Name = c . Key , c . Value } ) ;
2020-11-01 22:55:43 -05:00
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
}
}